--- phase: 18-print-queue-backend plan: 01 type: execute wave: 1 depends_on: [] files_modified: - database/migrations/20260322_000058_create_print_tables.sql - src/Modules/Printing/PrintJobRepository.php - src/Modules/Printing/PrintApiKeyRepository.php - src/Modules/Printing/PrintApiController.php - src/Modules/Printing/ApiKeyMiddleware.php - src/Modules/Settings/PrintSettingsController.php - routes/web.php autonomous: false --- ## Goal Stworzyć backend do zdalnego drukowania etykiet: tabele DB (print_jobs, print_api_keys), REST API z uwierzytelnianiem kluczem API, oraz CRUD kluczy API w ustawieniach. ## Purpose Fundament pod system zdalnego drukowania — aplikacja Windows (faza 20) będzie odpytywać te endpointy aby pobierać zlecenia wydruku i drukować etykiety na drukarce termicznej. ## Output - Migracja SQL: tabele print_jobs i print_api_keys - Moduł Printing: PrintJobRepository, PrintApiKeyRepository, PrintApiController - ApiKeyMiddleware do uwierzytelniania requestów z aplikacji Windows - PrintSettingsController: CRUD kluczy API w ustawieniach - REST API: 4 endpointy (create job, list pending, download label, mark complete) ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Source Files @src/Core/Routing/Router.php @src/Modules/Auth/AuthMiddleware.php @src/Modules/Shipments/ShipmentController.php @routes/web.php ## Required Skills (from SPECIAL-FLOWS.md) | Skill | Priority | When to Invoke | Loaded? | |-------|----------|----------------|---------| | sonar-scanner | required | Po APPLY, przed UNIFY | ○ | ## AC-1: Tabele DB utworzone poprawnie ```gherkin Given czysta baza danych When uruchomię migrację 20260322_000058_create_print_tables.sql Then tabele print_jobs i print_api_keys istnieją z poprawnymi kolumnami i indeksami ``` ## AC-2: CRUD kluczy API w ustawieniach ```gherkin Given użytkownik jest zalogowany i otwiera Ustawienia > Drukowanie When tworzy nowy klucz API (podaje nazwę) Then klucz jest generowany, wyświetlany jednorazowo, zapisany w DB (hash) And klucz można dezaktywować i usunąć ``` ## AC-3: API — tworzenie zlecenia wydruku ```gherkin Given istnieje etykieta przesyłki (label_path w shipment_packages) When użytkownik orderPRO wyśle POST /api/print/jobs z session auth Then zlecenie wydruku zostaje utworzone ze statusem 'pending' And odpowiedź zawiera ID zlecenia ``` ## AC-4: API — pobieranie zleceń i etykiet przez klienta ```gherkin Given klient Windows uwierzytelnia się kluczem API (header X-Api-Key) When wyśle GET /api/print/jobs/pending Then otrzyma listę zleceń ze statusem 'pending' And gdy wyśle GET /api/print/jobs/{id}/download Then otrzyma plik PDF etykiety ``` ## AC-5: API — oznaczanie zlecenia jako wydrukowane ```gherkin Given klient Windows pobrał i wydrukował etykietę When wyśle PATCH /api/print/jobs/{id}/complete z kluczem API Then zlecenie zmieni status na 'completed' z timestampem completed_at ``` ## AC-6: Nieprawidłowy klucz API odrzucony ```gherkin Given request z nieprawidłowym lub brakującym kluczem API When klient wyśle request do /api/print/* Then odpowiedź to 401 Unauthorized (JSON) ``` Task 1: Migracja DB — tabele print_jobs i print_api_keys database/migrations/20260322_000058_create_print_tables.sql Utworzyć migrację SQL z dwiema tabelami: **print_api_keys:** - `id` INT AUTO_INCREMENT PRIMARY KEY - `name` VARCHAR(128) NOT NULL — nazwa przyjazna ("Komputer biurowy") - `key_hash` VARCHAR(128) NOT NULL — hash klucza (SHA-256) - `key_prefix` VARCHAR(8) NOT NULL — pierwsze 8 znaków klucza (do identyfikacji) - `is_active` TINYINT(1) NOT NULL DEFAULT 1 - `last_used_at` DATETIME NULL - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP Indeksy: UNIQUE na key_hash, INDEX na is_active. **print_jobs:** - `id` INT AUTO_INCREMENT PRIMARY KEY - `order_id` BIGINT NOT NULL — FK → orders.id ON DELETE CASCADE - `package_id` INT NOT NULL — FK → shipment_packages.id ON DELETE CASCADE - `label_path` VARCHAR(255) NOT NULL — ścieżka do pliku PDF - `status` ENUM('pending', 'printing', 'completed', 'failed') NOT NULL DEFAULT 'pending' - `created_by` INT NOT NULL — kto zlecił wydruk - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP - `completed_at` DATETIME NULL Indeksy: INDEX na status, INDEX na order_id, INDEX na package_id. Konwencja: zgodna z istniejącymi migracjami (plik .sql, plain DDL). Uruchomić migrację i sprawdzić SHOW CREATE TABLE print_jobs; SHOW CREATE TABLE print_api_keys; AC-1 satisfied: tabele istnieją z poprawnymi kolumnami i indeksami Task 2: Repozytoria i kontrolery — PrintJobRepository, PrintApiKeyRepository, ApiKeyMiddleware, PrintApiController, PrintSettingsController src/Modules/Printing/PrintJobRepository.php, src/Modules/Printing/PrintApiKeyRepository.php, src/Modules/Printing/PrintApiController.php, src/Modules/Printing/ApiKeyMiddleware.php, src/Modules/Settings/PrintSettingsController.php, routes/web.php **PrintApiKeyRepository** (src/Modules/Printing/): - `create(string $name, string $keyHash, string $keyPrefix): int` — INSERT, zwraca ID - `findByKeyHash(string $keyHash): ?array` — szukanie po hashu (do auth) - `listAll(): array` — lista kluczy (bez hashy, z prefixem) - `deactivate(int $id): void` — SET is_active = 0 - `delete(int $id): void` — DELETE - `updateLastUsed(int $id): void` — SET last_used_at = NOW() - Używać PDO + prepared statements (wzorzec jak ReceiptRepository) **PrintJobRepository** (src/Modules/Printing/): - `create(array $data): int` — INSERT, zwraca ID - `findPending(): array` — SELECT WHERE status = 'pending' ORDER BY created_at ASC - `findById(int $id): ?array` — z JOIN na orders (internal_order_number) i shipment_packages (tracking_number) - `markCompleted(int $id): void` — SET status = 'completed', completed_at = NOW() - `markFailed(int $id, string $reason): void` — SET status = 'failed' - Używać PDO + prepared statements **ApiKeyMiddleware** (src/Modules/Printing/): - Callable `__invoke(Request $request, callable $next): Response` - Odczytuje header `X-Api-Key` z requestu - Hashuje klucz (SHA-256), szuka w print_api_keys przez PrintApiKeyRepository - Jeśli znaleziony i is_active = 1: updateLastUsed(), przekazuje do $next - Jeśli brak/nieaktywny: zwraca Response::json(['error' => 'Unauthorized'], 401) - Wzorzec: analogiczny do AuthMiddleware ale dla API **PrintApiController** (src/Modules/Printing/): - Constructor DI: PrintJobRepository, Request, AuthService - `createJob(Request $request): Response` — POST /api/print/jobs - Wymaga session auth (AuthMiddleware) — wywoływane z UI orderPRO - Przyjmuje: package_id (required) - Waliduje: shipment_packages.label_path istnieje i plik fizycznie istnieje - Tworzy print_job ze statusem 'pending' - Zwraca JSON: { id, status: 'pending' } - `listPending(Request $request): Response` — GET /api/print/jobs/pending - Wymaga API key (ApiKeyMiddleware) - Zwraca JSON: lista pending jobs z: id, order_number, tracking_number, created_at - `downloadLabel(Request $request): Response` — GET /api/print/jobs/{id}/download - Wymaga API key - Waliduje: job istnieje, plik istnieje - Zwraca plik PDF (Content-Type: application/pdf) - `markComplete(Request $request): Response` — PATCH /api/print/jobs/{id}/complete - Wymaga API key - Oznacza job jako completed - Zwraca JSON: { id, status: 'completed' } **PrintSettingsController** (src/Modules/Settings/): - `index(Request $request): Response` — lista kluczy API (widok settings/printing) - `createKey(Request $request): Response` — POST: generuje klucz (bin2hex(random_bytes(32))), hashuje SHA-256, zapisuje hash+prefix, zwraca klucz jednorazowo we flash message - `deleteKey(Request $request): Response` — POST: usuwa klucz **Routes** (routes/web.php): Dodać na końcu pliku: ```php // Print API — session auth (from orderPRO UI) $router->post('/api/print/jobs', [$printApiController, 'createJob'], [$authMiddleware]); // Print API — API key auth (from Windows client) $router->get('/api/print/jobs/pending', [$printApiController, 'listPending'], [$apiKeyMiddleware]); $router->get('/api/print/jobs/{id}/download', [$printApiController, 'downloadLabel'], [$apiKeyMiddleware]); $router->post('/api/print/jobs/{id}/complete', [$printApiController, 'markComplete'], [$apiKeyMiddleware]); // Print settings $router->get('/settings/printing', [$printSettingsController, 'index'], [$authMiddleware]); $router->post('/settings/printing/keys/create', [$printSettingsController, 'createKey'], [$authMiddleware]); $router->post('/settings/printing/keys/{id}/delete', [$printSettingsController, 'deleteKey'], [$authMiddleware]); ``` DI: Zarejestrować nowe klasy w Application.php (lub tam gdzie inne moduły są rejestrowane). Avoid: - Nie używać PATCH (router może nie obsługiwać) — użyć POST dla markComplete - Nie sklejać SQL — tylko prepared statements - Nie przechowywać klucza API w plaintext — tylko hash SHA-256 1. Sprawdzić czy pliki PHP nie mają błędów składni: php -l na każdym pliku 2. Sprawdzić czy routes/web.php parsuje się poprawnie AC-2 partially (backend), AC-3, AC-4, AC-5, AC-6 satisfied: API endpoints działają z prawidłowym auth Backend do zdalnego drukowania: - Tabele DB: print_jobs, print_api_keys - REST API: 4 endpointy (create job, list pending, download, complete) - API key middleware (X-Api-Key header) - CRUD kluczy API w ustawieniach (backend only — widok w fazie 19) 1. Uruchom aplikację — sprawdź czy migracja przeszła (brak błędów) 2. Otwórz /settings/printing — powinien załadować się widok (nawet pusty) 3. Opcjonalnie: testuj API curlem Type "approved" to continue, or describe issues to fix ## DO NOT CHANGE - src/Modules/Shipments/* (nie modyfikować istniejącego flow etykiet) - src/Modules/Auth/AuthMiddleware.php (nie zmieniać session auth) - src/Core/Routing/Router.php (nie modyfikować routera) - database/migrations/ istniejące pliki (nie zmieniać) ## SCOPE LIMITS - Tylko backend — brak widoków HTML (oprócz minimalnego dla settings/printing) - Brak integracji z przyciskiem "Drukuj" w widoku przesyłek (faza 19) - Brak aplikacji Windows (faza 20) - Widok settings/printing: minimalna lista kluczy + formularz tworzenia (pełny design w fazie 19) Before declaring plan complete: - [ ] Migracja SQL wykonuje się bez błędów - [ ] php -l bez błędów na wszystkich nowych plikach - [ ] GET /api/print/jobs/pending bez klucza → 401 - [ ] POST /settings/printing/keys/create tworzy klucz - [ ] Routing nie psuje istniejących endpointów - [ ] All acceptance criteria met - All tasks completed - All verification checks pass - No errors or warnings introduced - API key auth działa (valid key → 200, invalid → 401) - Print jobs CRUD przez API After completion, create `.paul/phases/18-print-queue-backend/18-01-SUMMARY.md`