--- phase: 27-shipment-tracking-backend plan: 01 type: execute wave: 1 depends_on: [] files_modified: - database/migrations/20260323_000042_add_delivery_tracking_columns.sql - src/Modules/Shipments/DeliveryStatus.php - src/Modules/Shipments/ShipmentTrackingInterface.php - src/Modules/Shipments/InpostTrackingService.php - src/Modules/Shipments/ApaczkaTrackingService.php - src/Modules/Shipments/AllegroTrackingService.php - src/Modules/Shipments/ShipmentTrackingRegistry.php - src/Modules/Shipments/ShipmentPackageRepository.php - src/Modules/Cron/ShipmentTrackingHandler.php - src/Modules/Cron/CronHandlerFactory.php - database/migrations/20260323_000043_add_shipment_tracking_cron_schedule.sql autonomous: true --- ## Goal Dodać infrastrukturę backendową do śledzenia statusu dostawy przesyłek — migracja DB, interfejs trackingu, implementacje dla 3 providerów (InPost, Apaczka, Allegro WZA), rejestr trackingu i cron handler odpytujący statusy cyklicznie. ## Purpose Sprzedawca musi widzieć aktualny status dostawy przesyłki bez wchodzenia na stronę przewoźnika. Śledzenie automatyczne przez cron eliminuje ręczne sprawdzanie. ## Output - 3 nowe kolumny w `shipment_packages`: `delivery_status`, `delivery_status_raw`, `delivery_status_updated_at` - Klasa `DeliveryStatus` z mapowaniami statusów per provider - `ShipmentTrackingInterface` + 3 implementacje (InPost, Apaczka, Allegro) - `ShipmentTrackingRegistry` — rejestr providerów trackingu - `ShipmentTrackingHandler` — cron handler - Nowy wpis w `cron_schedules` dla `shipment_tracking_sync` ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Source Files @src/Modules/Shipments/ShipmentProviderInterface.php @src/Modules/Shipments/ShipmentPackageRepository.php @src/Modules/Shipments/InpostShipmentService.php @src/Modules/Shipments/ApaczkaShipmentService.php @src/Modules/Shipments/AllegroShipmentService.php @src/Modules/Settings/ApaczkaApiClient.php @src/Modules/Settings/AllegroApiClient.php @src/Modules/Cron/CronHandlerFactory.php @src/Modules/Cron/CronRunner.php ## Reference @DOCS/SHIPMENT_TRACKING_STATUSES.md ## Required Skills (from SPECIAL-FLOWS.md) | Skill | Priority | When to Invoke | Loaded? | |-------|----------|----------------|---------| | sonar-scanner | required | Po APPLY, przed UNIFY | ○ | ## Skill Invocation Checklist - [ ] sonar-scanner uruchomiony po zakończeniu APPLY ## AC-1: Kolumny delivery tracking w bazie danych ```gherkin Given tabela shipment_packages istnieje When migracja 000042 zostanie uruchomiona Then kolumny delivery_status (VARCHAR(32) DEFAULT 'unknown'), delivery_status_raw (VARCHAR(128) NULL), delivery_status_updated_at (DATETIME NULL) istnieją w tabeli shipment_packages And indeks na delivery_status istnieje ``` ## AC-2: DeliveryStatus mapuje statusy providerów na znormalizowane ```gherkin Given klasa DeliveryStatus z mapowaniami per provider When wywołam DeliveryStatus::normalize('inpost', 'adopted_at_sorting_center') Then zwróci 'in_transit' And DeliveryStatus::normalize('apaczka', '5') zwróci 'delivered' And DeliveryStatus::normalize('allegro_wza', 'IN_TRANSIT') zwróci 'in_transit' And nieznany status zwróci 'unknown' ``` ## AC-3: ShipmentTrackingInterface i implementacje providerów ```gherkin Given interfejs ShipmentTrackingInterface z metodą getDeliveryStatus(array $package): ?array When wywołam InpostTrackingService::getDeliveryStatus() dla paczki z shipment_id Then zwróci ['status' => 'in_transit', 'status_raw' => 'adopted_at_sorting_center', 'description' => 'Przyjęta w centrum sortowania'] And ApaczkaTrackingService odpyta GET /order/{id}/ i zwróci zmapowany status And AllegroTrackingService odpyta GET /shipment-management/shipments/{id} i zwróci zmapowany status ``` ## AC-4: Cron handler odpytuje aktywne przesyłki ```gherkin Given istnieją przesyłki ze statusem delivery_status != 'delivered' AND delivery_status != 'returned' AND delivery_status != 'cancelled' AND status IN ('created', 'label_ready') When cron job shipment_tracking_sync się uruchomi Then każda aktywna przesyłka (nie-manual) zostanie odpytana przez odpowiedni tracking service And delivery_status, delivery_status_raw, delivery_status_updated_at zostaną zaktualizowane w bazie And przesyłki manual zostaną pominięte And błędy pojedynczej przesyłki nie blokują przetwarzania pozostałych ``` ## AC-5: Cron schedule zarejestrowany ```gherkin Given migracja 000043 uruchomiona When sprawdzę tabelę cron_schedules Then istnieje wpis job_type='shipment_tracking_sync', interval_seconds=900 (15 min), enabled=1 And CronHandlerFactory zwraca handler dla 'shipment_tracking_sync' ``` Task 1: Migracja DB — kolumny delivery tracking + cron schedule database/migrations/20260323_000042_add_delivery_tracking_columns.sql, database/migrations/20260323_000043_add_shipment_tracking_cron_schedule.sql Migracja 000042: - ALTER TABLE shipment_packages ADD COLUMN delivery_status VARCHAR(32) NOT NULL DEFAULT 'unknown' AFTER status - ALTER TABLE shipment_packages ADD COLUMN delivery_status_raw VARCHAR(128) NULL AFTER delivery_status - ALTER TABLE shipment_packages ADD COLUMN delivery_status_updated_at DATETIME NULL AFTER delivery_status_raw - ADD INDEX idx_delivery_status (delivery_status) - UPDATE shipment_packages SET delivery_status = 'delivered' WHERE status = 'label_ready' — istniejące z etykietą traktuj jako dostarczone (nie mamy historii) - UPDATE shipment_packages SET delivery_status = 'confirmed' WHERE status = 'created' AND provider != 'manual' — istniejące utworzone, ale bez etykiety - UPDATE shipment_packages SET delivery_status = 'unknown' WHERE provider = 'manual' — manual bez auto-trackingu Migracja 000043: - INSERT INTO cron_schedules (job_type, interval_seconds, priority, max_attempts, enabled) VALUES ('shipment_tracking_sync', 900, 5, 3, 1) - 900 sekund = 15 minut domyślny interwał Avoid: nie zmieniaj istniejących kolumn — tylko ADD nowe Uruchom migracje na bazie i sprawdź DESCRIBE shipment_packages — 3 nowe kolumny widoczne; SELECT * FROM cron_schedules WHERE job_type = 'shipment_tracking_sync' — 1 wiersz AC-1 i AC-5 (część DB) satisfied Task 2: DeliveryStatus + ShipmentTrackingInterface + 3 implementacje + Registry src/Modules/Shipments/DeliveryStatus.php, src/Modules/Shipments/ShipmentTrackingInterface.php, src/Modules/Shipments/InpostTrackingService.php, src/Modules/Shipments/ApaczkaTrackingService.php, src/Modules/Shipments/AllegroTrackingService.php, src/Modules/Shipments/ShipmentTrackingRegistry.php **DeliveryStatus** (final class, stałe + static metody): - Stałe: UNKNOWN = 'unknown', CREATED = 'created', CONFIRMED = 'confirmed', IN_TRANSIT = 'in_transit', OUT_FOR_DELIVERY = 'out_for_delivery', READY_FOR_PICKUP = 'ready_for_pickup', DELIVERED = 'delivered', RETURNED = 'returned', CANCELLED = 'cancelled', PROBLEM = 'problem' - TERMINAL_STATUSES = [DELIVERED, RETURNED, CANCELLED] — statusy końcowe, nie odpytywać więcej - LABEL_PL: tablica status → polski label (np. 'in_transit' => 'W tranzycie') - Mapy per provider: INPOST_MAP, APACZKA_MAP, ALLEGRO_MAP — surowy status → znormalizowany - `normalize(string $provider, string $rawStatus): string` — zwraca zmapowany status lub UNKNOWN - `label(string $status): string` — zwraca polską nazwę - `isTerminal(string $status): bool` — czy status końcowy - Mapowania zgodne z DOCS/SHIPMENT_TRACKING_STATUSES.md **ShipmentTrackingInterface**: ```php interface ShipmentTrackingInterface { public function supports(string $provider): bool; public function getDeliveryStatus(array $package): ?array; // Zwraca: ['status' => string, 'status_raw' => string, 'description' => string] lub null przy błędzie } ``` **InpostTrackingService** (implements ShipmentTrackingInterface): - Constructor: InpostIntegrationSettings (reuse istniejącej klasy/repo z konfiguracją InPost) - supports('inpost') → true - getDeliveryStatus(): GET /v1/organizations/{orgId}/shipments/{shipmentId} z Bearer token - Odczytaj pole 'status' z odpowiedzi API - Zmapuj przez DeliveryStatus::normalize('inpost', $rawStatus) - Zwróć ['status' => normalized, 'status_raw' => raw, 'description' => opis z INPOST_DESCRIPTIONS] - Reuse HTTP client pattern z InpostShipmentService (cURL + Bearer auth) **ApaczkaTrackingService** (implements ShipmentTrackingInterface): - Constructor: ApaczkaApiClient (reuse istniejącego klienta) - supports('apaczka') → true - getDeliveryStatus(): użyj ApaczkaApiClient::getOrderDetails() z shipment_id jako orderId - Odczytaj pole 'status' (integer) z odpowiedzi - Zmapuj przez DeliveryStatus::normalize('apaczka', (string)$status) **AllegroTrackingService** (implements ShipmentTrackingInterface): - Constructor: AllegroApiClient, AllegroIntegrationRepository, AllegroTokenManager (reuse) - supports('allegro_wza') → true - getDeliveryStatus(): GET /shipment-management/shipments/{shipmentId} z OAuth Bearer - Potrzebuje credentials z integracji Allegro (pobranie tokena przez TokenManager) - Odczytaj pole 'status' z odpowiedzi - Zmapuj przez DeliveryStatus::normalize('allegro_wza', $rawStatus) - Uwaga: shipment_id w package to Allegro shipmentId **ShipmentTrackingRegistry** (final class): - Constructor: array $services (ShipmentTrackingInterface[]) - getForProvider(string $provider): ?ShipmentTrackingInterface — iteruj services, zwróć pierwszy supports($provider) Avoid: - NIE modyfikuj istniejących klas shipment services — tracking to NOWY interfejs - NIE dodawaj nowych zależności composer — reuse istniejących HTTP klientów (cURL) - Klasy final class, strict_types=1 PHP syntax check: php -l na każdym nowym pliku Sprawdź że DeliveryStatus::normalize('inpost', 'delivered') === 'delivered' Sprawdź że DeliveryStatus::normalize('apaczka', '5') === 'delivered' Sprawdź że DeliveryStatus::normalize('allegro_wza', 'DELIVERED') === 'delivered' Sprawdź że DeliveryStatus::isTerminal('delivered') === true AC-2 i AC-3 satisfied: statusy mapowane poprawnie, 3 implementacje trackingu gotowe Task 3: ShipmentTrackingHandler cron + rejestracja w CronHandlerFactory + rozszerzenie ShipmentPackageRepository src/Modules/Cron/ShipmentTrackingHandler.php, src/Modules/Cron/CronHandlerFactory.php, src/Modules/Shipments/ShipmentPackageRepository.php **ShipmentPackageRepository — nowe metody**: - `findActiveForTracking(): array` — SELECT * FROM shipment_packages WHERE provider != 'manual' AND delivery_status NOT IN ('delivered', 'returned', 'cancelled') AND status IN ('created', 'label_ready') ORDER BY delivery_status_updated_at ASC NULLS FIRST LIMIT 50 - `updateDeliveryStatus(int $id, string $deliveryStatus, ?string $deliveryStatusRaw): void` — UPDATE delivery_status, delivery_status_raw, delivery_status_updated_at = NOW() - Dodaj 'delivery_status', 'delivery_status_raw' do allowedFields w update() method **ShipmentTrackingHandler** (implements CronHandlerInterface): - Constructor: ShipmentTrackingRegistry $registry, ShipmentPackageRepository $repository - handle(): void: 1. Pobierz aktywne paczki: $repository->findActiveForTracking() 2. Dla każdej paczki: a. $service = $registry->getForProvider($package['provider']) b. Jeśli null → skip (provider bez trackingu) c. try { $result = $service->getDeliveryStatus($package) } d. Jeśli $result !== null → $repository->updateDeliveryStatus($id, $result['status'], $result['status_raw']) e. catch (Throwable $e) → log warning, kontynuuj z następną paczką 3. Log: "Shipment tracking sync: {processed}/{total} packages updated" **CronHandlerFactory — rejestracja**: - Dodaj nowy handler 'shipment_tracking_sync' do mapy handlerów - Potrzebne zależności: ShipmentPackageRepository, ShipmentTrackingRegistry z 3 services - Stwórz instancje: InpostTrackingService, ApaczkaTrackingService, AllegroTrackingService - Reuse istniejących instancji: $integrationRepository, $tokenManager, $apiClient (Allegro), ApaczkaApiClient - Do InpostTrackingService potrzebny dostęp do inpost_integration_settings — pobierz przez PDO query lub nowy helper - Zbuduj ShipmentTrackingRegistry z tablicą services - Zbuduj ShipmentTrackingHandler z registry + repository Avoid: - NIE usuwaj istniejących handlerów z CronHandlerFactory - NIE zmieniaj sygnatur istniejących metod ShipmentPackageRepository - Błąd jednej paczki NIE blokuje przetwarzania pozostałych (try/catch w pętli) - LIMIT 50 w findActiveForTracking() zapobiega przeciążeniu przy dużej liczbie paczek php -l na zmienionych plikach Sprawdź że CronHandlerFactory::build() zwraca CronRunner z 7 handlerami (6 istniejących + 1 nowy) Sprawdź SQL query findActiveForTracking() — poprawna składnia AC-4 i AC-5 satisfied: cron handler przetwarza aktywne przesyłki, schedule zarejestrowany ## DO NOT CHANGE - src/Modules/Shipments/ShipmentProviderInterface.php (nie modyfikuj istniejącego interfejsu) - src/Modules/Shipments/InpostShipmentService.php (nie modyfikuj — tracking to osobny service) - src/Modules/Shipments/ApaczkaShipmentService.php (nie modyfikuj) - src/Modules/Shipments/AllegroShipmentService.php (nie modyfikuj) - src/Modules/Cron/CronRunner.php (nie modyfikuj) - database/migrations/20260305_000033_create_shipment_packages_table.sql (nie modyfikuj oryginalnej migracji) - resources/views/* (UI w fazie 28) ## SCOPE LIMITS - Ten plan dotyczy TYLKO backendu — bez zmian w UI/widokach - Nie dodawaj nowych zależności composer - Nie modyfikuj istniejących cron handlerów - Nie modyfikuj routingu (routes/web.php) — API endpointy trackingu w fazie 28 - Konfiguracja interwału crona w ustawieniach — w fazie 28 (na razie hardcoded 900s w migracji) Before declaring plan complete: - [ ] Migracje 000042 i 000043 uruchomione bez błędów - [ ] php -l przechodzi na wszystkich nowych i zmodyfikowanych plikach - [ ] DeliveryStatus::normalize() mapuje poprawnie dla wszystkich 3 providerów - [ ] DeliveryStatus::isTerminal() zwraca true dla delivered/returned/cancelled - [ ] ShipmentPackageRepository::findActiveForTracking() zwraca tylko nie-manual, nie-terminalne paczki - [ ] CronHandlerFactory::build() tworzy CronRunner z handlerem shipment_tracking_sync - [ ] Istniejące cron handlery działają bez zmian (regression check) - [ ] All acceptance criteria met - Wszystkie 3 taski ukończone - Wszystkie verification checks przechodzą - Brak błędów składni PHP - Brak nowych zależności composer - Istniejąca funkcjonalność przesyłek nienaruszona After completion, create `.paul/phases/27-shipment-tracking-backend/27-01-SUMMARY.md`