feat(27-shipment-tracking-backend): infrastruktura sledzenia przesylek — statusy, tracking services, cron handler
Dwupoziomowy system statusow dostawy (normalized + raw z API), implementacje trackingu dla InPost ShipX, Apaczka i Allegro WZA, cron handler odpytujacy aktywne przesylki co 15 minut. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
310
.paul/phases/27-shipment-tracking-backend/27-01-PLAN.md
Normal file
310
.paul/phases/27-shipment-tracking-backend/27-01-PLAN.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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`
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## 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
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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'
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracja DB — kolumny delivery tracking + cron schedule</name>
|
||||
<files>database/migrations/20260323_000042_add_delivery_tracking_columns.sql, database/migrations/20260323_000043_add_shipment_tracking_cron_schedule.sql</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>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</verify>
|
||||
<done>AC-1 i AC-5 (część DB) satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: DeliveryStatus + ShipmentTrackingInterface + 3 implementacje + Registry</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<action>
|
||||
**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
|
||||
</action>
|
||||
<verify>
|
||||
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
|
||||
</verify>
|
||||
<done>AC-2 i AC-3 satisfied: statusy mapowane poprawnie, 3 implementacje trackingu gotowe</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: ShipmentTrackingHandler cron + rejestracja w CronHandlerFactory + rozszerzenie ShipmentPackageRepository</name>
|
||||
<files>
|
||||
src/Modules/Cron/ShipmentTrackingHandler.php,
|
||||
src/Modules/Cron/CronHandlerFactory.php,
|
||||
src/Modules/Shipments/ShipmentPackageRepository.php
|
||||
</files>
|
||||
<action>
|
||||
**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
|
||||
</action>
|
||||
<verify>
|
||||
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
|
||||
</verify>
|
||||
<done>AC-4 i AC-5 satisfied: cron handler przetwarza aktywne przesyłki, schedule zarejestrowany</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/27-shipment-tracking-backend/27-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user