---
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
---

<objective>
## 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)
</objective>

<context>
## 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
</context>

<skills>
## Required Skills (from SPECIAL-FLOWS.md)

| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |

</skills>

<acceptance_criteria>

## 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)
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Migracja DB — tabele print_jobs i print_api_keys</name>
  <files>database/migrations/20260322_000058_create_print_tables.sql</files>
  <action>
    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).
  </action>
  <verify>Uruchomić migrację i sprawdzić SHOW CREATE TABLE print_jobs; SHOW CREATE TABLE print_api_keys;</verify>
  <done>AC-1 satisfied: tabele istnieją z poprawnymi kolumnami i indeksami</done>
</task>

<task type="auto">
  <name>Task 2: Repozytoria i kontrolery — PrintJobRepository, PrintApiKeyRepository, ApiKeyMiddleware, PrintApiController, PrintSettingsController</name>
  <files>
    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
  </files>
  <action>
    **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
  </action>
  <verify>
    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
  </verify>
  <done>AC-2 partially (backend), AC-3, AC-4, AC-5, AC-6 satisfied: API endpoints działają z prawidłowym auth</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <what-built>
    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)
  </what-built>
  <how-to-verify>
    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
  </how-to-verify>
  <resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>

</tasks>

<boundaries>

## 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)

</boundaries>

<verification>
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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.paul/phases/18-print-queue-backend/18-01-SUMMARY.md`
</output>
