From 0581fee6166c97626989b4e6fa50749d63b0ba3d Mon Sep 17 00:00:00 2001 From: E98Developer Date: Wed, 8 Apr 2026 21:47:28 +0200 Subject: [PATCH] ADD work_calendar api sync and test support --- .env.testing | 14 ++ app/Console/Commands/CheckTestDb.php | 58 ++++++ app/Console/Commands/SyncWorkCalendar.php | 44 +++++ app/Console/Kernel.php | 1 + app/Enums/DataSource.php | 17 ++ app/Enums/WorkDayType.php | 25 +++ app/Models/WorkCalendar.php | 34 ++++ app/Services/WorkCalendarService.php | 136 ++++++++++++++ config/logging.php | 7 + config/services.php | 6 + ..._08_184130_create_work_calendars_table.php | 39 ++++ phpunit.xml | 4 +- tests/Feature/WorkCalendarServiceTest.php | 173 ++++++++++++++++++ tests/TestCase.php | 11 +- 14 files changed, 566 insertions(+), 3 deletions(-) create mode 100644 .env.testing create mode 100644 app/Console/Commands/CheckTestDb.php create mode 100644 app/Console/Commands/SyncWorkCalendar.php create mode 100644 app/Enums/DataSource.php create mode 100644 app/Enums/WorkDayType.php create mode 100644 app/Models/WorkCalendar.php create mode 100644 app/Services/WorkCalendarService.php create mode 100644 database/migrations/2026_04_08_184130_create_work_calendars_table.php create mode 100644 tests/Feature/WorkCalendarServiceTest.php diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..d154f99 --- /dev/null +++ b/.env.testing @@ -0,0 +1,14 @@ +APP_ENV=testing +APP_KEY=base64:Ftjcs9gvv7IgMLvSzaCQIaqIPBFO2fezCFUb+owUSCE= +DB_CONNECTION=mysql +DB_DATABASE=d2dTest + +SZUNETNAPOK_API_KEY=test_api_key +SZUNETNAPOK_API_URL=https://szunetnapok.hu/api/ +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USERNAME=root +DB_PASSWORD= +CACHE_DRIVER=array +SESSION_DRIVER=array +QUEUE_CONNECTION=sync diff --git a/app/Console/Commands/CheckTestDb.php b/app/Console/Commands/CheckTestDb.php new file mode 100644 index 0000000..3be27f6 --- /dev/null +++ b/app/Console/Commands/CheckTestDb.php @@ -0,0 +1,58 @@ +newLine(); + $this->info('--- Adatbázis Kapcsolat Ellenőrzése ---'); + $this->info('APP_ENV: ' . app()->environment()); + $this->info('DB_CONNECTION: ' . config('database.default')); + $this->info('DB_DATABASE (config): ' . config('database.connections.mysql.database')); + $this->info('DB_DATABASE (env): ' . env('DB_DATABASE')); + + try { + $dbName = DB::select('SELECT DATABASE() as db')[0]->db; + $this->info('TÉNYLEGES ADATBÁZIS (MySQL select): ' . $dbName); + + if (app()->environment() === 'testing' && strtolower($dbName) !== strtolower('d2dTest')) { + $this->error('VIGYÁZAT: Teszt környezetben nem a d2dTest adatbázist használod! (Kapcsolódva: ' . $dbName . ')'); + } elseif (app()->environment() === 'testing' && strtolower($dbName) === strtolower('d2dTest')) { + $this->success('Minden rendben: Teszt környezetben a d2dTest adatbázist használod.'); + } + } catch (\Exception $e) { + $this->error('Hiba az adatbázishoz való csatlakozáskor: ' . $e->getMessage()); + } + $this->newLine(); + } + + /** + * Success message override + */ + protected function success(string $message) + { + $this->line("{$message}"); + } +} diff --git a/app/Console/Commands/SyncWorkCalendar.php b/app/Console/Commands/SyncWorkCalendar.php new file mode 100644 index 0000000..99e2cef --- /dev/null +++ b/app/Console/Commands/SyncWorkCalendar.php @@ -0,0 +1,44 @@ +argument('year') ? (int) $this->argument('year') : now()->year; + + $this->info("Szinkronizálás indítása {$year} évre..."); + + if ($service->syncFromApi($year)) { + $this->info('A szinkronizálás sikeresen befejeződött.'); + return self::SUCCESS; + } + + $this->error('A szinkronizálás során hiba történt:'); + foreach ($service->getError() as $error) { + $this->error("- " . implode(', ', (array) $error)); + } + + return self::FAILURE; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e6b9960..627d3e6 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule): void { // $schedule->command('inspire')->hourly(); + // $schedule->command('app:sync-work-calendar')->quarterly(); } /** diff --git a/app/Enums/DataSource.php b/app/Enums/DataSource.php new file mode 100644 index 0000000..d26d892 --- /dev/null +++ b/app/Enums/DataSource.php @@ -0,0 +1,17 @@ + 'API', + self::Manual => 'Kézi rögzítés', + }; + } +} diff --git a/app/Enums/WorkDayType.php b/app/Enums/WorkDayType.php new file mode 100644 index 0000000..72f9c53 --- /dev/null +++ b/app/Enums/WorkDayType.php @@ -0,0 +1,25 @@ + 'Munkaszüneti nap', + self::WorkingDay => 'Áthelyezett munkanap', + }; + } + + public function color(): string + { + return match($this) { + self::Holiday => 'danger', + self::WorkingDay => 'success', + }; + } +} diff --git a/app/Models/WorkCalendar.php b/app/Models/WorkCalendar.php new file mode 100644 index 0000000..8d8a3ad --- /dev/null +++ b/app/Models/WorkCalendar.php @@ -0,0 +1,34 @@ + 'date', + 'type' => WorkDayType::class, + 'data_source' => DataSource::class, + ]; + + /** + * Meghatározza, hogy az adott nap munkaszüneti nap-e az adatbázis szerint. + */ + public function isHoliday(): bool + { + return $this->type === WorkDayType::Holiday; + } + + /** + * Meghatározza, hogy az adott nap plusz munkanap-e az adatbázis szerint. + */ + public function isWorkingDay(): bool + { + return $this->type === WorkDayType::WorkingDay; + } +} diff --git a/app/Services/WorkCalendarService.php b/app/Services/WorkCalendarService.php new file mode 100644 index 0000000..1abc7e9 --- /dev/null +++ b/app/Services/WorkCalendarService.php @@ -0,0 +1,136 @@ +year; + $config = config('services.szunetnapok'); + $apiKey = $config['key'] ?? null; + $apiUrl = rtrim($config['url'] ?? 'https://szunetnapok.hu/api/', '/'); + $verify = $config['verify'] ?? true; + + Log::channel('work_calendar')->info('WorkCalendar sync started.', [ + 'year' => $year, + 'api_url' => $apiUrl, + 'has_api_key' => !empty($apiKey), + 'ssl_verify' => $verify, + ]); + + if (empty($apiKey)) { + $this->error->add('api', 'Hiányzó szunetnapok.hu API kulcs.'); + Log::channel('work_calendar')->error('WorkCalendar sync failed: Missing API key.'); + return false; + } + + try { + $response = Http::withOptions(['verify' => $verify]) + ->get("{$apiUrl}/{$apiKey}/{$year}/json"); + + Log::channel('work_calendar')->info('WorkCalendar API response.', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + if ($response->failed()) { + $this->error->add('api', 'Nem sikerült elérni a szunetnapok.hu API-t.'); + return false; + } + } catch (\Exception $e) { + $this->error->add('api', 'Hálózati hiba az API hívás közben: ' . $e->getMessage()); + Log::channel('work_calendar')->error('WorkCalendar sync API error (cURL/SSL):', [ + 'message' => $e->getMessage(), + 'exception' => get_class($e), + ]); + return false; + } + + $data = $response->json(); + + if (($data['response'] ?? '') !== 'OK') { + $this->error->add('api', $data['message'] ?? 'Hibás válasz az API-tól.'); + return false; + } + + if (!isset($data['days']) || !is_array($data['days'])) { + Log::channel('work_calendar')->info('WorkCalendar sync: No days to process.'); + return true; + } + + $savedRecords = []; + + foreach ($data['days'] as $day) { + $type = ($day['type'] == 1) ? WorkDayType::Holiday : WorkDayType::WorkingDay; + + $record = WorkCalendar::updateOrCreate( + ['date' => $day['date']], + [ + 'type' => $type, + 'description' => $day['name'] ?? '', + 'data_source' => DataSource::Api, + ] + ); + + $savedRecords[] = $record->toArray(); + } + + Log::channel('work_calendar')->info('WorkCalendar sync completed.', [ + 'count' => count($savedRecords), + 'records' => $savedRecords, + ]); + + return true; + } + + /** + * Megállapítja, hogy az adott nap munkanap-e. + */ + public function isWorkDay(Carbon $date): bool + { + $record = WorkCalendar::where('date', $date->toDateString())->first(); + + if ($record) { + return $record->type === WorkDayType::WorkingDay; + } + + // Ha nincs az adatbázisban, a hétvégék alapján döntünk + return !$date->isWeekend(); + } + + /** + * Kiszámítja a következő érvényes munkanapot. + */ + public function getNextWorkDay(Carbon $date): Carbon + { + $nextDay = $date->copy()->addDay(); + + while (!$this->isWorkDay($nextDay)) { + $nextDay->addDay(); + } + + return $nextDay; + } + + /** + * Visszaadja a megadott időszak munkaszüneti napjait és plusz munkanapjait. + */ + public function getCalendarEntries(Carbon $start, Carbon $end): Collection + { + return WorkCalendar::whereBetween('date', [$start->toDateString(), $end->toDateString()]) + ->orderBy('date') + ->get(); + } +} diff --git a/config/logging.php b/config/logging.php index c473c48..574f1d1 100644 --- a/config/logging.php +++ b/config/logging.php @@ -81,6 +81,13 @@ // 'formatter'=>\Monolog\Formatter\HtmlFormatter::class, ], + 'work_calendar' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/WorkCalendarAPI.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 14, + ], + 'BrowserConsole' => [ 'driver' => 'monolog', 'handler' => \Monolog\Handler\BrowserConsoleHandler::class, diff --git a/config/services.php b/config/services.php index 6a90eb8..9b85fa7 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,10 @@ ], ], + 'szunetnapok' => [ + 'key' => env('SZUNETNAPOK_API_KEY'), + 'url' => env('SZUNETNAPOK_API_URL', 'https://szunetnapok.hu/api/'), + 'verify' => env('SZUNETNAPOK_API_VERIFY', true), + ], + ]; diff --git a/database/migrations/2026_04_08_184130_create_work_calendars_table.php b/database/migrations/2026_04_08_184130_create_work_calendars_table.php new file mode 100644 index 0000000..9235bbe --- /dev/null +++ b/database/migrations/2026_04_08_184130_create_work_calendars_table.php @@ -0,0 +1,39 @@ +id(); + $table->date('date')->unique(); + $table->enum('type', array_column(\App\Enums\WorkDayType::cases(), 'value')); + $table->enum('data_source', array_column(\App\Enums\DataSource::cases(), 'value'))->default(\App\Enums\DataSource::Manual->value); + $table->text('description')->nullable(); + + // Audit mezők a BaseAuditable-hoz + $table->unsignedBigInteger('created_by')->nullable(); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->unsignedBigInteger('deleted_by')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('work_calendars'); + } +}; diff --git a/phpunit.xml b/phpunit.xml index d703241..461fb27 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,8 +23,8 @@ - - + + diff --git a/tests/Feature/WorkCalendarServiceTest.php b/tests/Feature/WorkCalendarServiceTest.php new file mode 100644 index 0000000..e565a70 --- /dev/null +++ b/tests/Feature/WorkCalendarServiceTest.php @@ -0,0 +1,173 @@ +isWorkDay($saturday))->toBeFalse(); + + // 2026-04-13 Hétfő (Alapesetben munkanap) + $monday = Carbon::parse('2026-04-13'); + expect($service->isWorkDay($monday))->toBeTrue(); + + // Rögzítsünk egy szünnapot a hétfőre + WorkCalendar::create([ + 'date' => '2026-04-13', + 'type' => WorkDayType::Holiday, + 'description' => 'Test Holiday' + ]); + + expect($service->isWorkDay($monday))->toBeFalse(); + + // Rögzítsünk egy munkanapot a szombatra + WorkCalendar::create([ + 'date' => '2026-04-11', + 'type' => WorkDayType::WorkingDay, + 'description' => 'Test Working Saturday' + ]); + + expect($service->isWorkDay($saturday))->toBeTrue(); +}); + +test('it calculates next working day correctly', function () { + $service = new WorkCalendarService(); + + // 2026-04-10 Péntek + $friday = Carbon::parse('2026-04-10'); + + // Alapesetben a hétvége miatt hétfő lesz a következő munkanap + expect($service->getNextWorkDay($friday)->toDateString())->toBe('2026-04-13'); + + // Ha a szombat munkanap lesz + WorkCalendar::create([ + 'date' => '2026-04-11', + 'type' => WorkDayType::WorkingDay, + 'description' => 'Working Saturday' + ]); + + expect($service->getNextWorkDay($friday)->toDateString())->toBe('2026-04-11'); + + // Ha a hétfő szünnap lesz + WorkCalendar::create([ + 'date' => '2026-04-13', + 'type' => WorkDayType::Holiday, + 'description' => 'Monday Holiday' + ]); + + // Péntek után szombat munkanap, vasárnap nem, hétfő szünnap -> kedd (04-14) + expect($service->getNextWorkDay($friday)->toDateString())->toBe('2026-04-11'); + + $saturday = Carbon::parse('2026-04-11'); + expect($service->getNextWorkDay($saturday)->toDateString())->toBe('2026-04-14'); +}); + +test('it can sync holidays from API', function () { + $service = new WorkCalendarService(); + + Http::fake([ + 'szunetnapok.hu/api/*' => Http::response([ + 'response' => 'OK', + 'year' => 2026, + 'days' => [ + [ + 'date' => '2026-01-01', + 'name' => 'Új Év napja', + 'type' => 1, + 'weekday' => 5 + ], + [ + 'date' => '2026-05-10', + 'name' => 'Áthelyezett munkanap', + 'type' => 2, + 'weekday' => 6 + ] + ] + ], 200) + ]); + + $result = $service->syncFromApi(2026); + + expect($result)->toBeTrue(); + expect(WorkCalendar::count())->toBe(2); + + $holiday = WorkCalendar::where('date', '2026-01-01')->first(); + expect($holiday->type)->toBe(WorkDayType::Holiday); + expect($holiday->data_source)->toBe(\App\Enums\DataSource::Api); + + $workingDay = WorkCalendar::where('date', '2026-05-10')->first(); + expect($workingDay->type)->toBe(WorkDayType::WorkingDay); + expect($workingDay->data_source)->toBe(\App\Enums\DataSource::Api); +}); + +test('it handles API errors during sync', function () { + $service = new WorkCalendarService(); + + Http::fake([ + 'szunetnapok.hu/api/*' => Http::response([ + 'response' => 'Error', + 'message' => 'Nincs API kulcs megadva' + ], 200) + ]); + + $result = $service->syncFromApi(2026); + + expect($result)->toBeFalse(); + expect($service->getError()['api'][0])->toBe('Nincs API kulcs megadva'); +}); + +test('it logs API calls and database entries during sync', function () { + $service = new WorkCalendarService(); + + Http::fake([ + 'szunetnapok.hu/api/*' => Http::response([ + 'response' => 'OK', + 'days' => [ + [ + 'date' => '2026-01-01', + 'name' => 'Új Év', + 'type' => 1 + ] + ] + ], 200) + ]); + + $mock = Log::partialMock(); + $mock->shouldReceive('channel')->with('work_calendar')->andReturnSelf(); + $mock->shouldReceive('info')->atLeast()->once(); + + $service->syncFromApi(2026); +}); + +test('it logs cURL/SSL exceptions during sync', function () { + $service = new WorkCalendarService(); + + Http::fake([ + 'szunetnapok.hu/api/*' => function () { + throw new \Exception('cURL error 60: SSL certificate problem'); + } + ]); + + $mock = Log::partialMock(); + $mock->shouldReceive('channel')->with('work_calendar')->andReturnSelf(); + $mock->shouldReceive('info')->once(); // Start log + $mock->shouldReceive('error')->once()->withArgs(function ($message, $context) { + return str_contains($message, 'cURL/SSL') && + str_contains($context['message'], 'SSL certificate problem'); + }); + + $result = $service->syncFromApi(2026); + + expect($result)->toBeFalse(); + expect($service->getError()['api'][0])->toContain('Hálózati hiba'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index fe1ffc2..89b2649 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,5 +6,14 @@ abstract class TestCase extends BaseTestCase { - // + protected function setUp(): void + { + parent::setUp(); + + $currentDb = config('database.connections.mysql.database'); + + if (strtolower($currentDb) !== strtolower('d2dTest') && env('APP_ENV') === 'testing') { + throw new \Exception('KRITIKUS HIBA: A tesztek NEM a d2dTest adatbázison futnak! Aktuális adatbázis: ' . $currentDb); + } + } }