Files
orderPRO/.paul/phases/66-allegro-delivery-tracking/66-01-PLAN.md
2026-04-03 22:35:49 +02:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
phase plan type wave depends_on files_modified autonomous delegation
66-allegro-delivery-tracking 01 execute 1
src/Modules/Shipments/AllegroTrackingService.php
src/Modules/Shipments/DeliveryStatus.php
src/Modules/Cron/ShipmentTrackingHandler.php
true 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"

<acceptance_criteria>

AC-1: Allegro Delivery tracking zwraca status

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

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

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

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ą

</acceptance_criteria>

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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.paul/phases/66-allegro-delivery-tracking/66-01-SUMMARY.md`