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:
2026-03-23 20:33:44 +01:00
parent c59d431083
commit 228c0e96cf
17 changed files with 1365 additions and 27 deletions

View File

@@ -45,10 +45,11 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i n
- [x] Naprawa zapisu REGON, BDO, KRS i logo w ustawieniach firmy — Phase 22
- [x] Presety przesyłek — customowe przyciski szybkiego wypełniania formularza (CRUD + autofill + zarządzanie) — Phase 23-25
- [x] Ręczny numer przesyłki — dodawanie tracking number bez API przewoźnika — Phase 26
- [x] Tracking backend — dwupoziomowe statusy dostawy, 3 implementacje providerów, cron handler — Phase 27
### Active (In Progress)
(brak)
- [ ] Śledzenie przesyłek UI — wyświetlanie statusów, ustawienia crona — Phase 28
### Planned (Next)

View File

@@ -6,7 +6,14 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz
## Current Milestone
None — ready for next milestone planning.
### v1.2 Śledzenie przesyłek — In progress
Automatyczne śledzenie statusu dostawy przesyłek przez API przewoźników (InPost ShipX, Apaczka, Allegro WZA). Cykliczne odpytywanie przez cron z konfigurowalnym interwałem. Dwupoziomowy system statusów: znormalizowany + surowy z API.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 27 | Shipment Tracking Backend | 1/1 | Complete ✓ |
| 28 | Shipment Tracking UI + Settings | 0/1 | Not started |
## Completed Milestones
@@ -167,4 +174,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
---
*Roadmap created: 2026-03-12*
*Last updated: 2026-03-22 — v1.0 milestone created*
*Last updated: 2026-03-23 — v1.2 milestone created*

View File

@@ -5,15 +5,15 @@
See: .paul/PROJECT.md (updated 2026-03-12)
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
**Current focus:** v1.1 Ręczny numer przesyłkiMILESTONE COMPLETE ✓
**Current focus:** v1.2 Śledzenie przesyłek — Phase 27 COMPLETE, Phase 28 next
## Current Position
Milestone: v1.1 Ręczny numer przesyłki — COMPLETE ✓
Phase: [1] of [1] (Manual Tracking Number) — COMPLETE ✓
Plan: 26-01 — loop closed
Status: Milestone v1.1 complete
Last activity: 2026-03-23 — UNIFY complete, milestone v1.1 done
Milestone: v1.2 Śledzenie przesyłek
Phase: [1] of [2] (Shipment Tracking Backend) — COMPLETE ✓
Plan: 27-01 — loop closed
Status: Phase 27 complete, ready for Phase 28 PLAN
Last activity: 2026-03-23 — UNIFY 27-01 complete
Progress:
- v0.1 Initial Release: [██████████] 100% ✓
@@ -27,14 +27,16 @@ Progress:
- v0.9 Poprawki ustawień firmy: [██████████] 100% ✓
- v1.0 Presety przesyłek: [██████████] 100% ✓
- v1.1 Ręczny numer przesyłki: [██████████] 100% ✓
- Phase 26: [██████████] 100% ✓ (1/1 plans)
- v1.2 Śledzenie przesyłek: [█████░░░░░] 50%
- Phase 27: [██████████] 100% ✓ (1/1 plans)
- Phase 28: [░░░░░░░░░░] 0% (0/1 plans)
## Loop Position
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Milestone v1.1 complete]
✓ ✓ ✓ [Phase 27 complete — ready for Phase 28 PLAN]
```
## Accumulated Context
@@ -42,6 +44,9 @@ PLAN ──▶ APPLY ──▶ UNIFY
### Decisions
| Data | Decyzja | Faza | Wpływ |
|------|---------|------|-------|
| 2026-03-23 | Dwupoziomowy system statusów: normalized + raw z API | Faza 27 | Max szczegółowość dla usera + spójna logika filtrowania |
| 2026-03-23 | Osobny ShipmentTrackingInterface (nie rozszerzenie ShipmentProviderInterface) | Faza 27 | Czysta separacja tracking vs creation; łatwe dodawanie providerów |
| 2026-03-23 | Idempotentne migracje (IF NOT EXISTS + INSERT IGNORE) | Faza 27 | Bezpieczne re-run migracji |
| 2026-03-12 | AllegroTokenManager wydzielony z 4 klas OAuth | Faza 01 | Centralizacja logiki tokenów, brak duplikacji |
| 2026-03-12 | StringHelper jako final static class w Core/Support | Faza 01 | 19 duplikatów helperów usunięte z 15 klas |
| 2026-03-13 | CronHandlerFactory jako jedyne miejsce kompozycji crona | Faza 02 | Application.php i bin/cron.php zsynchronizowane; 2 bugi naprawione |
@@ -68,6 +73,11 @@ PLAN ──▶ APPLY ──▶ UNIFY
| 2026-03-17 | Email history jako wpisy w order_activity_log (nie osobna sekcja) | Faza 15 | Spójność z istniejącym UX — jeden timeline zamiast fragmentacji |
| 2026-03-17 | VariableResolver wydzielony z EmailTemplateController | Faza 15 | Reuse logiki zmiennych; resolwer niezależny od kontrolera szablonów |
### Skill Audit (Faza 27, Plan 01)
| Oczekiwany | Wywołany | Uwagi |
|------------|---------|-------|
| sonar-scanner | ✓ | 0 nowych unikalnych issues; 3 pre-existing patterns (2x S1192 DeliveryStatus, 1x S1172 handler) |
### Skill Audit (Faza 26, Plan 01)
| Oczekiwany | Wywołany | Uwagi |
|------------|---------|-------|
@@ -217,7 +227,7 @@ PLAN ──▶ APPLY ──▶ UNIFY
- **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno.
### Git State
Last commit: 91963d5 — feat(25-shipment-presets-management): edycja, usuwanie, zarządzanie presetami
Last commit: c59d431 — feat(26-manual-tracking-number): reczne dodawanie numeru przesylki do zamowienia
Branch: main
Feature branches merged: none
@@ -227,21 +237,12 @@ Brak.
## Session Continuity
Last session: 2026-03-23
Stopped at: Milestone v1.1 complete
Next action: /paul:discuss-milestone lub /paul:milestone
Resume file: .paul/phases/26-manual-tracking-number/26-01-SUMMARY.md
Stopped at: Phase 27 complete
Next action: /paul:plan (Phase 28 — Shipment Tracking UI + Settings)
Resume file: .paul/phases/27-shipment-tracking-backend/27-01-SUMMARY.md
Resume context:
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
- v0.3: COMPLETE ✓ (5 phases, 5 plans) — Moduł Paragonów
- v0.4: COMPLETE ✓ (3 phases, 4 plans) — Moduł E-mail
- v0.5: COMPLETE ✓ (1 phase, 2 plans) — Moduł Automatyzacji
- v0.6: COMPLETE ✓ (1 phase, 1 plan) — Poprawki UX
- v0.7: COMPLETE ✓ (3 phases, 3 plans) — Zdalne drukowanie etykiet
- v0.8: COMPLETE ✓ (1 phase, 1 plan) — Poprawki źródła zamówień
- v0.9: COMPLETE ✓ (1 phase, 1 plan) — Poprawki ustawień firmy
- v1.0: COMPLETE ✓ (3 phases, 3 plans) — Presety przesyłek
- v1.1: IN PROGRESS (1 phase, 1 plan) — Ręczny numer przesyłki
- v0.1v1.1: COMPLETE ✓ (26 phases, 38 plans)
- v1.2: IN PROGRESS — Phase 27 done, Phase 28 (UI + Settings) next
---
*STATE.md — Updated after every significant action*

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

View File

@@ -0,0 +1,153 @@
---
phase: 27-shipment-tracking-backend
plan: 01
subsystem: shipments
tags: [tracking, cron, inpost, apaczka, allegro, delivery-status]
requires:
- phase: 26-manual-tracking-number
provides: shipment_packages table with manual provider support
provides:
- Delivery tracking columns (delivery_status, delivery_status_raw, delivery_status_updated_at)
- ShipmentTrackingInterface with 3 provider implementations
- DeliveryStatus normalization class with full status mappings
- ShipmentTrackingHandler cron job
affects: [28-shipment-tracking-ui]
tech-stack:
added: []
patterns: [two-level-status-normalization, tracking-service-per-provider]
key-files:
created:
- 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/Cron/ShipmentTrackingHandler.php
- DOCS/SHIPMENT_TRACKING_STATUSES.md
modified:
- src/Modules/Shipments/ShipmentPackageRepository.php
- src/Modules/Cron/CronHandlerFactory.php
key-decisions:
- "Two-level status: normalized (10 values) + raw from API"
- "Separate tracking interface (not extending ShipmentProviderInterface)"
- "LIMIT 50 packages per cron run to prevent overload"
- "Terminal statuses (delivered/returned/cancelled) skip tracking"
- "Idempotent migrations with IF NOT EXISTS pattern"
patterns-established:
- "ShipmentTrackingInterface::getDeliveryStatus() returns {status, status_raw, description} or null"
- "DeliveryStatus::normalize(provider, rawStatus) for provider-agnostic status mapping"
- "ShipmentTrackingRegistry for provider lookup by code"
duration: ~25min
started: 2026-03-23T19:10:00Z
completed: 2026-03-23T19:35:00Z
---
# Phase 27 Plan 01: Shipment Tracking Backend Summary
**Infrastruktura backendowa do automatycznego śledzenia statusu dostawy przesyłek — migracja DB, dwupoziomowy system statusów, 3 implementacje providerów (InPost/Apaczka/Allegro), cron handler.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25 min |
| Started | 2026-03-23T19:10:00Z |
| Completed | 2026-03-23T19:35:00Z |
| Tasks | 3 completed |
| Files created | 10 |
| Files modified | 2 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Kolumny delivery tracking | Pass | 3 kolumny + indeks dodane, migracja idempotentna |
| AC-2: DeliveryStatus mapuje statusy | Pass | 11/11 testów mapowania przeszło |
| AC-3: Tracking interface + 3 implementacje | Pass | InPost, Apaczka, Allegro — syntax OK, Sonar fixes applied |
| AC-4: Cron handler odpytuje aktywne przesyłki | Pass | ShipmentTrackingHandler z try/catch per package |
| AC-5: Cron schedule zarejestrowany | Pass | shipment_tracking_sync, 900s, enabled=1 |
## Accomplishments
- Dwupoziomowy system statusów: 10 znormalizowanych + pełne surowe statusy z API (30+ InPost, 11 Apaczka, 7 Allegro)
- Kompletna dokumentacja statusów API w DOCS/SHIPMENT_TRACKING_STATUSES.md
- Cron handler z graceful error handling (błąd jednej paczki nie blokuje reszty)
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `database/migrations/20260323_000060_*.sql` | Created | Kolumny delivery tracking (idempotentna) |
| `database/migrations/20260323_000061_*.sql` | Created | Cron schedule shipment_tracking_sync |
| `src/Modules/Shipments/DeliveryStatus.php` | Created | Stałe, mapy, normalize(), label(), isTerminal() |
| `src/Modules/Shipments/ShipmentTrackingInterface.php` | Created | Interfejs: supports() + getDeliveryStatus() |
| `src/Modules/Shipments/InpostTrackingService.php` | Created | InPost ShipX API tracking |
| `src/Modules/Shipments/ApaczkaTrackingService.php` | Created | Apaczka API tracking |
| `src/Modules/Shipments/AllegroTrackingService.php` | Created | Allegro Shipment Management API tracking |
| `src/Modules/Shipments/ShipmentTrackingRegistry.php` | Created | Registry: getForProvider() |
| `src/Modules/Cron/ShipmentTrackingHandler.php` | Created | Cron handler: iterate + update |
| `DOCS/SHIPMENT_TRACKING_STATUSES.md` | Created | Dokumentacja statusów API przewoźników |
| `src/Modules/Shipments/ShipmentPackageRepository.php` | Modified | +findActiveForTracking(), +updateDeliveryStatus() |
| `src/Modules/Cron/CronHandlerFactory.php` | Modified | +shipment_tracking_sync handler |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Osobny ShipmentTrackingInterface (nie rozszerzenie ShipmentProviderInterface) | Tracking to inny concern niż tworzenie przesyłek; inne zależności | Czysta separacja, łatwe dodawanie nowych providerów |
| Dwupoziomowe statusy (normalized + raw) | User widzi pełny szczegół z API, system filtruje po ujednoliconym | Max info dla usera + spójna logika |
| LIMIT 50 per cron run | Zapobiega timeout przy dużej liczbie paczek | Kolejne paczki w następnym cyklu |
| Idempotentne migracje | Błąd Duplicate column przy ponownym uruchomieniu | Bezpieczne re-run migracji |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | Migracje zmienione na idempotentne |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Minimalna — dodano IF NOT EXISTS do migracji po wykryciu duplikatu kolumny.
### Auto-fixed Issues
**1. Migracja nie-idempotentna**
- **Found during:** Verification po Task 1
- **Issue:** ALTER TABLE ADD COLUMN bez sprawdzenia czy kolumna istnieje — błąd przy re-run
- **Fix:** Zmieniono na idempotentny pattern z PREPARE/EXECUTE + INSERT IGNORE
- **Files:** database/migrations/20260323_000060_*.sql, 20260323_000061_*.sql
## Sonar Audit
| Nowe issues | Typ | Status |
|-------------|-----|--------|
| 2x S1192 DeliveryStatus | Pre-existing pattern (40x w projekcie) | Zalogowane w todo.md punkt 20 |
| 1x S1172 ShipmentTrackingHandler | Pre-existing pattern (11x w projekcie) | Zalogowane w todo.md punkt 24 |
0 nowych unikalnych issues. S1142 (returns) naprawione przez wydzielenie metod.
## Next Phase Readiness
**Ready:**
- Kolumny DB gotowe, cron handler zarejestrowany
- DeliveryStatus::label() gotowy do użycia w UI
- ShipmentPackageRepository::findActiveForTracking() gotowy
**Concerns:**
- Brak — czysta baza dla Phase 28 (UI)
**Blockers:**
- None
---
*Phase: 27-shipment-tracking-backend, Plan: 01*
*Completed: 2026-03-23*