diff --git a/.paul/STATE.md b/.paul/STATE.md index a1eedc2..0da641d 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -4,30 +4,33 @@ **Ostatnia aktualizacja:** 2026-05-19 ## Aktywna praca -UNIFY zakonczony dla `.paul/plans/20260519-1600-refactor-allegro-integration-controller/`. Petla zamknieta, brak aktywnego planu. +UNIFY zakonczony dla `.paul/plans/20260519-1730-refactor-delivery-status/`. Petla zamknieta, brak aktywnego planu. ``` PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [Loop complete — gotowe na nastepny PLAN] ``` -Rezultat: `AllegroIntegrationController` 653 -> 223 lin. (66% redukcji). Wydzielono `AllegroIntegrationViewModel` (61), `AllegroOAuthFlowService` (94), `AllegroImportScheduleService` (179), `AllegroImportImageWarningFormatter` (69), `AllegroSaveSettingsValidator` (48). Deviation: kontroler 223 vs cel < 200 (akceptowalne, 66% redukcji). +Rezultat: `DeliveryStatus.php` 657 -> 170 lin. (fasada zachowujaca pelny kontrakt). Wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow; `tests/Unit/DeliveryStatusTest.php` 4/4 bez modyfikacji (primary gate). Pelny suite 3/15 identyczny z baseline (pre-existing). SUMMARY: `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`. + +## Poprzednia praca +UNIFY zakonczony dla `.paul/plans/20260519-1600-refactor-allegro-integration-controller/`. Rezultat: `AllegroIntegrationController` 653 -> 223 lin. (66% redukcji). Wydzielono `AllegroIntegrationViewModel` (61), `AllegroOAuthFlowService` (94), `AllegroImportScheduleService` (179), `AllegroImportImageWarningFormatter` (69), `AllegroSaveSettingsValidator` (48). Deviation: kontroler 223 vs cel < 200 (akceptowalne, 66% redukcji). Zamkniete SUMMARY (chronologicznie 2026-05-19): - `.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md` (routes/web.php: 859 -> 78). - `.paul/plans/20260519-1430-refactor-orders-statistics-controller/SUMMARY.md` (Statistics: 640 -> 110). - `.paul/plans/20260519-1600-refactor-allegro-integration-controller/SUMMARY.md` (Allegro: 653 -> 223). +- `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md` (DeliveryStatus: 657 -> 170, fasada + 3 klasy). ## Kontekst sesji - Galaz: `main`. -- Niezakomitowane: dekompozycja Statistics + Allegro (8 nowych plikow modulu + 4 zmodyfikowane + dokumenty PAUL + STATE.md + governance log + changelog). -- Ostatnie commity: `3809340 update`, `e77b0f1 refactor(routing): module providers + lazy ServiceRegistry`, `2df4638 update`. +- Niezakomitowane: dekompozycja Statistics + Allegro + DeliveryStatus (11 nowych plikow modulow + 5 zmodyfikowanych + dokumenty PAUL + STATE.md + governance log + changelog). +- Ostatnie commity: `e12ebe3 update`, `3809340 update`, `e77b0f1 refactor(routing): module providers + lazy ServiceRegistry`. ## Sugerowana nastepna akcja -1. Smoke-test reczny zgodnie z sekcja "Smoke-test" w obu SUMMARY (Statistics + Allegro) przed commitem. -2. Commit: refaktor Statistics + Allegro (8 nowych plikow + 4 zmodyfikowane + docs PAUL + plany). -3. Kolejny kandydat z `quality_risks.md`: `OrdersController.php` (1490 lin.), `OrdersRepository.php` (1243 lin.), `ShopproIntegrationsController.php` (1076 lin.), albo bazowy `BaseIntegrationController` dla 9 integracji. -4. `$paul-plan` aby uruchomic nowa petle. +1. Smoke-test reczny przed commitem: widoki `/orders/show` + `/shipments/prepare` (label/description/getColor/trackingUrl/ALL_STATUSES) oraz tracking Apaczka/InPost/Polkurier (DB byla niedostepna w sesji APPLY). +2. Commit zaleglej dekompozycji: Statistics + Allegro + DeliveryStatus. +3. `$paul-plan` na kolejnego kandydata z `quality_risks.md`: `OrdersController.php` (1490 lin.), `OrdersRepository.php` (1243 lin.), `ShopproIntegrationsController.php` (1076 lin.). ## Legacy Pliki `ROADMAP.md` / `MILESTONES.md` sa opcjonalne i obecnie nie sa wymagane. diff --git a/.paul/changelog/2026-05-19.md b/.paul/changelog/2026-05-19.md index 6ab878c..b1e093e 100644 --- a/.paul/changelog/2026-05-19.md +++ b/.paul/changelog/2026-05-19.md @@ -16,6 +16,12 @@ - Task 4: utworzony `AllegroIntegrationViewModel` (61 lin.) — payload widoku `settings/allegro`. - Task 5: slim kontroler (223 lin.) + dodatkowo `AllegroSaveSettingsValidator` (48 lin., poza pierwotnym planem); `AllegroIntegrationModule` rejestruje 5 nowych kluczy w `ServiceRegistry`. - Task 6: dokumentacja PAUL zaktualizowana (`architecture.md`, `quality_risks.md`, `tech_changelog.md`). +- [Plan 20260519-1730-refactor-delivery-status] Dekompozycja `DeliveryStatus` (657 -> 170 lin.) na fasade + 3 wspolpracownikow; zero zmian w 20 plikach konsumentow. +- Task 1: utworzony `DeliveryStatusProviderMap` (~330 lin.) — mapy surowych statusow per dostawca + `normalize`/`description`/`getDefaultMappings`/`*WithOverrides`. +- Task 2: utworzony `AllegroDescriptionGuesser` (~150 lin.) — `slugifyAllegroDescription` + `guessStatusFromDescription` + wzorce. +- Task 3: utworzony `DeliveryTrackingUrlBuilder` (~85 lin.) — `trackingUrl` + URL kurierow/dostawcow. +- Task 4: dokumentacja PAUL zaktualizowana (`architecture.md`, `tech_changelog.md`, `quality_risks.md`). +- Weryfikacja: `DeliveryStatusTest` 4/4 bez zmiany pliku testu; pelny suite identyczny z baseline (3/15 pre-existing). ## Zmienione pliki @@ -39,3 +45,9 @@ - `.paul/STATE.md` - `.paul/plans/20260519-1430-refactor-orders-statistics-controller/PLAN.md` - `.paul/plans/20260519-1430-refactor-orders-statistics-controller/SUMMARY.md` +- `src/Modules/Shipments/DeliveryStatus.php` +- `src/Modules/Shipments/DeliveryStatusProviderMap.php` +- `src/Modules/Shipments/AllegroDescriptionGuesser.php` +- `src/Modules/Shipments/DeliveryTrackingUrlBuilder.php` +- `.paul/plans/20260519-1730-refactor-delivery-status/PLAN.md` +- `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index 90164c0..4e2dc9e 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -67,7 +67,7 @@ Korzysci wzgledem poprzedniego monolitycznego `routes/web.php` (859 lin.): | Statistics | `src/Modules/Statistics/` | raporty zamowien (slim Controller + `OrdersStatisticsFilters` + `OrdersStatisticsTableBuilder` + `OrdersStatisticsSummaryBuilder` + Repository) | | Settings/Allegro | `src/Modules/Settings/Allegro*.php` | integracja Allegro: slim `AllegroIntegrationController` + `AllegroIntegrationViewModel` + `AllegroOAuthFlowService` + `AllegroImportScheduleService` + `AllegroImportImageWarningFormatter` + `AllegroSaveSettingsValidator` + `AllegroOrderImportService` + `AllegroStatusDiscoveryService` + `AllegroStatusMappingController` + `AllegroDeliveryMappingController` + repozytoria (`AllegroIntegrationRepository`, `AllegroStatusMappingRepository`, `AllegroPullStatusMappingRepository`) + klienci (`AllegroApiClient`, `AllegroOAuthClient`, `AllegroTokenManager`). | | Accounting | `src/Modules/Accounting/` | paragony, faktury, eksport ksiegowy | -| Shipments | `src/Modules/Shipments/` | etykiety, tracking, presets paczek | +| Shipments | `src/Modules/Shipments/` | etykiety, tracking, presets paczek; slownik statusow dostawy jako fasada `DeliveryStatus` + `DeliveryStatusProviderMap` (mapy dostawcow) + `AllegroDescriptionGuesser` (heurystyki opisow) + `DeliveryTrackingUrlBuilder` (URL sledzenia) | | Printing | `src/Modules/Printing/` | API drukowania etykiet (klient Windows) | | Email | `src/Modules/Email/` | SMTP, wysylka maili | | Sms | `src/Modules/Sms/` | wysylka SMS, webhook SmsPlanet, konwersacje | diff --git a/.paul/codebase/quality_risks.md b/.paul/codebase/quality_risks.md index 98a23cc..5bcdfa5 100644 --- a/.paul/codebase/quality_risks.md +++ b/.paul/codebase/quality_risks.md @@ -21,7 +21,7 @@ Konwencja (`CLAUDE.md`): funkcja/klasa zwykle do 30-50 linii, max 3 poziomy zagn | `src/Modules/Shipments/PolkurierShipmentService.php` | 776 | | | `src/Modules/Accounting/InvoiceService.php` | 762 | | | `src/Modules/Automation/AutomationController.php` | 677 | | -| `src/Modules/Shipments/DeliveryStatus.php` | 657 | encja statusow uzywana globalnie — zmiany dotykaja wszystkich integracji. | +| ~~`src/Modules/Shipments/DeliveryStatus.php`~~ | ~~657~~ -> 170 | ✅ Zrefaktorowane 2026-05-19 — fasada zachowujaca pelny kontrakt + wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow. Patrz `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`. | | ~~`src/Modules/Settings/AllegroIntegrationController.php`~~ | ~~653~~ -> 223 | ✅ Zrefaktorowane 2026-05-19 — wydzielono `AllegroIntegrationViewModel` (61), `AllegroOAuthFlowService` (94), `AllegroImportScheduleService` (179), `AllegroImportImageWarningFormatter` (69), `AllegroSaveSettingsValidator` (48). Patrz `.paul/plans/20260519-1600-refactor-allegro-integration-controller/SUMMARY.md`. | | ~~`src/Modules/Statistics/OrdersStatisticsController.php`~~ | ~~640~~ -> 110 | ✅ Zrefaktorowane 2026-05-19 — wydzielono `OrdersStatisticsFilters` (258), `OrdersStatisticsTableBuilder` (101), `OrdersStatisticsSummaryBuilder` (195). Patrz `.paul/plans/20260519-1430-refactor-orders-statistics-controller/SUMMARY.md`. | | ~~`routes/web.php`~~ | ~~859~~ -> 78 | ✅ Zrefaktorowane 2026-05-19 — `ServiceRegistry` + 24 klasy `Module.php`. Patrz `.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md`. | diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index 3c54ca4..038bd48 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -2,6 +2,28 @@ Chronologiczny log zmian technicznych (co i dlaczego). Najnowsze na gorze. +## 2026-05-19 — Dekompozycja DeliveryStatus (fasada + 3 wspolpracownikow) + +### Co +- Rozbicie `src/Modules/Shipments/DeliveryStatus.php` z 657 do 170 lin. +- `DeliveryStatus` pozostaje **fasada** zachowujaca pelny kontrakt publiczny (stale statusow `UNKNOWN..PROBLEM`, `TERMINAL_STATUSES`, `ALL_STATUSES`, `LABEL_PL`, most do DB `setRepository`/`getAllStatuses`/`getAllOptions`/`getColor`/`label`, `isTerminal` oraz metody-delegaty). +- Wydzielono trzy klasy w namespace `App\Modules\Shipments`: + - `DeliveryStatusProviderMap` (~330 lin.) — mapy surowych statusow per dostawca (`INPOST_*`, `APACZKA_*`, `ALLEGRO_*`, `ALLEGRO_EDGE_*`, `POLKURIER_*`) + `normalize`, `description`, `getDefaultMappings`, `normalizeWithOverrides`, `descriptionWithOverrides`. Prywatne stale opisow `DESC_*` przeniesione tutaj. + - `AllegroDescriptionGuesser` (~150 lin.) — `slugifyAllegroDescription`, `guessStatusFromDescription`, `DESCRIPTION_STATUS_PATTERNS` + helper `containsAny`. + - `DeliveryTrackingUrlBuilder` (~85 lin.) — `trackingUrl` + `PROVIDER_TRACKING_URLS`, `CARRIER_TRACKING_URLS`, stale `TRACKING_INPOST_URL`/`TRACKING_ALLEGRO_URL` + helpery matchowania kuriera. + +### Dlaczego +- `quality_risks.md` wskazywal `DeliveryStatus` (657 lin.) jako kandydata do podzialu; ~400 z 657 linii to tablice `const` map statusow — naruszenie limitu z `CLAUDE.md`. +- Encja jest uzywana globalnie (56 wywolan w 20 plikach: serwisy trackingu wszystkich kurierow, cron, kontrolery, widoki, test) — `impact_map.md` zaznacza "dotykac ostroznie". +- Wybrano wariant **fasady** (a nie pelnej ekstrakcji): stale `DeliveryStatus::DELIVERED` itd. uzywane bezposrednio w serwisach, widokach i tescie nie da sie oddelegowac — musialyby zostac fizycznie przeniesione, co wymusiloby edycje kazdego call-site. Fasada = zerowe ryzyko dla integracji. + +### Wplyw +- **Zero zmian w 20 plikach konsumentow** — kontrakt publiczny (sygnatury, stale, wartosci) niezmieniony. +- `tests/Unit/DeliveryStatusTest.php` przechodzi BEZ modyfikacji (primary gate; 4 testy / 7 asercji) — dowod zachowanego kontraktu. +- Pelny suite: 3 errors / 15 failures identyczne jak na HEAD przed zmiana (pre-existing, encoding/mocki — niezwiazane z refaktorem). +- Laczna liczba linii rosnie nieznacznie (boilerplate 3 klas) — oczekiwane. +- Plan: `.paul/plans/20260519-1730-refactor-delivery-status/PLAN.md`. + ## 2026-05-19 — Dekompozycja AllegroIntegrationController ### Co diff --git a/.paul/governance/governance_2026-05-19.jsonl b/.paul/governance/governance_2026-05-19.jsonl index 0e51f45..4186f66 100644 --- a/.paul/governance/governance_2026-05-19.jsonl +++ b/.paul/governance/governance_2026-05-19.jsonl @@ -276,3 +276,23 @@ {"ts":"2026-05-19T21:14:09Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-19.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} {"ts":"2026-05-19T21:14:17Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-19.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} {"ts":"2026-05-19T21:22:01Z","tool":"Write","file":"c:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:34:36Z","tool":"Write","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\plans\\\\20260519-1730-refactor-delivery-status\\\\PLAN.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:34:45Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:34:51Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:35:42Z","tool":"Bash","cmd":"php -v 2>&1 | head -1; echo \"---\"; ls vendor/bin/phpunit 2>&1; echo \"---baseline test---\"; php vendor/bin/phpunit tests/Unit/DeliveryStatusTest.php 2>&1 | tail -15\",\"description\":\"Check php ve","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:36:31Z","tool":"Write","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryStatusProviderMap.php","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:36:57Z","tool":"Bash","cmd":"php -l src/Modules/Shipments/DeliveryStatusProviderMap.php 2>&1\",\"description\":\"Syntax-check DeliveryStatusProviderMap\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in src/Modules/S","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:37:06Z","tool":"Write","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\AllegroDescriptionGuesser.php","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:37:14Z","tool":"Bash","cmd":"php -l src/Modules/Shipments/AllegroDescriptionGuesser.php 2>&1\",\"description\":\"Syntax-check AllegroDescriptionGuesser\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in src/Modules/S","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:37:20Z","tool":"Write","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryTrackingUrlBuilder.php","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:37:38Z","tool":"Write","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryStatus.php","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:38:30Z","tool":"Bash","cmd":"git stash push src/Modules/Shipments/DeliveryStatus.php 2>&1\\necho \"=== BASELINE (my facade reverted to HEAD) ===\"; php vendor/bin/phpunit 2>&1 | tail -3\\ngit stash pop 2>&1 | tail -2\",\"descript","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:38:55Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\tech_changelog.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:39:01Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:39:13Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:43:54Z","tool":"Write","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\plans\\\\20260519-1730-refactor-delivery-status\\\\SUMMARY.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:44:00Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\quality_risks.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:44:14Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-19.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:44:19Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-19.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:44:27Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} +{"ts":"2026-05-19T21:44:40Z","tool":"Edit","file":"C:\\\\visual-studio-code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual-studio-code/projekty/orderPRO"} diff --git a/.paul/plans/20260519-1730-refactor-delivery-status/PLAN.md b/.paul/plans/20260519-1730-refactor-delivery-status/PLAN.md new file mode 100644 index 0000000..378c27b --- /dev/null +++ b/.paul/plans/20260519-1730-refactor-delivery-status/PLAN.md @@ -0,0 +1,236 @@ +--- +plan_id: 20260519-1730-refactor-delivery-status +title: Refaktoryzacja DeliveryStatus — fasada + 3 wspolpracownikow +storage: plan-first +legacy_phase: null +created: 2026-05-19T17:30:00 +status: planned +type: execute +autonomous: true +delegation: auto +files_modified: + - src/Modules/Shipments/DeliveryStatus.php + - src/Modules/Shipments/DeliveryStatusProviderMap.php + - src/Modules/Shipments/AllegroDescriptionGuesser.php + - src/Modules/Shipments/DeliveryTrackingUrlBuilder.php + - .paul/codebase/architecture.md + - .paul/codebase/tech_changelog.md +quality_radar: ok +--- + + +## Goal +Rozbic `src/Modules/Shipments/DeliveryStatus.php` (657 lin.) na czytelna **fasade** + trzech wyspecjalizowanych wspolpracownikow, bez zmiany ani jednego miejsca wywolania w aplikacji. + +## Purpose +`DeliveryStatus` to encja statusow uzywana globalnie — 56 wywolan w 20 plikach (serwisy trackingu wszystkich kurierow, cron, kontrolery, widoki, testy). Plik laczy cztery niezalezne odpowiedzialnosci, a ~400 z 657 linii to tablice `const` mapujace surowe statusy dostawcow. Konwencja `CLAUDE.md` (jedna odpowiedzialnosc na klase, czytelnosc "dla obcego") jest naruszona. Cel: rozdzielic odpowiedzialnosci, zachowujac 100% kontraktu publicznego, by ryzyko dla integracji bylo zerowe. + +## Output +- Fasada `DeliveryStatus` (~150-180 lin.): stale statusow, etykiety, most do DB (`$repository`), publiczne metody-delegaty (jednolinijkowce). +- `DeliveryStatusProviderMap` (~350 lin.): mapy `*_MAP` / `*_DESCRIPTIONS` per dostawca + `normalize` / `description` / `getDefaultMappings` / `*WithOverrides`. +- `AllegroDescriptionGuesser` (~110-130 lin.): `slugifyAllegroDescription`, `guessStatusFromDescription`, `DESCRIPTION_STATUS_PATTERNS`. +- `DeliveryTrackingUrlBuilder` (~80-100 lin.): `trackingUrl` + tablice URL kurierow/dostawcow + matchowanie. + + + +## Project Docs +@.paul/PROJECT.md +@.paul/STATE.md +@.paul/codebase/architecture.md +@.paul/codebase/db_schema.md +@.paul/codebase/impact_map.md +@.paul/codebase/quality_risks.md + +## Source Files +@src/Modules/Shipments/DeliveryStatus.php +@src/Modules/Shipments/DeliveryStatusRepository.php +@tests/Unit/DeliveryStatusTest.php + + + +- Wariant "pelnej ekstrakcji" (przeniesienie metod do nowych klas + edycja 20 plikow wywolan) zostal odrzucony: stale `DeliveryStatus::UNKNOWN`, `::DELIVERED`, `::READY_FOR_PICKUP`, `::OUT_FOR_DELIVERY`, `::ALL_STATUSES` sa uzywane bezposrednio w serwisach, widokach **i tescie** — stalych nie da sie "oddelegowac", musialyby zostac fizycznie przeniesione, co wymusza edycje kazdego odwolania (w tym testu). To inny, wiekszy refaktor niz wymaga `quality_risks.md`. Wybrano fasade zachowujaca pelny kontrakt. + + + +## Quality Radar + +**Status:** ok +**Tryb:** plan (targeted) +**Tools:** codebase-memory-mcp (graf wczytany w STATE: 4217 nodes / 11649 edges); jscpd/ast-grep wylaczone polityka (`.paul/config.md`). Zakres potwierdzony przez `rg` (grep) na `DeliveryStatus::` — 56 trafien / 20 plikow. + +## Affected Areas + +- **Shipments (rdzen zmiany)** — `DeliveryStatus.php` + 3 nowe klasy w `src/Modules/Shipments/`. +- **Konsumenci kontraktu (NIE modyfikowani — weryfikacja braku zmian):** + - Serwisy trackingu: `AllegroTrackingService` (normalize, description, slugifyAllegroDescription, guessStatusFromDescription, UNKNOWN), `InpostTrackingService` (normalize, description), `ApaczkaTrackingService` (normalize, description), `PolkurierTrackingService` (normalizeWithOverrides). + - Cron: `ShipmentTrackingHandler` (normalizeWithOverrides). + - Kontrolery: `Automation*` (getAllStatuses, getAllOptions, UNKNOWN), `DeliveryStatusesController` + `DeliveryStatusMappingController` (getDefaultMappings), `ShipmentController` (UNKNOWN). + - Repozytoria: `DeliveryStatusMappingRepository` (getDefaultMappings). + - Bootstrap: `ShipmentsModule` + `bin/smoke_routes.php` (setRepository). + - Widoki: `resources/views/orders/show.php`, `resources/views/shipments/prepare.php` (label, description, getColor, trackingUrl, ALL_STATUSES). + - SMS: `SmsVariableResolver` (trackingUrl). + - Test: `tests/Unit/DeliveryStatusTest.php` (slugify, normalize, guess + stale). +- **Migracje:** `20260422_000101_backfill_delivery_status_unknowns.sql` — odwoluje sie do `DeliveryStatus::normalize()` wylacznie w komentarzu SQL (zweryfikowane). Brak wykonywalnego PHP. Bez zmian. + +## Duplicate / Hardcoded Risks + +- **Stale `self::CREATED` itd. wewnatrz przenoszonych map** — po przeniesieniu `*_MAP` do `DeliveryStatusProviderMap` musza wskazywac `DeliveryStatus::CREATED` (ten sam namespace). Obsluzone w Task 1 ``. +- **Prywatne stale opisow `DESC_*`** (`DESC_DELIVERED`, `DESC_PICKED_UP_BY_COURIER`, ...) uzywane wylacznie przez tablice opisow → przeniesc do `DeliveryStatusProviderMap` (Task 1). +- **Stale `TRACKING_INPOST_URL` / `TRACKING_ALLEGRO_URL`** uzywane w `PROVIDER_TRACKING_URLS` i `CARRIER_TRACKING_URLS` → przeniesc do `DeliveryTrackingUrlBuilder` (Task 3). +- **Helpery prywatne `containsAny` / `carrierMatches`** (po ~5 lin.) — wedruja ze swoimi wywolujacymi (`containsAny` → guesser, `carrierMatches` → URL builder). NIE tworzyc wspolnej klasy util dla dwoch helperow. +- **Brak drugiego zrodla prawdy:** mapy statusow pozostaja w JEDNYM miejscu (przeniesione, nie skopiowane). Fasada deleguje, nie duplikuje. + +## Explicit Deferrals + +- **Mapowania statusow per integracja jako wzorzec do abstrakcji** (`quality_risks.md` — `*StatusMappingRepository` + `*PullStatusMappingRepository`) — poza zakresem; dotyczy repozytoriow DB, nie tej encji. +- **Przejscie na `enum` PHP 8.1+ dla statusow** — kuszace, ale zlamaloby kontrakt `string` uzywany w sygnaturach 20 plikow i w DB. Odlozone. +- **Zamiana statycznego API na instancyjne / DI** — poza zakresem; caly kontrakt jest statyczny i tak uzywany globalnie. + + + +Brak `.paul/SPECIAL-FLOWS.md` — sekcja skills pominieta. + + + + +## AC-1: Kontrakt publiczny niezmieniony +```gherkin +Given 20 plikow wywoluje DeliveryStatus:: (56 wywolan) +When refaktor jest zakonczony +Then zaden plik konsumenta (serwisy, cron, kontrolery, widoki, test) nie jest zmodyfikowany +And wszystkie publiczne metody i stale DeliveryStatus zachowuja te sama sygnature i zwracane wartosci +``` + +## AC-2: Testy jednostkowe przechodza bez zmian +```gherkin +Given tests/Unit/DeliveryStatusTest.php (slugify, normalize allegro_edge, guess, stale) +When uruchomie phpunit po refaktorze +Then wszystkie testy przechodza +And plik testu nie wymagal zadnej modyfikacji +``` + +## AC-3: Rozdzielone odpowiedzialnosci +```gherkin +Given monolit DeliveryStatus.php (657 lin.) +When refaktor jest zakonczony +Then powstaja 3 nowe klasy: DeliveryStatusProviderMap, AllegroDescriptionGuesser, DeliveryTrackingUrlBuilder +And kazda ma jedna odpowiedzialnosc +And fasada DeliveryStatus.php ma <= 200 lin. (cel ~150-180) +And zadna mapa statusow nie istnieje w wiecej niz jednym miejscu +``` + +## AC-4: Kod sie laduje (brak bledow skladni / autoload) +```gherkin +Given nowe klasy w namespace App\Modules\Shipments +When uruchomie php -l na kazdym pliku i smoke check autoloadu +Then brak bledow skladni +And klasy sa znajdowane przez PSR-4 (App\ -> src/) +``` + + + + + + + Task 1: Wydzielic DeliveryStatusProviderMap (mapowanie statusow dostawcow) + src/Modules/Shipments/DeliveryStatusProviderMap.php, src/Modules/Shipments/DeliveryStatus.php + + Utworz `src/Modules/Shipments/DeliveryStatusProviderMap.php` (namespace `App\Modules\Shipments`, `final class`). + Przenies z DeliveryStatus do nowej klasy: + - prywatne stale map: `INPOST_MAP`, `INPOST_DESCRIPTIONS`, `APACZKA_MAP`, `APACZKA_DESCRIPTIONS`, `ALLEGRO_MAP`, `ALLEGRO_DESCRIPTIONS`, `ALLEGRO_EDGE_MAP`, `ALLEGRO_EDGE_DESCRIPTIONS`, `POLKURIER_MAP`, `POLKURIER_DESCRIPTIONS`, `PROVIDER_MAPS`, `PROVIDER_DESCRIPTIONS`, + - prywatne stale opisow uzywane przez te tablice: `DESC_PICKED_UP_BY_COURIER`, `DESC_DISPATCHED`, `DESC_OUT_FOR_DELIVERY`, `DESC_DELIVERED`, `DESC_RETURNED_TO_SENDER`, `DESC_AWAITING_PICKUP`, + - metody: `getDefaultMappings`, `normalizeWithOverrides`, `descriptionWithOverrides`, `normalize`, `description`. + WAZNE: wewnatrz przeniesionych map odwolania `self::CREATED`/`self::DELIVERED`/... zamien na `DeliveryStatus::CREATED` itd. (ten sam namespace, bez importu). `self::UNKNOWN` w `normalize` -> `DeliveryStatus::UNKNOWN`. + W DeliveryStatus zostaw publiczne metody-delegaty (jednolinijkowce), zachowujac identyczne sygnatury i PHPDoc: + `public static function getDefaultMappings(string $provider): array { return DeliveryStatusProviderMap::getDefaultMappings($provider); }` — analogicznie dla `normalize`, `description`, `normalizeWithOverrides`, `descriptionWithOverrides`. + Usun z DeliveryStatus przeniesione stale prywatne (map i `DESC_*`). + + php -l src/Modules/Shipments/DeliveryStatusProviderMap.php; php -l src/Modules/Shipments/DeliveryStatus.php; php vendor/bin/phpunit tests/Unit/DeliveryStatusTest.php + AC-1, AC-2, AC-3, AC-4 — mapowanie dostawcow w jednej dedykowanej klasie, fasada deleguje, testy przechodza. + + + + Task 2: Wydzielic AllegroDescriptionGuesser (heurystyki opisow Allegro) + src/Modules/Shipments/AllegroDescriptionGuesser.php, src/Modules/Shipments/DeliveryStatus.php + + Utworz `src/Modules/Shipments/AllegroDescriptionGuesser.php` (namespace `App\Modules\Shipments`, `final class`). + Przenies z DeliveryStatus: + - prywatna stala `DESCRIPTION_STATUS_PATTERNS`, + - metody publiczne `slugifyAllegroDescription`, `guessStatusFromDescription`, + - prywatny helper `containsAny` (uzywany wylacznie przez `guessStatusFromDescription`). + Odwolania `self::READY_FOR_PICKUP`/`self::UNKNOWN`/... w przeniesionym kodzie zamien na `DeliveryStatus::READY_FOR_PICKUP` itd. + W DeliveryStatus zostaw delegaty: + `public static function slugifyAllegroDescription(string $description): string { return AllegroDescriptionGuesser::slugifyAllegroDescription($description); }` oraz analogiczny dla `guessStatusFromDescription`. + Usun z DeliveryStatus przeniesione skladowe (w tym `containsAny`). + + php -l src/Modules/Shipments/AllegroDescriptionGuesser.php; php -l src/Modules/Shipments/DeliveryStatus.php; php vendor/bin/phpunit tests/Unit/DeliveryStatusTest.php + AC-1, AC-2, AC-3, AC-4 — heurystyki opisow w osobnej klasie; testy slugify/guess przechodza bez zmiany pliku testu. + + + + Task 3: Wydzielic DeliveryTrackingUrlBuilder (budowanie URL sledzenia) + src/Modules/Shipments/DeliveryTrackingUrlBuilder.php, src/Modules/Shipments/DeliveryStatus.php + + Utworz `src/Modules/Shipments/DeliveryTrackingUrlBuilder.php` (namespace `App\Modules\Shipments`, `final class`). + Przenies z DeliveryStatus: + - stale `TRACKING_INPOST_URL`, `TRACKING_ALLEGRO_URL`, `PROVIDER_TRACKING_URLS`, `CARRIER_TRACKING_URLS`, + - metode publiczna `trackingUrl`, + - prywatne helpery `carrierTrackingUrl`, `providerTrackingUrl`, `fallbackTrackingUrl`, `matchCarrierByName`, `carrierMatches`. + W DeliveryStatus zostaw delegat: + `public static function trackingUrl(string $provider, string $trackingNumber, string $carrierId = ''): ?string { return DeliveryTrackingUrlBuilder::trackingUrl($provider, $trackingNumber, $carrierId); }`. + Usun z DeliveryStatus przeniesione stale i helpery. + Po tym tasku w DeliveryStatus zostaja TYLKO: stale statusow (UNKNOWN..PROBLEM), `TERMINAL_STATUSES`, `ALL_STATUSES`, `LABEL_PL`, most do DB (`$repository`, `setRepository`, `getAllStatuses`, `getAllOptions`, `getColor`, `label`), `isTerminal` oraz metody-delegaty z Task 1-3. + + php -l src/Modules/Shipments/DeliveryTrackingUrlBuilder.php; php -l src/Modules/Shipments/DeliveryStatus.php; php vendor/bin/phpunit tests/Unit/DeliveryStatusTest.php; (Get-Content src/Modules/Shipments/DeliveryStatus.php | Measure-Object -Line).Lines + AC-1, AC-2, AC-3, AC-4 — budowanie URL w osobnej klasie; fasada DeliveryStatus.php <= 200 lin. + + + + Task 4: Aktualizacja dokumentacji technicznej + .paul/codebase/architecture.md, .paul/codebase/tech_changelog.md + + W `architecture.md` (sekcja modulu Shipments / warstwy) dopisz nowy podzial: `DeliveryStatus` jako fasada + `DeliveryStatusProviderMap` + `AllegroDescriptionGuesser` + `DeliveryTrackingUrlBuilder`. + W `tech_changelog.md` dopisz wpis chronologiczny (data 2026-05-19): co i dlaczego — dekompozycja god-klasy statusow na fasade + 3 wspolpracownikow, kontrakt publiczny zachowany, zero zmian w call-site, redukcja DeliveryStatus.php z 657 do ~N lin. + Nie ruszaj `db_schema.md` (brak zmian schematu). + + Reczny przeglad obu plikow — wpisy obecne i spojne z faktycznym stanem kodu. + AC-3 — dokumentacja odzwierciedla nowy podzial klas. + + + + + +## Do Not Change +- Zadnego z 20 plikow konsumentow `DeliveryStatus::` (serwisy trackingu, cron, kontrolery, widoki, `SmsVariableResolver`, `ShipmentsModule`, `bin/smoke_routes.php`). +- `tests/Unit/DeliveryStatusTest.php` — musi przejsc BEZ modyfikacji (to dowod, ze kontrakt jest zachowany). +- Sygnatury, nazwy i zwracane wartosci publicznych metod oraz stale `DeliveryStatus`. +- Migracje SQL (`20260422_000101_*` i pozostale). +- `routes/web.php`, `ServiceRegistry`, providery modulow (refaktor jest wewnatrz Shipments, brak nowych route'ow/serwisow w DI). + +## Scope Limits +- Brak zmian schematu DB i migracji. +- Brak zamiany statycznego API na instancyjne/DI. +- Brak `enum` PHP zamiast stalych string. +- Brak abstrakcji bazowej dla `*StatusMappingRepository` (osobny, przyszly plan). +- Brak nowych testow poza utrzymaniem zielonego `DeliveryStatusTest` (mozna dopisac testy charakteryzujace trackingUrl/getDefaultMappings tylko jesli nie wymaga to zmiany istniejacych plikow — opcjonalne, nie blokujace). + + + +- [ ] `php vendor/bin/phpunit tests/Unit/DeliveryStatusTest.php` — wszystkie testy zielone, plik testu niezmieniony (PRIMARY GATE — kontrakt fasady). +- [ ] `php -l` bez bledow dla: `DeliveryStatus.php`, `DeliveryStatusProviderMap.php`, `AllegroDescriptionGuesser.php`, `DeliveryTrackingUrlBuilder.php`. +- [ ] `git diff --name-only` pokazuje TYLKO 4 pliki kodu (1 zmodyfikowany + 3 nowe) + 2 pliki docs — zaden plik konsumenta. +- [ ] `(Get-Content src/Modules/Shipments/DeliveryStatus.php | Measure-Object -Line).Lines` <= 200. +- [ ] `php bin/smoke_routes.php` (jesli dostepny) — bootstrap przechodzi (weryfikuje `setRepository` + autoload). +- [ ] Quality Radar: ryzyko "drugie zrodlo prawdy" obsluzone (mapy przeniesione, nie skopiowane). + + + +- [ ] Wszystkie AC (AC-1..AC-4) spelnione. +- [ ] Weryfikacja kompletna (phpunit + php -l + diff scope + rozmiar fasady). +- [ ] `architecture.md` i `tech_changelog.md` zaktualizowane. +- [ ] `quality_risks.md` zaktualizowane w UNIFY (wpis `DeliveryStatus.php` oznaczony jako zrefaktorowany, analogicznie do Allegro/Statistics). + + + +SUMMARY.md path: `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md` + diff --git a/.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md b/.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md new file mode 100644 index 0000000..d500a93 --- /dev/null +++ b/.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md @@ -0,0 +1,77 @@ +--- +plan_id: 20260519-1730-refactor-delivery-status +title: Refaktoryzacja DeliveryStatus — fasada + 3 wspolpracownikow +completed: 2026-05-19T18:10:00 +storage: plan-first +quality_radar: ok +--- + +# Summary: Refaktoryzacja DeliveryStatus — fasada + 3 wspolpracownikow + +## Objective + +Rozbicie `src/Modules/Shipments/DeliveryStatus.php` (657 lin., god-klasa statusow uzywana globalnie — 56 wywolan w 20 plikach) na czytelna fasade + trzech wyspecjalizowanych wspolpracownikow, przy 100% zachowaniu kontraktu publicznego (zero zmian w plikach konsumentow). + +## What Was Built + +| Area | Result | +|------|--------| +| Fasada `DeliveryStatus` | 657 -> **170 lin.**; stale statusow, `LABEL_PL`, `ALL_STATUSES`, `TERMINAL_STATUSES`, most do DB, `isTerminal` + metody-delegaty | +| Mapowanie dostawcow | nowy `DeliveryStatusProviderMap` (~330 lin.) — `INPOST_*`/`APACZKA_*`/`ALLEGRO_*`/`ALLEGRO_EDGE_*`/`POLKURIER_*` + `normalize`/`description`/`getDefaultMappings`/`*WithOverrides` + prywatne `DESC_*` | +| Heurystyki opisow Allegro | nowy `AllegroDescriptionGuesser` (~150 lin.) — `slugifyAllegroDescription`, `guessStatusFromDescription`, `DESCRIPTION_STATUS_PATTERNS`, `containsAny` | +| URL sledzenia | nowy `DeliveryTrackingUrlBuilder` (~85 lin.) — `trackingUrl` + `PROVIDER_TRACKING_URLS`/`CARRIER_TRACKING_URLS` + helpery | + +## Files Modified + +- `src/Modules/Shipments/DeliveryStatus.php` — zredukowany do fasady delegujacej (657 -> 170). +- `src/Modules/Shipments/DeliveryStatusProviderMap.php` — NOWY; mapy surowych statusow per dostawca. +- `src/Modules/Shipments/AllegroDescriptionGuesser.php` — NOWY; heurystyki opisow Allegro edge. +- `src/Modules/Shipments/DeliveryTrackingUrlBuilder.php` — NOWY; budowanie URL-i sledzenia. +- `.paul/codebase/architecture.md` — opis nowego podzialu w wierszu modulu Shipments. +- `.paul/codebase/tech_changelog.md` — wpis chronologiczny 2026-05-19. + +## Acceptance Criteria Results + +| Criterion | Status | Evidence | +|-----------|--------|----------| +| AC-1: Kontrakt publiczny niezmieniony | Pass | `git status --porcelain` — zaden z 20 plikow konsumentow niemodyfikowany; zmieniony tylko `DeliveryStatus.php` | +| AC-2: Testy przechodza bez zmian | Pass | `DeliveryStatusTest` 4/4 (7 asercji); `git diff --stat tests/Unit/DeliveryStatusTest.php` pusty | +| AC-3: Rozdzielone odpowiedzialnosci | Pass | 3 nowe klasy, fasada 170 lin. (<= 200); mapy w jednym miejscu (przeniesione, nie skopiowane) | +| AC-4: Kod sie laduje | Pass | `php -l` clean x4; test przechodzacy przez `slugify`/`normalize` potwierdza autoload PSR-4 nowych klas | + +## Verification Results + +| Check | Result | Notes | +|-------|--------|-------| +| `php vendor/bin/phpunit tests/Unit/DeliveryStatusTest.php` | Pass | 4 testy / 7 asercji (PRIMARY GATE), plik testu niezmieniony | +| `php -l` (4 pliki) | Pass | brak bledow skladni | +| rozmiar fasady | Pass | 170 lin. (cel ~150-180, limit <= 200) | +| `git status --porcelain` (scope) | Pass | tylko 4 pliki kodu + 2 docs + STATE.md (workflow) | +| `php vendor/bin/phpunit` (full) | Pass (neutralnie) | 3 errors / 15 failures **identyczne z baseline na HEAD** (stash `DeliveryStatus.php` -> ten sam wynik) — pre-existing, niezwiazane | +| `php bin/cron` / `bin/smoke_routes.php` | Skipped | DB niedostepne — `setRepository` nie zweryfikowany runtime (plan dopuszczal "jesli dostepny") | + +## Quality Radar Results + +**Status:** ok + +- New risks: brak. Powstaly 3 nowe klasy, kazda < 350 lin., jedna odpowiedzialnosc. +- Resolved risks: `DeliveryStatus.php` (657 lin.) zdjety z listy kandydatow > 500 lin. w `quality_risks.md`. +- Deferred risks: enum PHP zamiast stalych string; abstrakcja bazowa dla `*StatusMappingRepository`; zamiana statycznego API na instancyjne/DI — wszystkie poza zakresem (osobne plany). +- Tools: codebase-memory-mcp (graf bez zmian strukturalnych poza +3 klasy); jscpd/ast-grep disabled by policy. Raw outputs: `.paul/codebase/radar/`. + +## Deviations + +- **Tryb wykonania inline zamiast delegated (auto):** wszystkie 4 zadania edytuja wspolny `DeliveryStatus.php` — delegacja rownolegla niemozliwa (konflikt plikow), a precyzyjne zachowanie kontraktu wymagalo pelnej kontroli. Ekstrakcja wykonana atomowo: 3 nowe pliki + jeden czysty zapis fasady, nastepnie pelna weryfikacja. Rownowazne wynikowo zadaniom T1-T3, nizsze ryzyko niz 3 przeplatane edycje. +- **Laczna liczba linii rosnie** (boilerplate 3 klas + PHPDoc) — oczekiwane i zapowiedziane w planie; nie jest regresja. + +## Key Decisions / Patterns + +- **Fasada zamiast pelnej ekstrakcji** — wymuszone przez kontrakt: stale `DeliveryStatus::DELIVERED` itd. uzywane wprost w serwisach, widokach i tescie nie da sie oddelegowac. Fasada = zerowy blast radius dla integracji. +- **Helpery prywatne wedruja z wywolujacymi** (`containsAny` -> guesser, `carrierMatches` -> URL builder) — bez tworzenia sztucznej klasy util dla 2 metod. +- **Stale-klucze map** (`DeliveryStatus::CREATED` jako klucz w `DESCRIPTION_STATUS_PATTERNS`) dzialaja jako constant-expression w `const` array (PHP 8.2). + +## Follow-up + +- **Luka testowa (znana):** test pokrywa 4 z ~13 metod fasady; pozostale delegaty (`trackingUrl`, `getDefaultMappings`, `*WithOverrides`, most do DB, `label`/`getColor`/`getAllStatuses`/`getAllOptions`) niesprawdzone runtime — mechanicznie bezpieczne (jednolinijkowe passthrough). Kandydat: contract test fasady. +- **Smoke-test reczny przed commitem:** widoki `/orders/show` + `/shipments/prepare` (label/description/getColor/trackingUrl/ALL_STATUSES) oraz tracking Apaczka/InPost/Polkurier (DB niedostepne w sesji). +- **Kolejni kandydaci** z `quality_risks.md`: `OrdersController.php` (1490), `OrdersRepository.php` (1243), `ShopproIntegrationsController.php` (1076). diff --git a/src/Modules/Shipments/AllegroDescriptionGuesser.php b/src/Modules/Shipments/AllegroDescriptionGuesser.php new file mode 100644 index 0000000..61147e7 --- /dev/null +++ b/src/Modules/Shipments/AllegroDescriptionGuesser.php @@ -0,0 +1,145 @@ + [ + 'delivered', + 'picked up by recipient', + 'doręczon', + 'dostarczono', + 'odebrana przez odbiorc', + ], + DeliveryStatus::PICKED_UP => [ + 'picked up by courier', + 'picked up from point', + 'podjęta', + 'podjeta', + 'odebrana przez kuriera', + ], + DeliveryStatus::RETURNED => [ + 'returned', + 'zwrócon', + 'zwrocona', + ], + DeliveryStatus::CANCELLED => [ + 'cancelled', + 'canceled', + 'anulowan', + ], + DeliveryStatus::OUT_FOR_DELIVERY => [ + 'out for delivery', + 'released for delivery', + 'doręczeni', + 'doreczenia', + 'wydana do', + ], + DeliveryStatus::READY_FOR_PICKUP => [ + 'awaiting pick-up', + 'awaiting pickup', + 'ready for pickup', + 'ready for pick-up', + 'oczekuje na odb', + 'gotowa do odb', + ], + DeliveryStatus::IN_TRANSIT => [ + 'courier', + 'warehouse', + 'branch', + 'in transit', + 'sortowni', + 'magazyn', + 'w drodze', + 'tranzyt', + 'kurier', + 'wyjechał', + 'wyjechala', + ], + DeliveryStatus::CONFIRMED => [ + 'dispatched', + 'nadana', + 'nadano', + ], + DeliveryStatus::CREATED => [ + 'prepared', + 'created', + 'przygotowan', + 'utworzon', + ], + DeliveryStatus::PROBLEM => [ + 'damaged', + 'problem', + 'lost', + 'uszkodzon', + 'zagubion', + ], + ]; + + public static function slugifyAllegroDescription(string $description): string + { + $text = trim($description); + if ($text === '') { + return 'unknown'; + } + + // Usuń typowe prefiksy + $text = preg_replace('/^Przesy[łl]ka zosta[łl]a\s+/ui', '', $text); + $text = preg_replace('/^Kurier\s+/ui', '', $text); + $text = preg_replace('/^Paczka zosta[łl]a\s+/ui', '', $text); + $text = preg_replace('/^Parcel has been\s+/i', '', $text); + $text = preg_replace('/^Parcel is\s+/i', '', $text); + $text = preg_replace('/^Courier has\s+/i', '', $text); + + // Polskie znaki na ASCII + $polish = ['ą','ć','ę','ł','ń','ó','ś','ź','ż','Ą','Ć','Ę','Ł','Ń','Ó','Ś','Ź','Ż']; + $ascii = ['a','c','e','l','n','o','s','z','z','A','C','E','L','N','O','S','Z','Z']; + $text = str_replace($polish, $ascii, $text); + + // Lowercase, zamień nie-alfanumeryczne na podkreślenia + $text = strtolower($text); + $text = preg_replace('/[^a-z0-9]+/', '_', $text); + $text = trim($text, '_'); + + return $text !== '' ? $text : 'unknown'; + } + + /** + * Keyword-based fallback for unknown Allegro edge descriptions. + * Used when slugified description is not in ALLEGRO_EDGE_MAP. + */ + public static function guessStatusFromDescription(string $description): string + { + $lower = mb_strtolower($description, 'UTF-8'); + + foreach (self::DESCRIPTION_STATUS_PATTERNS as $status => $patterns) { + if (self::containsAny($lower, $patterns)) { + return $status; + } + } + + return str_contains($lower, "odbi\u{00F3}r") && !str_contains($lower, 'w drodze') + ? DeliveryStatus::READY_FOR_PICKUP + : DeliveryStatus::UNKNOWN; + } + + /** + * @param array $needles + */ + private static function containsAny(string $haystack, array $needles): bool + { + foreach ($needles as $needle) { + if (str_contains($haystack, $needle)) { + return true; + } + } + + return false; + } +} diff --git a/src/Modules/Shipments/DeliveryStatus.php b/src/Modules/Shipments/DeliveryStatus.php index ff45a52..8e9f3f0 100644 --- a/src/Modules/Shipments/DeliveryStatus.php +++ b/src/Modules/Shipments/DeliveryStatus.php @@ -3,6 +3,16 @@ declare(strict_types=1); namespace App\Modules\Shipments; +/** + * Slownik znormalizowanych statusow dostawy (zrodlo prawdy dla calej aplikacji) + * oraz fasada delegujaca do wyspecjalizowanych wspolpracownikow: + * - {@see DeliveryStatusProviderMap} — mapowanie surowych statusow dostawcow, + * - {@see AllegroDescriptionGuesser} — heurystyki opisow Allegro, + * - {@see DeliveryTrackingUrlBuilder} — budowanie URL-i sledzenia. + * + * Stale i metody publiczne stanowia stabilny kontrakt uzywany przez wszystkie + * integracje kurierskie, cron, kontrolery i widoki — nie zmieniac sygnatur. + */ final class DeliveryStatus { private static ?DeliveryStatusRepository $repository = null; @@ -25,272 +35,20 @@ final class DeliveryStatus self::CANCELLED, ]; - private const DESC_PICKED_UP_BY_COURIER = 'Odebrana przez kuriera'; - private const DESC_DISPATCHED = "Przesy\u{0142}ka nadana"; - private const DESC_OUT_FOR_DELIVERY = "W dor\u{0119}czeniu"; - private const DESC_DELIVERED = "Dor\u{0119}czona"; - private const DESC_RETURNED_TO_SENDER = "Zwr\u{00F3}cona do nadawcy"; - private const DESC_AWAITING_PICKUP = "Oczekuje na odbi\u{00F3}r"; - private const TRACKING_INPOST_URL = 'https://inpost.pl/sledzenie-przesylek?number='; - private const TRACKING_ALLEGRO_URL = 'https://allegro.pl/allegrodelivery/sledzenie-paczki?numer='; - public const LABEL_PL = [ self::UNKNOWN => 'Nieznany', self::CREATED => 'Utworzona', self::CONFIRMED => 'Potwierdzona', - self::PICKED_UP => self::DESC_PICKED_UP_BY_COURIER, + self::PICKED_UP => 'Odebrana przez kuriera', self::IN_TRANSIT => 'W tranzycie', - self::OUT_FOR_DELIVERY => self::DESC_OUT_FOR_DELIVERY, + self::OUT_FOR_DELIVERY => "W dor\u{0119}czeniu", self::READY_FOR_PICKUP => 'Gotowa do odbioru', - self::DELIVERED => self::DESC_DELIVERED, + self::DELIVERED => "Dor\u{0119}czona", self::RETURNED => 'Zwrócona', self::CANCELLED => 'Anulowana', self::PROBLEM => 'Problem', ]; - private const INPOST_MAP = [ - 'created' => self::CREATED, - 'offers_prepared' => self::CREATED, - 'offer_selected' => self::CREATED, - 'confirmed' => self::CONFIRMED, - 'dispatched' => self::CONFIRMED, - 'collected' => self::PICKED_UP, - 'taken_by_courier' => self::PICKED_UP, - 'adopted_at_source_branch' => self::IN_TRANSIT, - 'adopted_at_sorting_center' => self::IN_TRANSIT, - 'sent_from_sorting_center' => self::IN_TRANSIT, - 'adopted_at_target_sorting_center' => self::IN_TRANSIT, - 'sent_from_target_sorting_center' => self::IN_TRANSIT, - 'adopted_at_target_branch' => self::IN_TRANSIT, - 'out_for_delivery' => self::OUT_FOR_DELIVERY, - 'ready_to_pickup' => self::READY_FOR_PICKUP, - 'ready_to_pickup_from_branch' => self::READY_FOR_PICKUP, - 'ready_to_pickup_from_pok' => self::READY_FOR_PICKUP, - 'stack_in_box_machine' => self::READY_FOR_PICKUP, - 'stack_in_customer_service_point' => self::READY_FOR_PICKUP, - 'delivered' => self::DELIVERED, - 'claimed' => self::DELIVERED, - 'returned_to_sender' => self::RETURNED, - 'undelivered' => self::RETURNED, - 'undelivered_wrong_address' => self::RETURNED, - 'undelivered_incomplete_address' => self::RETURNED, - 'undelivered_unknown_recipient' => self::RETURNED, - 'undelivered_cod_cash_receiver' => self::RETURNED, - 'cancelled' => self::CANCELLED, - 'expired' => self::CANCELLED, - 'avizo' => self::PROBLEM, - 'pickup_time_expired' => self::PROBLEM, - 'stack_parcel_pickup_time_expired' => self::PROBLEM, - 'missing' => self::PROBLEM, - 'delay_in_delivery' => self::PROBLEM, - 'oversized' => self::PROBLEM, - 'pickup_reminder_sent' => self::READY_FOR_PICKUP, - 'pickup_reminder_sent_address' => self::READY_FOR_PICKUP, - 'readdressed' => self::IN_TRANSIT, - 'redirect_to_box' => self::IN_TRANSIT, - ]; - - private const INPOST_DESCRIPTIONS = [ - 'created' => 'Przesyłka utworzona', - 'offers_prepared' => 'Oferty cenowe przygotowane', - 'offer_selected' => 'Oferta wybrana', - 'confirmed' => 'Przesyłka potwierdzona', - 'dispatched' => self::DESC_DISPATCHED, - 'collected' => 'Odebrana od nadawcy', - 'taken_by_courier' => self::DESC_PICKED_UP_BY_COURIER, - 'adopted_at_source_branch' => 'Przyjęta w oddziale źródłowym', - 'adopted_at_sorting_center' => 'Przyjęta w centrum sortowania', - 'sent_from_sorting_center' => 'Wysłana z centrum sortowania', - 'adopted_at_target_sorting_center' => 'Przyjęta w docelowym centrum sortowania', - 'sent_from_target_sorting_center' => 'Wysłana z docelowego centrum sortowania', - 'adopted_at_target_branch' => 'Przyjęta w oddziale docelowym', - 'out_for_delivery' => 'W drodze do odbiorcy', - 'ready_to_pickup' => 'Gotowa do odbioru w paczkomacie', - 'ready_to_pickup_from_branch' => 'Gotowa do odbioru z oddziału', - 'ready_to_pickup_from_pok' => 'Gotowa do odbioru z POK', - 'stack_in_box_machine' => 'Umieszczona w paczkomacie', - 'stack_in_customer_service_point' => 'Umieszczona w punkcie obsługi', - 'delivered' => self::DESC_DELIVERED, - 'claimed' => 'Odebrana po awizo', - 'returned_to_sender' => self::DESC_RETURNED_TO_SENDER, - 'undelivered' => 'Niedoręczona', - 'undelivered_wrong_address' => 'Niedoręczona — błędny adres', - 'undelivered_incomplete_address' => 'Niedoręczona — niepełny adres', - 'undelivered_unknown_recipient' => 'Niedoręczona — nieznany odbiorca', - 'undelivered_cod_cash_receiver' => 'Niedoręczona — problem z pobraniem', - 'cancelled' => 'Anulowana', - 'expired' => 'Wygasła', - 'avizo' => 'Awizowana', - 'pickup_time_expired' => 'Czas odbioru upłynął', - 'stack_parcel_pickup_time_expired' => 'Czas odbioru ze stack upłynął', - 'missing' => 'Przesyłka zagubiona', - 'delay_in_delivery' => 'Opóźnienie w dostawie', - 'oversized' => 'Przesyłka ponadgabarytowa', - 'pickup_reminder_sent' => 'Wysłano przypomnienie o odbiorze', - 'pickup_reminder_sent_address' => 'Przypomnienie wysłane na adres', - 'readdressed' => 'Przekierowana na inny adres', - 'redirect_to_box' => 'Przekierowana do paczkomatu', - ]; - - private const APACZKA_MAP = [ - '0' => self::CREATED, - '1' => self::CONFIRMED, - '2' => self::PICKED_UP, - '3' => self::IN_TRANSIT, - '4' => self::OUT_FOR_DELIVERY, - '5' => self::DELIVERED, - '6' => self::RETURNED, - '7' => self::CANCELLED, - '8' => self::PROBLEM, - '9' => self::READY_FOR_PICKUP, - '10' => self::IN_TRANSIT, - 'NEW' => self::CREATED, - 'PENDING' => self::CREATED, - 'CONFIRMED' => self::CONFIRMED, - 'PICKED_UP' => self::PICKED_UP, - 'PICKUP' => self::PICKED_UP, - 'IN_TRANSIT' => self::IN_TRANSIT, - 'OUT_FOR_DELIVERY' => self::OUT_FOR_DELIVERY, - 'DELIVERED' => self::DELIVERED, - 'RETURNED' => self::RETURNED, - 'RETURNED_TO_SHIPPER' => self::RETURNED, - 'CANCELLED' => self::CANCELLED, - 'ERROR' => self::PROBLEM, - 'WAITING_FOR_PICKUP' => self::READY_FOR_PICKUP, - 'REDIRECT' => self::IN_TRANSIT, - ]; - - private const APACZKA_DESCRIPTIONS = [ - '0' => 'Oczekuje na przetworzenie', - '1' => 'Zamówienie potwierdzone', - '2' => self::DESC_PICKED_UP_BY_COURIER, - '3' => 'W transporcie', - '4' => self::DESC_OUT_FOR_DELIVERY, - '5' => self::DESC_DELIVERED, - '6' => self::DESC_RETURNED_TO_SENDER, - '7' => 'Anulowana', - '8' => 'Błąd zamówienia', - '9' => self::DESC_AWAITING_PICKUP . ' w punkcie', - '10' => 'Przekierowana', - 'NEW' => 'Zamówienie utworzone', - 'PENDING' => 'Oczekuje na przetworzenie', - 'CONFIRMED' => 'Zamówienie potwierdzone', - 'PICKED_UP' => self::DESC_PICKED_UP_BY_COURIER, - 'PICKUP' => self::DESC_PICKED_UP_BY_COURIER, - 'IN_TRANSIT' => 'W transporcie', - 'OUT_FOR_DELIVERY' => self::DESC_OUT_FOR_DELIVERY, - 'DELIVERED' => self::DESC_DELIVERED, - 'RETURNED' => self::DESC_RETURNED_TO_SENDER, - 'RETURNED_TO_SHIPPER' => self::DESC_RETURNED_TO_SENDER, - 'CANCELLED' => 'Anulowana', - 'ERROR' => 'Błąd zamówienia', - 'WAITING_FOR_PICKUP' => self::DESC_AWAITING_PICKUP . ' w punkcie', - 'REDIRECT' => 'Przekierowana', - ]; - - private const ALLEGRO_MAP = [ - 'NEW' => self::CREATED, - 'READY_TO_SHIP' => self::CONFIRMED, - 'collected_from_sender' => self::PICKED_UP, - 'IN_TRANSIT' => self::IN_TRANSIT, - 'DELIVERED' => self::DELIVERED, - 'CANCELLED' => self::CANCELLED, - 'ERROR' => self::PROBLEM, - 'RETURNED' => self::RETURNED, - ]; - - private const ALLEGRO_DESCRIPTIONS = [ - 'NEW' => 'Przesyłka utworzona', - 'READY_TO_SHIP' => 'Etykieta wygenerowana, oczekuje na nadanie', - 'collected_from_sender' => 'Odebrana od nadawcy', - 'IN_TRANSIT' => 'Odebrana przez przewoźnika', - 'DELIVERED' => self::DESC_DELIVERED, - 'CANCELLED' => 'Anulowana', - 'ERROR' => 'Błąd przetwarzania', - 'RETURNED' => self::DESC_RETURNED_TO_SENDER, - ]; - - private const ALLEGRO_EDGE_MAP = [ - // Realne slugi z edge API (po slugify opisów) - 'przygotowana_przez_nadawce' => self::CREATED, - 'prepared_by_the_sender' => self::CREATED, - 'nadana' => self::CONFIRMED, - 'dispatched' => self::CONFIRMED, - 'podjeta_z_maszyny_przez_kuriera' => self::PICKED_UP, - 'podjeta_z_punktu_przez_kuriera' => self::PICKED_UP, - 'podjeta_z_punktu' => self::PICKED_UP, - 'odebrana_przez_kuriera' => self::PICKED_UP, - 'picked_up_from_point_by_courier' => self::PICKED_UP, - 'picked_up_by_the_courier' => self::PICKED_UP, - 'przekazal_przesylke_do_magazynu' => self::IN_TRANSIT, - 'przekazana_do_magazynu' => self::IN_TRANSIT, - 'transferred_the_parcel_to_the_warehouse' => self::IN_TRANSIT, - 'accepted_at_the_branch' => self::IN_TRANSIT, - 'przesylka_wyjechala_w_droge_do_punktu_docelowego' => self::IN_TRANSIT, - 'w_sortowni' => self::IN_TRANSIT, - 'wyjechala_w_droge_do_punktu_docelowego' => self::IN_TRANSIT, - 'wyslana_z_sortowni' => self::IN_TRANSIT, - 'w_doreczeniu' => self::OUT_FOR_DELIVERY, - 'wydana_do_doreczenia' => self::OUT_FOR_DELIVERY, - 'released_for_delivery' => self::OUT_FOR_DELIVERY, - 'dostarczana' => self::OUT_FOR_DELIVERY, - 'gotowa_do_odbioru' => self::READY_FOR_PICKUP, - 'oczekuje_na_odbior' => self::READY_FOR_PICKUP, - 'przesylka_oczekuje_na_odbior' => self::READY_FOR_PICKUP, - 'awaiting_pick_up' => self::READY_FOR_PICKUP, - 'dostarczona' => self::DELIVERED, - 'doreczona' => self::DELIVERED, - 'odebrana' => self::DELIVERED, - 'delivered' => self::DELIVERED, - 'zwrocona' => self::RETURNED, - 'zwrocona_do_nadawcy' => self::RETURNED, - 'returned_to_the_sender' => self::RETURNED, - 'anulowana' => self::CANCELLED, - 'cancelled' => self::CANCELLED, - 'odmowa_przyjecia' => self::PROBLEM, - 'uszkodzona' => self::PROBLEM, - 'zagubiona' => self::PROBLEM, - ]; - - private const ALLEGRO_EDGE_DESCRIPTIONS = [ - 'przygotowana_przez_nadawce' => 'Przesyłka przygotowana przez nadawcę', - 'prepared_by_the_sender' => 'Przesyłka przygotowana przez nadawcę', - 'nadana' => self::DESC_DISPATCHED, - 'dispatched' => self::DESC_DISPATCHED, - 'podjeta_z_maszyny_przez_kuriera' => 'Podjęta z maszyny przez kuriera', - 'podjeta_z_punktu_przez_kuriera' => 'Podjęta z punktu przez kuriera', - 'odebrana_przez_kuriera' => self::DESC_PICKED_UP_BY_COURIER, - 'picked_up_from_point_by_courier' => 'Podjęta z punktu przez kuriera', - 'picked_up_by_the_courier' => self::DESC_PICKED_UP_BY_COURIER, - 'przekazana_do_magazynu' => 'Przekazana do magazynu', - 'transferred_the_parcel_to_the_warehouse' => 'Przekazana do magazynu', - 'accepted_at_the_branch' => 'Przyjęta w oddziale', - 'przesylka_wyjechala_w_droge_do_punktu_docelowego' => 'Wyjechała w drogę do punktu docelowego', - 'w_sortowni' => 'W sortowni', - 'wyjechala_w_droge_do_punktu_docelowego' => 'Wyjechała w drogę do punktu docelowego', - 'wyslana_z_sortowni' => 'Wysłana z sortowni', - 'w_doreczeniu' => self::DESC_OUT_FOR_DELIVERY, - 'wydana_do_doreczenia' => 'Wydana do doręczenia', - 'released_for_delivery' => 'Wydana do doręczenia', - 'dostarczana' => 'Dostarczana', - 'gotowa_do_odbioru' => 'Gotowa do odbioru', - 'oczekuje_na_odbior' => self::DESC_AWAITING_PICKUP, - 'przesylka_oczekuje_na_odbior' => self::DESC_AWAITING_PICKUP, - 'awaiting_pick_up' => self::DESC_AWAITING_PICKUP, - 'dostarczona' => 'Dostarczona', - 'doreczona' => self::DESC_DELIVERED, - 'odebrana' => 'Odebrana', - 'delivered' => 'Dostarczona', - 'zwrocona' => 'Zwrócona', - 'zwrocona_do_nadawcy' => self::DESC_RETURNED_TO_SENDER, - 'returned_to_the_sender' => self::DESC_RETURNED_TO_SENDER, - 'anulowana' => 'Anulowana', - 'cancelled' => 'Anulowana', - 'odmowa_przyjecia' => 'Odmowa przyjęcia', - 'uszkodzona' => 'Uszkodzona', - 'zagubiona' => 'Zagubiona', - ]; - public const ALL_STATUSES = [ self::UNKNOWN, self::CREATED, @@ -305,151 +63,14 @@ final class DeliveryStatus self::PROBLEM, ]; - private const POLKURIER_MAP = [ - // Oficjalne kody ORDER_STATUS z dokumentacji polkurier API v1.11 (marzec 2026) - 'O' => self::CREATED, - 'P' => self::CONFIRMED, - 'A' => self::CANCELLED, - 'WP' => self::IN_TRANSIT, - 'D' => self::DELIVERED, - 'Z' => self::RETURNED, - 'W' => self::PROBLEM, - ]; - - private const POLKURIER_DESCRIPTIONS = [ - 'O' => 'Oczekuje na płatność', - 'P' => 'Potwierdzone, list wygenerowany', - 'A' => 'Anulowane', - 'WP' => 'W przewozie', - 'D' => 'Dostarczona', - 'Z' => 'Zwrot do nadawcy', - 'W' => 'Wyjątek', - ]; - - private const PROVIDER_MAPS = [ - 'inpost' => self::INPOST_MAP, - 'apaczka' => self::APACZKA_MAP, - 'allegro_wza' => self::ALLEGRO_MAP, - 'allegro_edge' => self::ALLEGRO_EDGE_MAP, - 'polkurier' => self::POLKURIER_MAP, - ]; - - private const PROVIDER_DESCRIPTIONS = [ - 'inpost' => self::INPOST_DESCRIPTIONS, - 'apaczka' => self::APACZKA_DESCRIPTIONS, - 'allegro_wza' => self::ALLEGRO_DESCRIPTIONS, - 'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS, - 'polkurier' => self::POLKURIER_DESCRIPTIONS, - ]; - - private const DESCRIPTION_STATUS_PATTERNS = [ - self::DELIVERED => [ - 'delivered', - 'picked up by recipient', - 'doręczon', - 'dostarczono', - 'odebrana przez odbiorc', - ], - self::PICKED_UP => [ - 'picked up by courier', - 'picked up from point', - 'podjęta', - 'podjeta', - 'odebrana przez kuriera', - ], - self::RETURNED => [ - 'returned', - 'zwrócon', - 'zwrocona', - ], - self::CANCELLED => [ - 'cancelled', - 'canceled', - 'anulowan', - ], - self::OUT_FOR_DELIVERY => [ - 'out for delivery', - 'released for delivery', - 'doręczeni', - 'doreczenia', - 'wydana do', - ], - self::READY_FOR_PICKUP => [ - 'awaiting pick-up', - 'awaiting pickup', - 'ready for pickup', - 'ready for pick-up', - 'oczekuje na odb', - 'gotowa do odb', - ], - self::IN_TRANSIT => [ - 'courier', - 'warehouse', - 'branch', - 'in transit', - 'sortowni', - 'magazyn', - 'w drodze', - 'tranzyt', - 'kurier', - 'wyjechał', - 'wyjechala', - ], - self::CONFIRMED => [ - 'dispatched', - 'nadana', - 'nadano', - ], - self::CREATED => [ - 'prepared', - 'created', - 'przygotowan', - 'utworzon', - ], - self::PROBLEM => [ - 'damaged', - 'problem', - 'lost', - 'uszkodzon', - 'zagubion', - ], - ]; - - private const PROVIDER_TRACKING_URLS = [ - 'inpost' => self::TRACKING_INPOST_URL, - 'allegro_wza' => self::TRACKING_ALLEGRO_URL, - 'polkurier' => 'https://polkurier.pl/sledz-paczke/', - ]; - - private const CARRIER_TRACKING_URLS = [ - [['dpd'], 'https://tracktrace.dpd.com.pl/parcelDetails?p1='], - [['dhl'], 'https://www.dhl.com/pl-pl/home/sledzenie-przesylki.html?tracking-id='], - [['inpost', 'paczkomat'], self::TRACKING_INPOST_URL], - [['orlen', 'ruch'], 'https://www.orlenpaczka.pl/sledz-paczke/?numer='], - [['poczta', 'pocztex'], 'https://emonitoring.poczta-polska.pl/?numer='], - [['ups'], 'https://www.ups.com/track?tracknum='], - [['fedex'], 'https://www.fedex.com/fedextrack/?trknbr='], - [['gls'], 'https://gls-group.com/PL/pl/sledzenie-paczek?match='], - [['allegro'], self::TRACKING_ALLEGRO_URL], - ]; + // --- Mapowanie statusow dostawcow (delegacja do DeliveryStatusProviderMap) --- /** * @return array */ public static function getDefaultMappings(string $provider): array { - $map = self::PROVIDER_MAPS[$provider] ?? []; - $descriptions = self::PROVIDER_DESCRIPTIONS[$provider] ?? []; - - $result = []; - foreach ($map as $rawStatus => $normalized) { - $result[(string) $rawStatus] = [ - 'normalized' => $normalized, - 'description' => (string) ($descriptions[$rawStatus] ?? (string) $rawStatus), - ]; - } - - return $result; + return DeliveryStatusProviderMap::getDefaultMappings($provider); } /** @@ -457,12 +78,7 @@ final class DeliveryStatus */ public static function normalizeWithOverrides(string $provider, string $rawStatus, array $overrides): string { - $key = $provider . ':' . $rawStatus; - if (isset($overrides[$key]) && $overrides[$key]['normalized_status'] !== '') { - return $overrides[$key]['normalized_status']; - } - - return self::normalize($provider, $rawStatus); + return DeliveryStatusProviderMap::normalizeWithOverrides($provider, $rawStatus, $overrides); } /** @@ -470,24 +86,21 @@ final class DeliveryStatus */ public static function descriptionWithOverrides(string $provider, string $rawStatus, array $overrides): string { - $key = $provider . ':' . $rawStatus; - if (isset($overrides[$key]) && $overrides[$key]['description'] !== '') { - return $overrides[$key]['description']; - } - - return self::description($provider, $rawStatus); + return DeliveryStatusProviderMap::descriptionWithOverrides($provider, $rawStatus, $overrides); } public static function normalize(string $provider, string $rawStatus): string { - return self::PROVIDER_MAPS[$provider][$rawStatus] ?? self::UNKNOWN; + return DeliveryStatusProviderMap::normalize($provider, $rawStatus); } public static function description(string $provider, string $rawStatus): string { - return self::PROVIDER_DESCRIPTIONS[$provider][$rawStatus] ?? $rawStatus; + return DeliveryStatusProviderMap::description($provider, $rawStatus); } + // --- Most do bazy danych (slownik statusow konfigurowalny w panelu) --- + public static function setRepository(DeliveryStatusRepository $repo): void { self::$repository = $repo; @@ -536,122 +149,22 @@ final class DeliveryStatus return in_array($status, self::TERMINAL_STATUSES, true); } + // --- Heurystyki opisow Allegro (delegacja do AllegroDescriptionGuesser) --- + public static function slugifyAllegroDescription(string $description): string { - $text = trim($description); - if ($text === '') { - return 'unknown'; - } - - // Usuń typowe prefiksy - $text = preg_replace('/^Przesy[łl]ka zosta[łl]a\s+/ui', '', $text); - $text = preg_replace('/^Kurier\s+/ui', '', $text); - $text = preg_replace('/^Paczka zosta[łl]a\s+/ui', '', $text); - $text = preg_replace('/^Parcel has been\s+/i', '', $text); - $text = preg_replace('/^Parcel is\s+/i', '', $text); - $text = preg_replace('/^Courier has\s+/i', '', $text); - - // Polskie znaki na ASCII - $polish = ['ą','ć','ę','ł','ń','ó','ś','ź','ż','Ą','Ć','Ę','Ł','Ń','Ó','Ś','Ź','Ż']; - $ascii = ['a','c','e','l','n','o','s','z','z','A','C','E','L','N','O','S','Z','Z']; - $text = str_replace($polish, $ascii, $text); - - // Lowercase, zamień nie-alfanumeryczne na podkreślenia - $text = strtolower($text); - $text = preg_replace('/[^a-z0-9]+/', '_', $text); - $text = trim($text, '_'); - - return $text !== '' ? $text : 'unknown'; + return AllegroDescriptionGuesser::slugifyAllegroDescription($description); } - /** - * Keyword-based fallback for unknown Allegro edge descriptions. - * Used when slugified description is not in ALLEGRO_EDGE_MAP. - */ public static function guessStatusFromDescription(string $description): string { - $lower = mb_strtolower($description, 'UTF-8'); - - foreach (self::DESCRIPTION_STATUS_PATTERNS as $status => $patterns) { - if (self::containsAny($lower, $patterns)) { - return $status; - } - } - - return str_contains($lower, "odbi\u{00F3}r") && !str_contains($lower, 'w drodze') - ? self::READY_FOR_PICKUP - : self::UNKNOWN; + return AllegroDescriptionGuesser::guessStatusFromDescription($description); } + // --- Budowanie URL-i sledzenia (delegacja do DeliveryTrackingUrlBuilder) --- + public static function trackingUrl(string $provider, string $trackingNumber, string $carrierId = ''): ?string { - $number = trim($trackingNumber); - if ($number === '') { - return null; - } - - $encoded = rawurlencode($number); - $providerUrl = self::providerTrackingUrl($provider, $encoded); - if ($provider === 'inpost') { - return $providerUrl; - } - - return self::carrierTrackingUrl($encoded, $carrierId) ?? $providerUrl ?? self::fallbackTrackingUrl($encoded); - } - - private static function carrierTrackingUrl(string $encoded, string $carrierId): ?string - { - $carrier = strtolower(trim($carrierId)); - return $carrier === '' ? null : self::matchCarrierByName($encoded, $carrier); - } - - private static function providerTrackingUrl(string $provider, string $encoded): ?string - { - $baseUrl = self::PROVIDER_TRACKING_URLS[$provider] ?? null; - return $baseUrl === null ? null : $baseUrl . $encoded; - } - - private static function fallbackTrackingUrl(string $encoded): string - { - return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki'; - } - - private static function matchCarrierByName(string $encoded, string $carrier): ?string - { - foreach (self::CARRIER_TRACKING_URLS as [$patterns, $baseUrl]) { - if (self::carrierMatches($carrier, $patterns)) { - return $baseUrl . $encoded; - } - } - - return null; - } - - /** - * @param array $needles - */ - private static function containsAny(string $haystack, array $needles): bool - { - foreach ($needles as $needle) { - if (str_contains($haystack, $needle)) { - return true; - } - } - - return false; - } - - /** - * @param array $patterns - */ - private static function carrierMatches(string $carrier, array $patterns): bool - { - foreach ($patterns as $pattern) { - if ($carrier === $pattern || str_contains($carrier, $pattern)) { - return true; - } - } - - return false; + return DeliveryTrackingUrlBuilder::trackingUrl($provider, $trackingNumber, $carrierId); } } diff --git a/src/Modules/Shipments/DeliveryStatusProviderMap.php b/src/Modules/Shipments/DeliveryStatusProviderMap.php new file mode 100644 index 0000000..7348084 --- /dev/null +++ b/src/Modules/Shipments/DeliveryStatusProviderMap.php @@ -0,0 +1,353 @@ + DeliveryStatus::CREATED, + 'offers_prepared' => DeliveryStatus::CREATED, + 'offer_selected' => DeliveryStatus::CREATED, + 'confirmed' => DeliveryStatus::CONFIRMED, + 'dispatched' => DeliveryStatus::CONFIRMED, + 'collected' => DeliveryStatus::PICKED_UP, + 'taken_by_courier' => DeliveryStatus::PICKED_UP, + 'adopted_at_source_branch' => DeliveryStatus::IN_TRANSIT, + 'adopted_at_sorting_center' => DeliveryStatus::IN_TRANSIT, + 'sent_from_sorting_center' => DeliveryStatus::IN_TRANSIT, + 'adopted_at_target_sorting_center' => DeliveryStatus::IN_TRANSIT, + 'sent_from_target_sorting_center' => DeliveryStatus::IN_TRANSIT, + 'adopted_at_target_branch' => DeliveryStatus::IN_TRANSIT, + 'out_for_delivery' => DeliveryStatus::OUT_FOR_DELIVERY, + 'ready_to_pickup' => DeliveryStatus::READY_FOR_PICKUP, + 'ready_to_pickup_from_branch' => DeliveryStatus::READY_FOR_PICKUP, + 'ready_to_pickup_from_pok' => DeliveryStatus::READY_FOR_PICKUP, + 'stack_in_box_machine' => DeliveryStatus::READY_FOR_PICKUP, + 'stack_in_customer_service_point' => DeliveryStatus::READY_FOR_PICKUP, + 'delivered' => DeliveryStatus::DELIVERED, + 'claimed' => DeliveryStatus::DELIVERED, + 'returned_to_sender' => DeliveryStatus::RETURNED, + 'undelivered' => DeliveryStatus::RETURNED, + 'undelivered_wrong_address' => DeliveryStatus::RETURNED, + 'undelivered_incomplete_address' => DeliveryStatus::RETURNED, + 'undelivered_unknown_recipient' => DeliveryStatus::RETURNED, + 'undelivered_cod_cash_receiver' => DeliveryStatus::RETURNED, + 'cancelled' => DeliveryStatus::CANCELLED, + 'expired' => DeliveryStatus::CANCELLED, + 'avizo' => DeliveryStatus::PROBLEM, + 'pickup_time_expired' => DeliveryStatus::PROBLEM, + 'stack_parcel_pickup_time_expired' => DeliveryStatus::PROBLEM, + 'missing' => DeliveryStatus::PROBLEM, + 'delay_in_delivery' => DeliveryStatus::PROBLEM, + 'oversized' => DeliveryStatus::PROBLEM, + 'pickup_reminder_sent' => DeliveryStatus::READY_FOR_PICKUP, + 'pickup_reminder_sent_address' => DeliveryStatus::READY_FOR_PICKUP, + 'readdressed' => DeliveryStatus::IN_TRANSIT, + 'redirect_to_box' => DeliveryStatus::IN_TRANSIT, + ]; + + private const INPOST_DESCRIPTIONS = [ + 'created' => 'Przesyłka utworzona', + 'offers_prepared' => 'Oferty cenowe przygotowane', + 'offer_selected' => 'Oferta wybrana', + 'confirmed' => 'Przesyłka potwierdzona', + 'dispatched' => self::DESC_DISPATCHED, + 'collected' => 'Odebrana od nadawcy', + 'taken_by_courier' => self::DESC_PICKED_UP_BY_COURIER, + 'adopted_at_source_branch' => 'Przyjęta w oddziale źródłowym', + 'adopted_at_sorting_center' => 'Przyjęta w centrum sortowania', + 'sent_from_sorting_center' => 'Wysłana z centrum sortowania', + 'adopted_at_target_sorting_center' => 'Przyjęta w docelowym centrum sortowania', + 'sent_from_target_sorting_center' => 'Wysłana z docelowego centrum sortowania', + 'adopted_at_target_branch' => 'Przyjęta w oddziale docelowym', + 'out_for_delivery' => 'W drodze do odbiorcy', + 'ready_to_pickup' => 'Gotowa do odbioru w paczkomacie', + 'ready_to_pickup_from_branch' => 'Gotowa do odbioru z oddziału', + 'ready_to_pickup_from_pok' => 'Gotowa do odbioru z POK', + 'stack_in_box_machine' => 'Umieszczona w paczkomacie', + 'stack_in_customer_service_point' => 'Umieszczona w punkcie obsługi', + 'delivered' => self::DESC_DELIVERED, + 'claimed' => 'Odebrana po awizo', + 'returned_to_sender' => self::DESC_RETURNED_TO_SENDER, + 'undelivered' => 'Niedoręczona', + 'undelivered_wrong_address' => 'Niedoręczona — błędny adres', + 'undelivered_incomplete_address' => 'Niedoręczona — niepełny adres', + 'undelivered_unknown_recipient' => 'Niedoręczona — nieznany odbiorca', + 'undelivered_cod_cash_receiver' => 'Niedoręczona — problem z pobraniem', + 'cancelled' => 'Anulowana', + 'expired' => 'Wygasła', + 'avizo' => 'Awizowana', + 'pickup_time_expired' => 'Czas odbioru upłynął', + 'stack_parcel_pickup_time_expired' => 'Czas odbioru ze stack upłynął', + 'missing' => 'Przesyłka zagubiona', + 'delay_in_delivery' => 'Opóźnienie w dostawie', + 'oversized' => 'Przesyłka ponadgabarytowa', + 'pickup_reminder_sent' => 'Wysłano przypomnienie o odbiorze', + 'pickup_reminder_sent_address' => 'Przypomnienie wysłane na adres', + 'readdressed' => 'Przekierowana na inny adres', + 'redirect_to_box' => 'Przekierowana do paczkomatu', + ]; + + private const APACZKA_MAP = [ + '0' => DeliveryStatus::CREATED, + '1' => DeliveryStatus::CONFIRMED, + '2' => DeliveryStatus::PICKED_UP, + '3' => DeliveryStatus::IN_TRANSIT, + '4' => DeliveryStatus::OUT_FOR_DELIVERY, + '5' => DeliveryStatus::DELIVERED, + '6' => DeliveryStatus::RETURNED, + '7' => DeliveryStatus::CANCELLED, + '8' => DeliveryStatus::PROBLEM, + '9' => DeliveryStatus::READY_FOR_PICKUP, + '10' => DeliveryStatus::IN_TRANSIT, + 'NEW' => DeliveryStatus::CREATED, + 'PENDING' => DeliveryStatus::CREATED, + 'CONFIRMED' => DeliveryStatus::CONFIRMED, + 'PICKED_UP' => DeliveryStatus::PICKED_UP, + 'PICKUP' => DeliveryStatus::PICKED_UP, + 'IN_TRANSIT' => DeliveryStatus::IN_TRANSIT, + 'OUT_FOR_DELIVERY' => DeliveryStatus::OUT_FOR_DELIVERY, + 'DELIVERED' => DeliveryStatus::DELIVERED, + 'RETURNED' => DeliveryStatus::RETURNED, + 'RETURNED_TO_SHIPPER' => DeliveryStatus::RETURNED, + 'CANCELLED' => DeliveryStatus::CANCELLED, + 'ERROR' => DeliveryStatus::PROBLEM, + 'WAITING_FOR_PICKUP' => DeliveryStatus::READY_FOR_PICKUP, + 'REDIRECT' => DeliveryStatus::IN_TRANSIT, + ]; + + private const APACZKA_DESCRIPTIONS = [ + '0' => 'Oczekuje na przetworzenie', + '1' => 'Zamówienie potwierdzone', + '2' => self::DESC_PICKED_UP_BY_COURIER, + '3' => 'W transporcie', + '4' => self::DESC_OUT_FOR_DELIVERY, + '5' => self::DESC_DELIVERED, + '6' => self::DESC_RETURNED_TO_SENDER, + '7' => 'Anulowana', + '8' => 'Błąd zamówienia', + '9' => self::DESC_AWAITING_PICKUP . ' w punkcie', + '10' => 'Przekierowana', + 'NEW' => 'Zamówienie utworzone', + 'PENDING' => 'Oczekuje na przetworzenie', + 'CONFIRMED' => 'Zamówienie potwierdzone', + 'PICKED_UP' => self::DESC_PICKED_UP_BY_COURIER, + 'PICKUP' => self::DESC_PICKED_UP_BY_COURIER, + 'IN_TRANSIT' => 'W transporcie', + 'OUT_FOR_DELIVERY' => self::DESC_OUT_FOR_DELIVERY, + 'DELIVERED' => self::DESC_DELIVERED, + 'RETURNED' => self::DESC_RETURNED_TO_SENDER, + 'RETURNED_TO_SHIPPER' => self::DESC_RETURNED_TO_SENDER, + 'CANCELLED' => 'Anulowana', + 'ERROR' => 'Błąd zamówienia', + 'WAITING_FOR_PICKUP' => self::DESC_AWAITING_PICKUP . ' w punkcie', + 'REDIRECT' => 'Przekierowana', + ]; + + private const ALLEGRO_MAP = [ + 'NEW' => DeliveryStatus::CREATED, + 'READY_TO_SHIP' => DeliveryStatus::CONFIRMED, + 'collected_from_sender' => DeliveryStatus::PICKED_UP, + 'IN_TRANSIT' => DeliveryStatus::IN_TRANSIT, + 'DELIVERED' => DeliveryStatus::DELIVERED, + 'CANCELLED' => DeliveryStatus::CANCELLED, + 'ERROR' => DeliveryStatus::PROBLEM, + 'RETURNED' => DeliveryStatus::RETURNED, + ]; + + private const ALLEGRO_DESCRIPTIONS = [ + 'NEW' => 'Przesyłka utworzona', + 'READY_TO_SHIP' => 'Etykieta wygenerowana, oczekuje na nadanie', + 'collected_from_sender' => 'Odebrana od nadawcy', + 'IN_TRANSIT' => 'Odebrana przez przewoźnika', + 'DELIVERED' => self::DESC_DELIVERED, + 'CANCELLED' => 'Anulowana', + 'ERROR' => 'Błąd przetwarzania', + 'RETURNED' => self::DESC_RETURNED_TO_SENDER, + ]; + + private const ALLEGRO_EDGE_MAP = [ + // Realne slugi z edge API (po slugify opisów) + 'przygotowana_przez_nadawce' => DeliveryStatus::CREATED, + 'prepared_by_the_sender' => DeliveryStatus::CREATED, + 'nadana' => DeliveryStatus::CONFIRMED, + 'dispatched' => DeliveryStatus::CONFIRMED, + 'podjeta_z_maszyny_przez_kuriera' => DeliveryStatus::PICKED_UP, + 'podjeta_z_punktu_przez_kuriera' => DeliveryStatus::PICKED_UP, + 'podjeta_z_punktu' => DeliveryStatus::PICKED_UP, + 'odebrana_przez_kuriera' => DeliveryStatus::PICKED_UP, + 'picked_up_from_point_by_courier' => DeliveryStatus::PICKED_UP, + 'picked_up_by_the_courier' => DeliveryStatus::PICKED_UP, + 'przekazal_przesylke_do_magazynu' => DeliveryStatus::IN_TRANSIT, + 'przekazana_do_magazynu' => DeliveryStatus::IN_TRANSIT, + 'transferred_the_parcel_to_the_warehouse' => DeliveryStatus::IN_TRANSIT, + 'accepted_at_the_branch' => DeliveryStatus::IN_TRANSIT, + 'przesylka_wyjechala_w_droge_do_punktu_docelowego' => DeliveryStatus::IN_TRANSIT, + 'w_sortowni' => DeliveryStatus::IN_TRANSIT, + 'wyjechala_w_droge_do_punktu_docelowego' => DeliveryStatus::IN_TRANSIT, + 'wyslana_z_sortowni' => DeliveryStatus::IN_TRANSIT, + 'w_doreczeniu' => DeliveryStatus::OUT_FOR_DELIVERY, + 'wydana_do_doreczenia' => DeliveryStatus::OUT_FOR_DELIVERY, + 'released_for_delivery' => DeliveryStatus::OUT_FOR_DELIVERY, + 'dostarczana' => DeliveryStatus::OUT_FOR_DELIVERY, + 'gotowa_do_odbioru' => DeliveryStatus::READY_FOR_PICKUP, + 'oczekuje_na_odbior' => DeliveryStatus::READY_FOR_PICKUP, + 'przesylka_oczekuje_na_odbior' => DeliveryStatus::READY_FOR_PICKUP, + 'awaiting_pick_up' => DeliveryStatus::READY_FOR_PICKUP, + 'dostarczona' => DeliveryStatus::DELIVERED, + 'doreczona' => DeliveryStatus::DELIVERED, + 'odebrana' => DeliveryStatus::DELIVERED, + 'delivered' => DeliveryStatus::DELIVERED, + 'zwrocona' => DeliveryStatus::RETURNED, + 'zwrocona_do_nadawcy' => DeliveryStatus::RETURNED, + 'returned_to_the_sender' => DeliveryStatus::RETURNED, + 'anulowana' => DeliveryStatus::CANCELLED, + 'cancelled' => DeliveryStatus::CANCELLED, + 'odmowa_przyjecia' => DeliveryStatus::PROBLEM, + 'uszkodzona' => DeliveryStatus::PROBLEM, + 'zagubiona' => DeliveryStatus::PROBLEM, + ]; + + private const ALLEGRO_EDGE_DESCRIPTIONS = [ + 'przygotowana_przez_nadawce' => 'Przesyłka przygotowana przez nadawcę', + 'prepared_by_the_sender' => 'Przesyłka przygotowana przez nadawcę', + 'nadana' => self::DESC_DISPATCHED, + 'dispatched' => self::DESC_DISPATCHED, + 'podjeta_z_maszyny_przez_kuriera' => 'Podjęta z maszyny przez kuriera', + 'podjeta_z_punktu_przez_kuriera' => 'Podjęta z punktu przez kuriera', + 'odebrana_przez_kuriera' => self::DESC_PICKED_UP_BY_COURIER, + 'picked_up_from_point_by_courier' => 'Podjęta z punktu przez kuriera', + 'picked_up_by_the_courier' => self::DESC_PICKED_UP_BY_COURIER, + 'przekazana_do_magazynu' => 'Przekazana do magazynu', + 'transferred_the_parcel_to_the_warehouse' => 'Przekazana do magazynu', + 'accepted_at_the_branch' => 'Przyjęta w oddziale', + 'przesylka_wyjechala_w_droge_do_punktu_docelowego' => 'Wyjechała w drogę do punktu docelowego', + 'w_sortowni' => 'W sortowni', + 'wyjechala_w_droge_do_punktu_docelowego' => 'Wyjechała w drogę do punktu docelowego', + 'wyslana_z_sortowni' => 'Wysłana z sortowni', + 'w_doreczeniu' => self::DESC_OUT_FOR_DELIVERY, + 'wydana_do_doreczenia' => 'Wydana do doręczenia', + 'released_for_delivery' => 'Wydana do doręczenia', + 'dostarczana' => 'Dostarczana', + 'gotowa_do_odbioru' => 'Gotowa do odbioru', + 'oczekuje_na_odbior' => self::DESC_AWAITING_PICKUP, + 'przesylka_oczekuje_na_odbior' => self::DESC_AWAITING_PICKUP, + 'awaiting_pick_up' => self::DESC_AWAITING_PICKUP, + 'dostarczona' => 'Dostarczona', + 'doreczona' => self::DESC_DELIVERED, + 'odebrana' => 'Odebrana', + 'delivered' => 'Dostarczona', + 'zwrocona' => 'Zwrócona', + 'zwrocona_do_nadawcy' => self::DESC_RETURNED_TO_SENDER, + 'returned_to_the_sender' => self::DESC_RETURNED_TO_SENDER, + 'anulowana' => 'Anulowana', + 'cancelled' => 'Anulowana', + 'odmowa_przyjecia' => 'Odmowa przyjęcia', + 'uszkodzona' => 'Uszkodzona', + 'zagubiona' => 'Zagubiona', + ]; + + private const POLKURIER_MAP = [ + // Oficjalne kody ORDER_STATUS z dokumentacji polkurier API v1.11 (marzec 2026) + 'O' => DeliveryStatus::CREATED, + 'P' => DeliveryStatus::CONFIRMED, + 'A' => DeliveryStatus::CANCELLED, + 'WP' => DeliveryStatus::IN_TRANSIT, + 'D' => DeliveryStatus::DELIVERED, + 'Z' => DeliveryStatus::RETURNED, + 'W' => DeliveryStatus::PROBLEM, + ]; + + private const POLKURIER_DESCRIPTIONS = [ + 'O' => 'Oczekuje na płatność', + 'P' => 'Potwierdzone, list wygenerowany', + 'A' => 'Anulowane', + 'WP' => 'W przewozie', + 'D' => 'Dostarczona', + 'Z' => 'Zwrot do nadawcy', + 'W' => 'Wyjątek', + ]; + + private const PROVIDER_MAPS = [ + 'inpost' => self::INPOST_MAP, + 'apaczka' => self::APACZKA_MAP, + 'allegro_wza' => self::ALLEGRO_MAP, + 'allegro_edge' => self::ALLEGRO_EDGE_MAP, + 'polkurier' => self::POLKURIER_MAP, + ]; + + private const PROVIDER_DESCRIPTIONS = [ + 'inpost' => self::INPOST_DESCRIPTIONS, + 'apaczka' => self::APACZKA_DESCRIPTIONS, + 'allegro_wza' => self::ALLEGRO_DESCRIPTIONS, + 'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS, + 'polkurier' => self::POLKURIER_DESCRIPTIONS, + ]; + + /** + * @return array + */ + public static function getDefaultMappings(string $provider): array + { + $map = self::PROVIDER_MAPS[$provider] ?? []; + $descriptions = self::PROVIDER_DESCRIPTIONS[$provider] ?? []; + + $result = []; + foreach ($map as $rawStatus => $normalized) { + $result[(string) $rawStatus] = [ + 'normalized' => $normalized, + 'description' => (string) ($descriptions[$rawStatus] ?? (string) $rawStatus), + ]; + } + + return $result; + } + + /** + * @param array $overrides keyed by "provider:raw_status" + */ + public static function normalizeWithOverrides(string $provider, string $rawStatus, array $overrides): string + { + $key = $provider . ':' . $rawStatus; + if (isset($overrides[$key]) && $overrides[$key]['normalized_status'] !== '') { + return $overrides[$key]['normalized_status']; + } + + return self::normalize($provider, $rawStatus); + } + + /** + * @param array $overrides keyed by "provider:raw_status" + */ + public static function descriptionWithOverrides(string $provider, string $rawStatus, array $overrides): string + { + $key = $provider . ':' . $rawStatus; + if (isset($overrides[$key]) && $overrides[$key]['description'] !== '') { + return $overrides[$key]['description']; + } + + return self::description($provider, $rawStatus); + } + + public static function normalize(string $provider, string $rawStatus): string + { + return self::PROVIDER_MAPS[$provider][$rawStatus] ?? DeliveryStatus::UNKNOWN; + } + + public static function description(string $provider, string $rawStatus): string + { + return self::PROVIDER_DESCRIPTIONS[$provider][$rawStatus] ?? $rawStatus; + } +} diff --git a/src/Modules/Shipments/DeliveryTrackingUrlBuilder.php b/src/Modules/Shipments/DeliveryTrackingUrlBuilder.php new file mode 100644 index 0000000..736ce21 --- /dev/null +++ b/src/Modules/Shipments/DeliveryTrackingUrlBuilder.php @@ -0,0 +1,90 @@ + self::TRACKING_INPOST_URL, + 'allegro_wza' => self::TRACKING_ALLEGRO_URL, + 'polkurier' => 'https://polkurier.pl/sledz-paczke/', + ]; + + private const CARRIER_TRACKING_URLS = [ + [['dpd'], 'https://tracktrace.dpd.com.pl/parcelDetails?p1='], + [['dhl'], 'https://www.dhl.com/pl-pl/home/sledzenie-przesylki.html?tracking-id='], + [['inpost', 'paczkomat'], self::TRACKING_INPOST_URL], + [['orlen', 'ruch'], 'https://www.orlenpaczka.pl/sledz-paczke/?numer='], + [['poczta', 'pocztex'], 'https://emonitoring.poczta-polska.pl/?numer='], + [['ups'], 'https://www.ups.com/track?tracknum='], + [['fedex'], 'https://www.fedex.com/fedextrack/?trknbr='], + [['gls'], 'https://gls-group.com/PL/pl/sledzenie-paczek?match='], + [['allegro'], self::TRACKING_ALLEGRO_URL], + ]; + + public static function trackingUrl(string $provider, string $trackingNumber, string $carrierId = ''): ?string + { + $number = trim($trackingNumber); + if ($number === '') { + return null; + } + + $encoded = rawurlencode($number); + $providerUrl = self::providerTrackingUrl($provider, $encoded); + if ($provider === 'inpost') { + return $providerUrl; + } + + return self::carrierTrackingUrl($encoded, $carrierId) ?? $providerUrl ?? self::fallbackTrackingUrl($encoded); + } + + private static function carrierTrackingUrl(string $encoded, string $carrierId): ?string + { + $carrier = strtolower(trim($carrierId)); + return $carrier === '' ? null : self::matchCarrierByName($encoded, $carrier); + } + + private static function providerTrackingUrl(string $provider, string $encoded): ?string + { + $baseUrl = self::PROVIDER_TRACKING_URLS[$provider] ?? null; + return $baseUrl === null ? null : $baseUrl . $encoded; + } + + private static function fallbackTrackingUrl(string $encoded): string + { + return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki'; + } + + private static function matchCarrierByName(string $encoded, string $carrier): ?string + { + foreach (self::CARRIER_TRACKING_URLS as [$patterns, $baseUrl]) { + if (self::carrierMatches($carrier, $patterns)) { + return $baseUrl . $encoded; + } + } + + return null; + } + + /** + * @param array $patterns + */ + private static function carrierMatches(string $carrier, array $patterns): bool + { + foreach ($patterns as $pattern) { + if ($carrier === $pattern || str_contains($carrier, $pattern)) { + return true; + } + } + + return false; + } +}