--- phase: 66-allegro-delivery-tracking plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/Modules/Shipments/AllegroTrackingService.php - src/Modules/Shipments/DeliveryStatus.php - src/Modules/Cron/ShipmentTrackingHandler.php autonomous: true delegation: auto --- ## Goal Zaimplementować śledzenie statusu przesyłek Allegro Delivery (numery A-*) przez publiczne edge API Allegro. Aktualnie `AllegroTrackingService` zwraca `null` dla przesyłek nie-InPost. Po zmianie — pobiera statusy z `https://edge.allegro.pl/ad/tracking?packageNo={nr}` i normalizuje je do istniejącego systemu `DeliveryStatus`. ## Purpose Przesyłki Allegro Delivery (One Kurier, DPD via Allegro, itp.) nie mają śledzenia w orderPRO. Użytkownicy muszą ręcznie sprawdzać status na allegro.pl. Ta zmiana automatyzuje ten proces. ## Output - `AllegroTrackingService` pobiera statusy z edge API dla przesyłek non-InPost - `DeliveryStatus` ma rozszerzony mapping Allegro o statusy z edge API (opisy PL) - Rate limiting: max 1 request na minutę per przesyłka (w cron handler) ## Project Context @.paul/PROJECT.md @.paul/STATE.md ## Source Files @src/Modules/Shipments/AllegroTrackingService.php @src/Modules/Shipments/DeliveryStatus.php @src/Modules/Cron/ShipmentTrackingHandler.php ## API Research Edge API endpoint (publiczny, bez autoryzacji): - URL: `https://edge.allegro.pl/ad/tracking?packageNo={trackingNumber}` - Header: `Accept: application/vnd.allegro.internal.v1+json` - Response: `{"status": [{"eventTimestamp": "ISO8601", "description": "Opis PL"}]}` - Ostatni element tablicy = aktualny status - Opisy są po polsku, np.: - "Przesyłka została przygotowana przez nadawcę" - "Przesyłka została nadana" - "Przesyłka została podjęta z punktu przez kuriera" - "Przesyłka została odebrana przez kuriera" - "Kurier przekazał przesyłkę do magazynu" ## AC-1: Allegro Delivery tracking zwraca status ```gherkin Given przesyłka z provider=allegro_wza i tracking_number zaczynający się od "A" When cron tracking handler odpytuje AllegroTrackingService Then serwis pobiera status z edge.allegro.pl/ad/tracking And zwraca znormalizowany status + opis po polsku ``` ## AC-2: Mapowanie opisów na znormalizowane statusy ```gherkin Given odpowiedź z edge API zawiera description np. "Przesyłka została nadana" When AllegroTrackingService przetwarza odpowiedź Then mapuje opis na raw status (np. "shipped") i normalizuje przez DeliveryStatus And status_raw zawiera oryginalny opis z API ``` ## AC-3: Rate limiting — max 1 request/min ```gherkin Given cron handler przetwarza wiele przesyłek allegro_wza When odpytuje edge API Then między kolejnymi requestami do edge.allegro.pl czeka minimum 60 sekund And inne providery (InPost, Apaczka) nie są objęte tym limitem ``` ## AC-4: Fallback InPost nadal działa ```gherkin Given przesyłka z provider=allegro_wza i carrier_id zawierający "inpost" When cron tracking handler odpytuje AllegroTrackingService Then serwis nadal używa InPost API (nie edge API) And zachowanie jest identyczne jak przed zmianą ``` Task 1: Rozszerzenie DeliveryStatus o mapowanie Allegro edge API src/Modules/Shipments/DeliveryStatus.php Dodaj nową mapę `ALLEGRO_EDGE_MAP` i `ALLEGRO_EDGE_DESCRIPTIONS` do DeliveryStatus. Edge API zwraca opisy po polsku (nie kody). Trzeba mapować opisy na wewnętrzne klucze, a potem na znormalizowane statusy. Nowa mapa `ALLEGRO_EDGE_MAP` (klucz = slug z opisu, wartość = normalized status): ```php private const ALLEGRO_EDGE_MAP = [ 'przygotowana_przez_nadawce' => self::CREATED, 'nadana' => self::CONFIRMED, 'podjeta_z_punktu' => self::IN_TRANSIT, 'odebrana_przez_kuriera' => self::IN_TRANSIT, 'przekazana_do_magazynu' => self::IN_TRANSIT, 'w_sortowni' => self::IN_TRANSIT, 'w_doreceniu' => self::OUT_FOR_DELIVERY, 'gotowa_do_odbioru' => self::READY_FOR_PICKUP, 'dostarczona' => self::DELIVERED, 'doreczona' => self::DELIVERED, 'zwrocona' => self::RETURNED, 'anulowana' => self::CANCELLED, 'problem' => self::PROBLEM, ]; ``` Nowa mapa `ALLEGRO_EDGE_DESCRIPTIONS` — klucz = slug, wartość = oryginalny opis PL (identyczny jak z API). Dodaj nowy provider `allegro_edge` do: - `PROVIDER_MAPS` array - `PROVIDER_DESCRIPTIONS` array - match w `normalize()` i `description()` Dodaj statyczną metodę `slugifyAllegroDescription(string $description): string` — konwertuje opis PL na slug: 1. Usuwa prefiks "Przesyłka została " / "Kurier " 2. Bierze główne słowo kluczowe 3. Zamienia polskie znaki na ASCII 4. Zamienia spacje na podkreślenia 5. Zwraca lowercase slug WAŻNE: Metoda musi obsługiwać nieznane opisy — jeśli slug nie istnieje w mapie, zwróć sam slug jako raw_status i self::UNKNOWN jako normalized. Klasa DeliveryStatus kompiluje się bez błędów. Nowe stałe ALLEGRO_EDGE_MAP i ALLEGRO_EDGE_DESCRIPTIONS istnieją. Provider 'allegro_edge' jest obsługiwany w normalize() i description(). AC-2 satisfied: mapowanie opisów na znormalizowane statusy zdefiniowane. Task 2: Implementacja fetchAllegroEdgeStatus w AllegroTrackingService src/Modules/Shipments/AllegroTrackingService.php Zmodyfikuj `AllegroTrackingService::getDeliveryStatus()` aby obsługiwał przesyłki Allegro Delivery (non-InPost): 1. W metodzie `getDeliveryStatus()`, po bloku `if (str_contains(... 'inpost'))`, zamiast `return null` dodaj: ```php return $this->fetchAllegroEdgeStatus($trackingNumber); ``` 2. Dodaj nową prywatną metodę `fetchAllegroEdgeStatus(string $trackingNumber): ?array`: ```php private function fetchAllegroEdgeStatus(string $trackingNumber): ?array { try { $url = 'https://edge.allegro.pl/ad/tracking?packageNo=' . rawurlencode($trackingNumber); $response = $this->edgeApiRequest($url); $statuses = $response['status'] ?? []; if (!is_array($statuses) || $statuses === []) { return null; } // Ostatni element = najnowszy status $latest = end($statuses); $description = trim((string) ($latest['description'] ?? '')); if ($description === '') { return null; } $slug = DeliveryStatus::slugifyAllegroDescription($description); return [ 'status' => DeliveryStatus::normalize('allegro_edge', $slug), 'status_raw' => $description, 'description' => $description, ]; } catch (Throwable) { return null; } } ``` 3. Dodaj nową prywatną metodę `edgeApiRequest(string $url): array` — osobna od istniejącej `apiRequest()` bo nie wymaga autoryzacji: ```php private function edgeApiRequest(string $url): array { $ch = curl_init($url); if ($ch === false) { return []; } $opts = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => [ 'Accept: application/vnd.allegro.internal.v1+json', 'Content-Type: application/vnd.allegro.internal.v1+json', ], ]; $caPath = $this->getCaBundlePath(); if ($caPath !== null) { $opts[CURLOPT_CAINFO] = $caPath; } curl_setopt_array($ch, $opts); $body = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $ch = null; if ($body === false || $httpCode < 200 || $httpCode >= 300) { return []; } $json = json_decode((string) $body, true); return is_array($json) ? $json : []; } ``` NIE zmieniaj istniejącej metody `fetchInpostStatus()` ani `apiRequest()` — to osobne ścieżki. AllegroTrackingService kompiluje się. Metoda getDeliveryStatus nie zwraca null dla non-InPost przesyłek (wywołuje fetchAllegroEdgeStatus). Metoda edgeApiRequest wysyła Accept: application/vnd.allegro.internal.v1+json. AC-1 satisfied: Allegro Delivery tracking pobiera status z edge API. AC-4 satisfied: InPost path bez zmian. Task 3: Rate limiting w ShipmentTrackingHandler dla Allegro edge API src/Modules/Cron/ShipmentTrackingHandler.php Dodaj rate limiting do `ShipmentTrackingHandler::handle()` — max 1 request na 60 sekund do edge.allegro.pl. 1. Dodaj stałą: ```php private const ALLEGRO_EDGE_RATE_LIMIT_SECONDS = 60; ``` 2. W metodzie `handle()`, przed pętlą `foreach`, dodaj zmienną śledzącą czas ostatniego requestu Allegro edge: ```php $lastAllegroEdgeRequestTime = 0.0; ``` 3. Wewnątrz pętli `foreach`, PO uzyskaniu `$service` a PRZED wywołaniem `$service->getDeliveryStatus()`, dodaj sprawdzenie: ```php if ($provider === 'allegro_wza') { $carrierId = strtolower(trim((string) ($package['carrier_id'] ?? ''))); $isInpost = str_contains($carrierId, 'inpost') || str_contains($carrierId, 'paczkomat'); if (!$isInpost) { $elapsed = microtime(true) - $lastAllegroEdgeRequestTime; if ($elapsed < self::ALLEGRO_EDGE_RATE_LIMIT_SECONDS) { $sleepTime = (int) ceil(self::ALLEGRO_EDGE_RATE_LIMIT_SECONDS - $elapsed); sleep($sleepTime); } // Zaraz po sleep (lub bez), przed wywołaniem getDeliveryStatus: $lastAllegroEdgeRequestTime = microtime(true); } } ``` WAŻNE: - Rate limit dotyczy TYLKO requestów do edge.allegro.pl (non-InPost allegro_wza) - InPost i Apaczka requestów NIE ograniczaj - sleep() w cronie jest OK — to background process ShipmentTrackingHandler kompiluje się. Stała ALLEGRO_EDGE_RATE_LIMIT_SECONDS = 60 istnieje. Kod rate limitingu jest w pętli foreach przed getDeliveryStatus dla allegro_wza non-inpost. AC-3 satisfied: max 1 request/min do edge.allegro.pl. ## DO NOT CHANGE - src/Modules/Shipments/InpostTrackingService.php - src/Modules/Shipments/ApaczkaTrackingService.php - src/Modules/Shipments/ShipmentTrackingInterface.php - src/Modules/Shipments/ShipmentTrackingRegistry.php - src/Modules/Shipments/ShipmentPackageRepository.php - src/Modules/Cron/CronHandlerFactory.php - Istniejące mapy INPOST_MAP, APACZKA_MAP w DeliveryStatus - Istniejąca metoda fetchInpostStatus() w AllegroTrackingService ## SCOPE LIMITS - Nie tworzymy nowych klas — rozszerzamy istniejące - Nie zmieniamy schematu DB — delivery_status_raw już obsługuje dowolne stringi - Nie dodajemy UI — tracking UI już istnieje i działa z dowolnym statusem - Nie tworzymy unit testów w tym planie Before declaring plan complete: - [ ] DeliveryStatus::normalize('allegro_edge', 'nadana') zwraca 'confirmed' - [ ] DeliveryStatus::slugifyAllegroDescription('Przesyłka została nadana') zwraca slug mapowany na status - [ ] AllegroTrackingService::getDeliveryStatus() dla non-InPost allegro_wza wywołuje edge API - [ ] AllegroTrackingService::getDeliveryStatus() dla InPost allegro_wza nadal używa InPost API - [ ] ShipmentTrackingHandler ma rate limit 60s między requestami edge.allegro.pl - [ ] Żadne chronione pliki nie zostały zmodyfikowane - [ ] PHP syntax check: `php -l` na każdym zmienionym pliku - Wszystkie 3 taski auto completed - Przesyłki Allegro Delivery (A-numery) mają automatyczny tracking statusu - Rate limit chroni przed blokadą przez Allegro - Istniejący tracking InPost i Apaczka bez zmian After completion, create `.paul/phases/66-allegro-delivery-tracking/66-01-SUMMARY.md`