From f9792a4a3f03dcfd226e8b0e46b08ae3f4bf0f7e Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 16 May 2026 15:37:07 +0200 Subject: [PATCH] feat(131): erli tracking automation hooks Phase 131 complete: - retry Erli external parcel sync from shipment tracking cron - keep local provider tracking as source of truth, Allegro-style - extend shared shipment automation context and tests Co-Authored-By: OpenAI Codex --- .paul/PROJECT.md | 11 +- .paul/ROADMAP.md | 8 +- .paul/STATE.md | 31 ++- .paul/changelog/2026-05-16.md | 9 + .../131-01-PLAN.md | 244 ++++++++++++++++++ .../131-01-SUMMARY.md | 92 +++++++ DOCS/ARCHITECTURE.md | 12 +- DOCS/TECH_CHANGELOG.md | 17 ++ src/Modules/Automation/AutomationService.php | 2 + src/Modules/Cron/CronHandlerFactory.php | 16 +- src/Modules/Cron/ShipmentTrackingHandler.php | 39 ++- src/Modules/Shipments/ShipmentController.php | 4 + tests/Unit/AutomationServiceTest.php | 67 +++++ .../Unit/ErliExternalShipmentServiceTest.php | 100 +++++++ 14 files changed, 623 insertions(+), 29 deletions(-) create mode 100644 .paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md create mode 100644 .paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index ca3825b..4b72d7a 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,8 +13,8 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| | Version | 3.8.0-dev | -| Status | v3.8 Erli Marketplace Integration in progress — Phase 130 shipped (Erli shipments + labels/external parcel sync); Phase 131 next | -| Last Updated | 2026-05-16 (Phase 130 closed) | +| Status | v3.8 Erli Marketplace Integration in progress — Phase 131 shipped (Erli tracking + automation hooks); Phase 132 next | +| Last Updated | 2026-05-16 (Phase 131 closed) | ## Requirements @@ -130,6 +130,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Import zamowien Erli: pobieranie `/inbox` przez cron i recznie, mapper do orderPRO, delta-only re-import, `invoice_requested` z danych firmowych/NIP, bezpieczny ACK `/inbox/mark-read` po bezblednym batchu — Phase 128 - [x] Mapowanie i synchronizacja statusow Erli: osobne pull/push mappings, discovery statusow z inboxa, reczny-only push `PATCH /orders/{id}/status`, cron `erli_status_sync` i zakladki w ustawieniach Erli — Phase 129 - [x] Przesylki Erli: zakladka mapowania dostaw, etykiety przez lokalne providery InPost/Apaczka i rejestracja paczek zewnetrznych w Erli przez `POST /shipping/external` — Phase 130 +- [x] Tracking i automatyzacje Erli: lokalny provider tracking jak w Allegro, retry niekrytycznej rejestracji paczki zewnetrznej Erli z `shipment_tracking_sync`, wspolny kontekst `shipment.created`/`shipment.status_changed` dla regul e-mail/SMS/statystyk — Phase 131 - [x] Integracja polkurier.pl (fundament): pojedyncza globalna konfiguracja w `/settings/integrations/polkurier`, szyfrowany Token API + login, karta w hubie integracji obok Apaczki i realny test polaczenia przez `apimetod=test_auth_api` zweryfikowany na zywym koncie operatora; `ShipmentProviderRegistry` netkniety — `PolkurierShipmentService/TrackingService` w kolejnych fazach — Phase 127 - [x] polkurier ShipmentService + TrackingService + UI prepare panel: pelen kontrakt API (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers), `PolkurierShipmentService` implementujacy `ShipmentProviderInterface` z normalizacja shipmenttype (lowercase) i splitem ulicy na street/housenumber/flatnumber, `PolkurierTrackingService` mapujacy statusy O/P/A/WP/D/Z/W na znormalizowane, panel "polkurier" w `prepare.php` z dynamiczna lista uslug z `available_carriers`, seed migracja `delivery_status_mappings(provider='polkurier')` z 7 wpisami z PDF v1.11; live test na #114/#115 zakonczony sukcesem po 4 iteracjach (ReferenceError → uppercase shipmenttype → orderno parsing → A4/A6); rozmiar etykiety sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — Phase 128 - [x] Order User Notes module (Phase 129): pelen CRUD notatek autorskich operatora per zamowienie. Reuse `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) + `author_name` (snapshot) + indeks `idx_order_notes_type_order`. `OrderNotesService` z autoryzacja DB-level (`WHERE user_id = :user_id`, rowCount=0 ⇒ 403). Sekcja `#notes` w "Wiadomosci i zalaczniki" w `/orders/{id}` z inline edit form + delete przez `OrderProAlerts.confirm`. Badge `[N]` (indigo neutralny) przy nr zamowienia na `/orders/list` (subquery `user_notes_count` w paginate). Brak admin override (brak systemu rol w aplikacji) — edit/delete tylko dla autora — Phase 129 @@ -142,11 +143,10 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów ### Active (In Progress) -- [ ] v3.8 Erli Marketplace Integration — Phase 131 next: tracking przesylek Erli i hooki automatyzacji. +- [ ] v3.8 Erli Marketplace Integration — Phase 132 next: hardening, observability and docs. ### Planned (Next) -- [ ] Erli tracking + automation hooks — Phase 131 - [ ] Erli hardening, observability + docs — Phase 132 - [ ] ZarzÄ…dzanie produktami - [ ] ZarzÄ…dzanie stanami magazynowymi @@ -212,6 +212,7 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | Historia automatyzacji zapisywana per regula (success/failed) i czyszczona cronem po 30 dniach | Audyt wykonywania regul bez recznego utrzymania danych | 2026-03-28 | Active | | Akcja update_order_status korzysta z OrdersRepository::updateOrderStatus | Spojnosc z historia statusow i activity log bez duplikowania logiki | 2026-03-28 | Active | | Push waybilla do Allegro checkout forms wykonywany tylko dla zamowien source=allegro i jest niekrytyczny dla lokalnego tworzenia paczki | Eliminacja recznego kroku po stronie Allegro bez ryzyka utraty lokalnie utworzonej przesylki przy bledzie API | 2026-03-28 | Active | +| Erli tracking uzywa lokalnych providerow jak Allegro, bez osobnego ErliTrackingService/UI statusow dostawy | Lokalny delivery status pozostaje zrodlem prawdy, a Erli dostaje niekrytyczny retry external parcel/tracking z crona | 2026-05-16 | Active | | HostedSMS startuje jako jedna globalna konfiguracja z realnym testowym SMS | Operator potrzebowal na start tylko ustawien i potwierdzenia dzialania; SimpleAPI nie ma osobnego ping endpointu | 2026-05-12 | Active | | Odbior odpowiedzi SMS z HostedSMS odlozony do osobnej fazy | Dokumentacja przewiduje metody odbioru SMS, ale wymagaja aktywacji interfejsu po stronie DCS/HostedSMS | 2026-05-12 | Deferred | | SMSPLANET startuje jako jedna globalna konfiguracja z realnym testowym SMS | Operator potrzebowal drugiej bramki porownywalnej z HostedSMS, bez automatyzacji SMS w tej fazie | 2026-05-12 | Active | @@ -304,6 +305,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-05-16 after Phase 130 (Erli Shipments + Labels) closure; v3.8 milestone in progress* +*Last updated: 2026-05-16 after Phase 131 (Erli Tracking + Automation Hooks) closure; v3.8 milestone in progress* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 09d4c8f..9e81964 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -10,7 +10,7 @@ v3.8 Erli Marketplace Integration — In progress Pelna integracja z erli.pl wzorowana na istniejacej integracji Allegro: konfiguracja konta/API, pobieranie zamowien, mapowanie i synchronizacja statusow, generowanie etykiet, tracking oraz wlaczenie Erli w istniejace przeplywy automatyzacji, statystyk i obslugi zamowien. -Progress: 4 of 6 phases complete (67%). +Progress: 5 of 6 phases complete (83%). | Phase | Name | Plans | Status | |-------|------|-------|--------| @@ -18,7 +18,7 @@ Progress: 4 of 6 phases complete (67%). | 128 | Erli Orders Import | 1/1 | Complete (2026-05-15; migration/manual Erli import smoke pending operator) | | 129 | Erli Status Mapping + Sync | 1/1 | Complete (2026-05-16; migration/manual Erli status smoke pending operator) | | 130 | Erli Shipments + Labels | 1/1 | Complete (2026-05-16; migration/manual Erli shipping smoke pending operator) | -| 131 | Erli Tracking + Automation Hooks | TBD | Not started | +| 131 | Erli Tracking + Automation Hooks | 1/1 | Complete (2026-05-16; manual Erli tracking/automation smoke pending operator) | | 132 | Erli Hardening, Observability + Docs | TBD | Not started | ### Phase 127: Erli Integration Foundation @@ -44,7 +44,7 @@ Plans: 130-01 (complete) ### Phase 131: Erli Tracking + Automation Hooks Focus: Tracking przesylek Erli, aktualizacja delivery statusow, zdarzenia automatyzacji (`order.imported`, `shipment.created`, `shipment.status_changed`) i zachowanie kompatybilnosci z szablonami e-mail/SMS oraz statystykami. -Plans: TBD (defined during $paul-plan) +Plans: 131-01 (complete) ### Phase 132: Erli Hardening, Observability + Docs @@ -561,4 +561,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-05-16 - Phase 130 complete, ready for Phase 131 planning* +*Last updated: 2026-05-16 - Phase 131 complete, ready for Phase 132 planning* diff --git a/.paul/STATE.md b/.paul/STATE.md index 9b1caaa..65cc08d 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,19 +5,19 @@ See: .paul/PROJECT.md (updated 2026-05-16) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** v3.8 Erli Marketplace Integration - Phase 131 ready to plan. +**Current focus:** v3.8 Erli Marketplace Integration - Phase 132 ready to plan. ## Current Position Milestone: v3.8 Erli Marketplace Integration -Phase: 131 of 132 (Erli Tracking + Automation Hooks) - Not started +Phase: 132 of 132 (Erli Hardening, Observability + Docs) - Not started Plan: Not started Status: Ready to plan -Last activity: 2026-05-16 00:51 - Phase 130 complete, transitioned to Phase 131 +Last activity: 2026-05-16 15:36 - Phase 131 complete, transitioned to Phase 132 Progress: -- Milestone v3.8: [#######---] 67% (Phases 127-130 complete; Phase 131 next) -- Phase 131: [----------] 0% (not planned) +- Milestone v3.8: [########--] 83% (Phases 127-131 complete; Phase 132 next) +- Phase 132: [----------] 0% (not planned) ## Loop Position @@ -29,9 +29,9 @@ PLAN -> APPLY -> UNIFY ## Session Continuity -Last session: 2026-05-16 00:51 -Stopped at: Phase 130 complete, ready to plan Phase 131 -Next action: $paul-plan for Phase 131 (Erli Tracking + Automation Hooks) +Last session: 2026-05-16 15:36 +Stopped at: Phase 131 complete, ready to plan Phase 132 +Next action: $paul-plan for Phase 132 (Erli Hardening, Observability + Docs) Resume file: .paul/ROADMAP.md ## Pending parallel work @@ -39,8 +39,8 @@ Resume file: .paul/ROADMAP.md ## Git State -Last phase commit: 13f570e feat(130): erli shipments and labels -Previous: 7972bb9 feat(129): erli status mapping sync +Last phase commit: feat(131): erli tracking automation hooks (created during UNIFY) +Previous: 13f570e feat(130): erli shipments and labels Branch: main ### Skill Audit (Phase 129) @@ -55,6 +55,12 @@ Branch: main |----------|---------|-------| | `sonar-scanner` | gap documented | Attempted after APPLY; CLI is not available in PATH. | +### Skill Audit (Phase 131) + +| Expected | Invoked | Notes | +|----------|---------|-------| +| `sonar-scanner` | gap documented | Attempted after APPLY; CLI is not available in PATH. | + ## Pending Actions - Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online). @@ -85,6 +91,9 @@ Branch: main - Phase 130 follow-up: uruchom `php bin/migrate.php` (dodaje `carrier_delivery_method_mappings.source_service_id/source_vendor_code`), otworz `/settings/integrations/erli?tab=delivery`, zapisz mapowanie metody Erli na InPost/Apaczka oraz vendor Erli, a potem utworz etykiete dla zamowienia Erli i potwierdz `POST /shipping/external`. - Phase 130 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec test `tests/Unit/ErliExternalShipmentServiceTest.php` nie zostal uruchomiony przez PHPUnit; wykonano `php -l` i `git diff --check`. - Phase 130 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony. +- Phase 131 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec testy `tests/Unit/ErliExternalShipmentServiceTest.php` i `tests/Unit/AutomationServiceTest.php` nie zostaly uruchomione przez PHPUnit; wykonano `php -l` i `git diff --check`. +- Phase 131 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony. +- Phase 131 follow-up: manualny smoke po migracjach/konfiguracji Erli — utworz paczke dla zamowienia Erli z lokalnym providerem, potwierdz `shipment.created`, uruchom `shipment_tracking_sync`, sprawdz `shipment.status_changed` i retry `POST /shipping/external` tylko po pojawieniu sie tracking number. - Phase 127 follow-up: zaplanowac kolejna faze polkurier — `PolkurierShipmentService` (CreateOrder + GetLabel + OrderValuationV2 + AvailableCarriers mapping + UI mapowan metod dostawy + presety przesylek) — fundament + zweryfikowany kontrakt API gotowy. - Phase 127 follow-up: drugi krok — `PolkurierTrackingService` + wpisy w `delivery_status_mappings` (provider='polkurier'). - Phase 127 follow-up: po polkurier shipment service rozwazyc fazy paczkomaty (`InpostParcelMachines` / `PocztexPostOffices` / `Kurier48PostOffices` API juz dostepne w SDK polkuriera). @@ -106,4 +115,4 @@ Branch: main ## Skill Requirements -- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128 and Phase 129 gaps documented because CLI was not available in PATH. +- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128, Phase 129, Phase 130 and Phase 131 gaps documented because CLI was not available in PATH. diff --git a/.paul/changelog/2026-05-16.md b/.paul/changelog/2026-05-16.md index afacc7f..d9edb0e 100644 --- a/.paul/changelog/2026-05-16.md +++ b/.paul/changelog/2026-05-16.md @@ -12,6 +12,11 @@ - Rozszerzono mapowania dostaw o `source_service_id` i `source_vendor_code`, zeby oddzielic Erli vendor od lokalnego providera etykiety. - Dodano niekrytyczna synchronizacje tracking number do Erli po utworzeniu lokalnej paczki. - Udokumentowano gapy srodowiskowe Phase 130: brak `vendor/bin/phpunit`, brak `sonar-scanner` w PATH, smoke testy Erli po migracji do wykonania przez operatora. +- [Phase 131, Plan 01] Domknieto Erli tracking i hooki automatyzacji wedlug wzorca Allegro: lokalny provider tracking jako zrodlo prawdy, bez osobnego `ErliTrackingService` i bez nowego UI statusow dostawy. +- `ShipmentTrackingHandler` probuje ponownie `ErliExternalShipmentService::syncPackage()` po odczycie statusu lokalnego providera, a blad syncu Erli pozostaje niekrytyczny dla lokalnego trackingu. +- Uzupelniono kontekst `shipment.created` i `shipment.status_changed` o wspolne pola `source`, `tracking_number`, `package_id`, `provider` i statusy dostawy. +- Dodano testy dla skip/idempotencji/bledu API w `ErliExternalShipmentServiceTest` oraz test wspolnego kontekstu automatyzacji Erli. +- Udokumentowano gapy srodowiskowe Phase 131: brak `vendor/bin/phpunit`, brak `sonar-scanner` w PATH, manualny smoke Erli tracking/automation pending. ## Zmienione pliki @@ -52,3 +57,7 @@ - `DOCS/DB_SCHEMA.md` - `DOCS/ARCHITECTURE.md` - `DOCS/TECH_CHANGELOG.md` +- `.paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md` +- `.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md` +- `src/Modules/Cron/ShipmentTrackingHandler.php` +- `tests/Unit/AutomationServiceTest.php` diff --git a/.paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md b/.paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md new file mode 100644 index 0000000..1c50005 --- /dev/null +++ b/.paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md @@ -0,0 +1,244 @@ +--- +phase: 131-erli-tracking-automation-hooks +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Cron/ShipmentTrackingHandler.php + - src/Modules/Cron/CronHandlerFactory.php + - src/Modules/Settings/ErliExternalShipmentService.php + - src/Modules/Shipments/ShipmentController.php + - src/Modules/Automation/AutomationService.php + - src/Modules/Sms/SmsVariableResolver.php + - src/Modules/Email/VariableResolver.php + - tests/Unit/ErliExternalShipmentServiceTest.php + - tests/Unit/AutomationServiceTest.php + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +autonomous: true +delegation: auto +--- + + +## Goal +Domknac Erli tracking i hooki automatyzacji w sposob zgodny z dzialajacym wzorcem Allegro: tracking paczek zostaje lokalny przez `shipment_tracking_sync`, zdarzenia `order.imported`, `shipment.created` i `shipment.status_changed` maja byc emitowane dla zamowien Erli, a Erli ma dostawac dane paczki zewnetrznej/tracking niekrytycznie, bez budowania osobnego UI statusow dostawy. + +## Purpose +Operator chce, aby Erli zachowywalo sie jak Allegro, bo Allegro dziala stabilnie: lokalna paczka i status dostawy sa zrodlem prawdy w orderPRO, marketplace dostaje numer przesylki, a automatyzacje e-mail/SMS/statystyki uzywaja wspolnych mechanizmow aplikacji. + +## Output +Plan ma dostarczyc male, testowalne domkniecie: retry/sync Erli external parcel podczas tracking cron gdy tracking pojawi sie pozniej, weryfikacje hookow automatyzacji dla Erli oraz dokumentacje kontraktu "jak Allegro". + + + + +- **Tracking Erli** - Czy dodac osobny `ErliTrackingService`, czy wzorowac sie na Allegro? + -> Odpowiedz: Robimy jak Allegro. Allegro uzywa lokalnego `shipment_tracking_sync` przez provider paczki (`allegro_wza`, InPost/edge), bez osobnego UI trackingu marketplace. +- **Aktualizacja Erli** - Czy statusy paczek wypychac do Erli tak jak Allegro? + -> Odpowiedz: Robimy jak Allegro. Marketplace dostaje tracking/paczke zewnetrzna niekrytycznie; lokalny delivery status i automatyzacja sa po stronie orderPRO. +- **Zakres UI** - Czy dodawac nowe mapowania/statusy w UI? + -> Odpowiedz: Robimy jak Allegro, ale bez nowego panelu dla statusow dostawy. Istniejace zakladki Erli z Phase 129/130 zostaja, a `delivery_statuses` i reguly automatyzacji sa wspolne. + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@.paul/codebase/architecture.md +@.paul/codebase/db_schema.md +@AGENTS.md + +## Prior Work +@.paul/phases/129-erli-status-mapping-sync/129-01-SUMMARY.md +@.paul/phases/130-erli-shipments-labels/130-01-SUMMARY.md +@.paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md + +## Allegro Reference Pattern +@src/Modules/Shipments/AllegroTrackingService.php +@src/Modules/Settings/AllegroStatusSyncService.php +@src/Modules/Settings/AllegroApiClient.php +@src/Modules/Shipments/ShipmentController.php + +Allegro reference decisions: +- Tracking paczki dziala w `shipment_tracking_sync` po providerze zapisanym w `shipment_packages`. +- Push numeru nadania do Allegro jest niekrytyczny i nie blokuje lokalnej etykiety. +- Status zamowienia Allegro ma osobny pull/push sync, ale status dostawy paczki nie dostaje osobnego panelu UI. +- `shipment.status_changed` jest emitowany z lokalnego crona trackingowego, gdy `delivery_status` realnie sie zmieni. + +## Source Files +@src/Modules/Cron/ShipmentTrackingHandler.php +@src/Modules/Cron/CronHandlerFactory.php +@src/Modules/Settings/ErliExternalShipmentService.php +@src/Modules/Settings/ErliOrdersSyncService.php +@src/Modules/Shipments/ShipmentController.php +@src/Modules/Shipments/ShipmentPackageRepository.php +@src/Modules/Automation/AutomationService.php +@src/Modules/Sms/SmsVariableResolver.php +@src/Modules/Email/VariableResolver.php +@tests/Unit/ErliExternalShipmentServiceTest.php +@tests/Unit/AutomationServiceTest.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| `sonar-scanner` | required | Po APPLY, przed UNIFY | ○ | +| /feature-dev | optional | Przed implementacja integracji marketplace/shipping | ○ | +| /code-review | optional | Po implementacji, przed UNIFY | ○ | + +**BLOCKING:** Required `sonar-scanner` must be attempted before UNIFY. If CLI is still unavailable in PATH, document the gap in SUMMARY and STATE like Phase 129/130. + +## Skill Invocation Checklist +- [ ] `sonar-scanner` attempted after APPLY +- [ ] Optional flows considered if implementation risk warrants them + + + + +## AC-1: Erli Uses Local Tracking Like Allegro +```gherkin +Given an Erli order has a local shipment package created through an existing provider +When `shipment_tracking_sync` runs and the provider returns a changed delivery status +Then `shipment_packages.delivery_status` and `delivery_status_raw` are updated by the common tracking flow +And no separate Erli-only tracking service or status UI is required. +``` + +## AC-2: External Parcel Sync Is Retried Non-Critically +```gherkin +Given an Erli shipment package gets a tracking number after the initial create path +When creation status, label download or tracking cron observes the package with a tracking number +Then orderPRO attempts `ErliExternalShipmentService::syncPackage()` +And failures are logged/returned as non-critical so the local label and tracking flow continue. +``` + +## AC-3: Shipment Automation Hooks Fire For Erli +```gherkin +Given an Erli shipment package is created or its local delivery status changes +When `ShipmentController` or `ShipmentTrackingHandler` emits automation events +Then `shipment.created` and `shipment.status_changed` include order id, package id, provider, tracking number and status context usable by existing automation rules. +``` + +## AC-4: Order Import Hook Remains Compatible +```gherkin +Given Erli inbox import creates a new order +When `ErliOrdersSyncService` saves the order successfully +Then it emits `order.imported` with source `erli` and does not emit duplicate import activities for no-op reimports. +``` + +## AC-5: Email/SMS Variables And Statistics Stay Common +```gherkin +Given an Erli order has a latest shipment package +When an e-mail or SMS template resolves `{{przesylka.numer}}` and `{{przesylka.link_sledzenia}}` +Then the existing resolvers return the same provider-aware values as for Allegro/shopPRO orders +And no Erli-specific duplicate resolver or statistics path is introduced. +``` + +## AC-6: Verification Documents The Allegro-Compatible Contract +```gherkin +Given the implementation is complete +When verification runs +Then focused tests/lints and docs prove that Erli follows the Allegro-like local tracking + non-critical marketplace sync contract. +``` + + + + + + + Task 1: Retry Erli external parcel sync from tracking-safe points + src/Modules/Cron/ShipmentTrackingHandler.php, src/Modules/Cron/CronHandlerFactory.php, src/Modules/Settings/ErliExternalShipmentService.php, tests/Unit/ErliExternalShipmentServiceTest.php + + Extend the existing Erli external parcel sync without creating a separate `ErliTrackingService`. + Wire `ErliExternalShipmentService` into tracking-safe execution where needed, most likely `ShipmentTrackingHandler` through an optional nullable dependency built by `CronHandlerFactory`. + During `shipment_tracking_sync`, after a provider returns a status result for a package, attempt `syncPackage($packageId)` only for Erli orders/packages where a tracking number exists and the payload does not already show the same tracking synced. + Keep the call non-critical: catch failures, do not increment tracking API errors just because Erli sync failed, and preserve local `delivery_status` updates. + Keep `ErliExternalShipmentService` responsible for source checks, idempotency and bounded activity logging so `ShipmentTrackingHandler` stays readable. + Add/extend tests for: + - skip when package is not Erli, + - skip when tracking number is empty, + - skip when same tracking was already synced, + - successful payload update, + - API failure returns non-critical result without throwing. + + `C:\xampp\php\php.exe -l src/Modules/Cron/ShipmentTrackingHandler.php`; `C:\xampp\php\php.exe -l src/Modules/Settings/ErliExternalShipmentService.php`; `vendor/bin/phpunit tests/Unit/ErliExternalShipmentServiceTest.php` if available. + AC-1 and AC-2 satisfied; Erli mirrors Allegro's local tracking + non-critical marketplace sync approach. + + + + Task 2: Prove Erli automation event context stays common + src/Modules/Shipments/ShipmentController.php, src/Modules/Automation/AutomationService.php, tests/Unit/AutomationServiceTest.php + + Review the existing `shipment.created`, `shipment.status_changed` and `order.imported` paths for Erli. + Keep behavior shared with Allegro/shopPRO: do not add Erli-only automation event names. + If current context is missing useful fields for Erli rules, add only generic fields that benefit all providers, such as `source`, `tracking_number`, `package_id`, `provider`, `delivery_status`, `previous_status`. + Ensure `update_shipment_status` still emits `shipment.status_changed` only on real local status change and does not create automation loops. + Add/extend focused tests that an Erli-like package/order context can satisfy shipment status conditions and that logs preserve sanitized context. + + `C:\xampp\php\php.exe -l src/Modules/Shipments/ShipmentController.php`; `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationService.php`; `vendor/bin/phpunit tests/Unit/AutomationServiceTest.php` if available. + AC-3 and AC-4 satisfied without creating marketplace-specific automation forks. + + + + Task 3: Document and verify template/statistics compatibility + src/Modules/Sms/SmsVariableResolver.php, src/Modules/Email/VariableResolver.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md + + Inspect the shared shipment variable flow and confirm it uses `ShipmentPackageRepository::findLatestByOrderId()` plus `DeliveryStatus::trackingUrl()` for Erli orders the same way it does for Allegro/shopPRO. + Do not fork e-mail/SMS variable logic unless a concrete failing case is found. + If a small generic fix is needed for Erli provider/carrier URLs, keep it in the shared delivery status/tracking URL layer and document it. + Confirm statistics remain source-based on `orders.source='erli'` and are not affected by delivery-status sync. + Update `DOCS/ARCHITECTURE.md` and `DOCS/TECH_CHANGELOG.md` with the Phase 131 contract: Erli tracking follows local provider tracking; external parcel sync is retryable/non-critical; automation uses shared event names. + + `C:\xampp\php\php.exe -l src/Modules/Sms/SmsVariableResolver.php`; `C:\xampp\php\php.exe -l src/Modules/Email/VariableResolver.php`; manual review of docs diff; `git diff --check`. + AC-5 and AC-6 satisfied; no unnecessary Erli-specific UI or resolver duplication introduced. + + + + + + +## DO NOT CHANGE +- Do not add a separate `ErliTrackingService` unless APPLY proves Erli has a documented parcel-status endpoint that is necessary and local provider tracking cannot cover the case. +- Do not add new native `alert()` / `confirm()` calls. +- Do not put CSS in `resources/views/...`. +- Do not connect runtime code to `DB_HOST_REMOTE`. +- Do not regress Allegro status sync, Allegro tracking or Phase 130 Erli external parcel creation. +- Do not change unrelated `.vscode/ftp-kr.sync.cache.json`. + +## SCOPE LIMITS +- No new Erli delivery-status UI in this plan. +- No new database schema unless a concrete bug requires it; existing `shipment_packages.payload_json` should store sync marker details. +- No Erli product/stock/offer work. +- No change to order status mapping UI from Phase 129 except if a small bug blocks automation hooks. +- No attempt to ship on Erli carrier agreement; local providers remain the label source. + + + + +Before declaring plan complete: +- [ ] `C:\xampp\php\php.exe -l` for every changed PHP/test file. +- [ ] `git diff --check`. +- [ ] `vendor/bin/phpunit tests/Unit/ErliExternalShipmentServiceTest.php` if PHPUnit dependencies are available; otherwise document the environment gap. +- [ ] `vendor/bin/phpunit tests/Unit/AutomationServiceTest.php` if PHPUnit dependencies are available; otherwise document the environment gap. +- [ ] Attempt `sonar-scanner`; if unavailable, document the gap in SUMMARY and STATE. +- [ ] Manual smoke: Erli order with mapped local provider creates package and `shipment.created` is logged/emitted. +- [ ] Manual smoke: `shipment_tracking_sync` updates local delivery status for the provider package and emits `shipment.status_changed` on real status change. +- [ ] Manual smoke/review: Erli external parcel sync is retried only when tracking exists and does not block local tracking. +- [ ] Docs updated for Phase 131 contract. +- [ ] All acceptance criteria met. + + + +- Erli tracking behavior matches Allegro's proven local-provider model. +- Erli external parcel sync is retryable, idempotent for same tracking number and non-critical. +- Automation hooks work for Erli through shared event names and context. +- Email/SMS shipment variables continue to resolve from shared latest-package logic. +- No unnecessary UI/schema/provider fork is introduced. +- Verification and skill audit gaps are documented honestly. + + + +After completion, create `.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md`. + diff --git a/.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md b/.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md new file mode 100644 index 0000000..528a2e8 --- /dev/null +++ b/.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md @@ -0,0 +1,92 @@ +--- +phase: 131-erli-tracking-automation-hooks +plan: 01 +subsystem: erli, shipments, automation, cron +tags: [erli, tracking, automation, external-parcels] +requires: + - phase: 129-erli-status-mapping-sync + provides: Erli order status pull/push and shared order import hooks + - phase: 130-erli-shipments-labels + provides: Erli delivery mapping and external parcel registration service +provides: + - Erli external parcel retry from shipment tracking cron + - Shared shipment automation context for Erli + - Documentation of Allegro-compatible Erli tracking contract +affects: [shipment_tracking_sync, erli-external-parcel-sync, automation-events] +tech-stack: + added: [] + patterns: [local-provider tracking source of truth, non-critical marketplace sync] +key-files: + modified: + - src/Modules/Cron/ShipmentTrackingHandler.php + - src/Modules/Cron/CronHandlerFactory.php + - src/Modules/Shipments/ShipmentController.php + - src/Modules/Automation/AutomationService.php + - tests/Unit/ErliExternalShipmentServiceTest.php + - tests/Unit/AutomationServiceTest.php + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +key-decisions: + - "Erli follows the working Allegro model: local provider tracking remains the source of truth." + - "No separate ErliTrackingService and no Erli-only delivery status UI were added." + - "Erli external parcel sync is retryable and non-critical." +started: 2026-05-16T15:21:00+02:00 +completed: 2026-05-16T15:29:00+02:00 +--- + +# Phase 131-01 Summary: Erli Tracking + Automation Hooks + +Phase 131 closes the Erli tracking/automation gap by reusing the existing local-provider tracking model that already works for Allegro. + +## Acceptance Criteria + +| AC | Result | Notes | +|----|--------|-------| +| AC-1: Erli Uses Local Tracking Like Allegro | Pass | `shipment_tracking_sync` still tracks by local provider; no Erli-only tracker/UI was introduced. | +| AC-2: External Parcel Sync Is Retried Non-Critically | Pass | `ShipmentTrackingHandler` retries `ErliExternalShipmentService::syncPackage()` after provider status reads; failures do not block local tracking. | +| AC-3: Shipment Automation Hooks Fire For Erli | Pass | `shipment.created` and `shipment.status_changed` contexts include shared fields such as source, package id, provider, tracking number and status. | +| AC-4: Order Import Hook Remains Compatible | Pass | Existing `ErliOrdersSyncService` order import hook remains unchanged and compatible. | +| AC-5: Email/SMS Variables And Statistics Stay Common | Pass | Shared SMS/e-mail shipment variable resolver remains the source; no Erli fork was needed. | +| AC-6: Verification Documents The Allegro-Compatible Contract | Pass with env gaps | Lints and diff checks passed; PHPUnit and Sonar CLI are unavailable in this checkout/PATH. | + +## Task Results + +| Task | Result | +|------|--------| +| Task 1: Retry Erli external parcel sync from tracking-safe points | Done | +| Task 2: Prove Erli automation event context stays common | Done | +| Task 3: Document and verify template/statistics compatibility | Done | + +## What Changed + +- `ShipmentTrackingHandler` now accepts optional `ErliExternalShipmentService` and `OrdersRepository`. +- `CronHandlerFactory` wires `ErliExternalShipmentService` into `shipment_tracking_sync`. +- Erli external parcel registration is retried after a provider returns tracking status, but remains non-critical. +- `shipment.created` and `shipment.status_changed` contexts now carry shared Erli-friendly fields without new event names. +- Tests were expanded for Erli external parcel idempotency/failure paths and shared automation context. +- `DOCS/ARCHITECTURE.md` and `DOCS/TECH_CHANGELOG.md` document the Phase 131 contract. + +## Verification + +| Check | Result | +|-------|--------| +| PHP lint: changed cron/settings/shipment/automation PHP files | Passed | +| PHP lint: SMS/e-mail resolvers | Passed | +| PHP lint: changed test files | Passed | +| `git diff --check -- . ':!.vscode/ftp-kr.sync.cache.json'` | Passed, with LF/CRLF warnings only | +| `vendor/bin/phpunit tests/Unit/ErliExternalShipmentServiceTest.php` | Not run: `vendor/bin/phpunit` missing | +| `vendor/bin/phpunit tests/Unit/AutomationServiceTest.php` | Not run: `vendor/bin/phpunit` missing | +| `sonar-scanner --version` | Failed: command unavailable in PATH | + +## Deviations / Gaps + +- PHPUnit binaries are absent from `vendor/bin`, so new/changed tests could only be linted. +- `sonar-scanner` is still unavailable in PATH; this matches previous Phase 129/130 skill gaps. +- Manual live smoke remains pending until the operator runs with live Erli/local carrier data. + +## Next Phase Readiness + +Phase 131 is complete and ready for Phase 132 hardening. Manual smoke after deployment should verify: +- mapped Erli order creates a local package and emits `shipment.created`, +- `shipment_tracking_sync` updates local delivery status and emits `shipment.status_changed`, +- Erli external parcel sync is retried only when tracking exists and does not block local tracking. diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index f71d672..19590eb 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -85,9 +85,9 @@ HTTP Request ### Shipment Flow -Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `ShipmentController` calls `ErliExternalShipmentService::syncPackage()` after a local provider has a tracking number. The service posts `vendor/status/trackingNumber/orderId` to Erli `POST /shipping/external` and stores the response in `shipment_packages.payload_json.erli_external_parcel`; failures are activity-log warnings and do not block local labels. +Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `ShipmentController` calls `ErliExternalShipmentService::syncPackage()` after a local provider has a tracking number. Phase 131 extends the same Allegro-like contract into cron tracking: `ShipmentTrackingHandler` retries the Erli external parcel sync after a provider returns tracking status, but the retry is non-critical and never blocks local `delivery_status` updates. The service posts `vendor/status/trackingNumber/orderId` to Erli `POST /shipping/external` and stores the response in `shipment_packages.payload_json.erli_external_parcel`; failures are activity-log warnings. 1. **Create** — `ShipmentController::create()` → `ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()` → `ShipmentPackageRepository::insert()` -2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()` +2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → optional Erli external parcel retry → `ShipmentPackageRepository::updateDeliveryStatus()` → shared `shipment.status_changed` automation event when normalized status really changes. ### Receipt / Invoice 1. **Generate** — `ReceiptController::store()` → `ReceiptService::generateReceipt()` → `ReceiptRepository::insert()` + Dompdf PDF @@ -102,7 +102,7 @@ Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `Sh ### Automation Rules 1. **Setup** — `AutomationController` → `AutomationRepository::insertRule()` -2. **Trigger** — `AutomationService::executeForOrder()` → evaluates trigger (`order_status_changed`, `order_status_aged`) → runs action (send email, update status) +2. **Trigger** — `AutomationService::executeForOrder()` → evaluates trigger (`order_status_changed`, `order_status_aged`, `shipment.created`, `shipment.status_changed`, `order.imported`) → runs action (send email, update status) 3. **Log** — `AutomationExecutionLogRepository` tracks every run ### Cron Jobs @@ -117,7 +117,7 @@ Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `Sh | `ErliStatusSyncHandler` | Pull Erli status events via inbox or push manual local status changes to Erli | | `ShopproStatusSyncHandler` | Push status to shopPRO | | `ShopproPaymentStatusSyncHandler` | Sync payment statuses | -| `ShipmentTrackingHandler` | Poll carrier tracking APIs | +| `ShipmentTrackingHandler` | Poll carrier tracking APIs, retry Erli external parcel sync and emit shipment delivery-status automation | | `OrderStatusAgedHandler` | Trigger automation for stuck statuses | | `AutomationHistoryCleanupHandler` | Purge old automation logs | @@ -129,8 +129,8 @@ Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `Sh 4. **Order import** - Phase 128 adds `/settings/integrations/erli/import` and cron `erli_orders_import`. Both call `ErliOrdersSyncService`, which fetches unread `/inbox` messages, maps supported order events through `ErliOrderMapper`, persists via `OrderImportRepository::upsertOrderAggregate()`, emits existing automation events, and acknowledges `POST /inbox/mark-read` only after a zero-failure batch. 5. **Status mapping/sync** - Phase 129 adds pull/push status mapping tables, status controls in Erli settings, and cron `erli_status_sync`. Pull reuses inbox import; push sends manual orderPRO status changes to `PATCH /orders/{id}/status`. 6. **Delivery mapping and labels** - Phase 130 adds `ErliDeliveryMappingController` and a Delivery tab. It maps imported Erli delivery method labels to local shipment providers (`inpost`/`apaczka`) and stores Erli `source_vendor_code` for external parcel registration. Label files are still produced by local providers, not by Erli carrier-contract parcels. -7. **External shipment sync** - Phase 130 extends `ErliApiClient` with shipping dictionary calls and `createExternalParcel()` (`POST /shipping/external`). `ErliExternalShipmentService` registers a local package in Erli only after `shipment_packages.tracking_number` exists; failures are activity-log warnings and do not block local labels. -8. **Deferred** - Carrier tracking automation and broader delivery-status hooks are planned for v3.8 Phase 131. +7. **External shipment sync** - Phase 130 extends `ErliApiClient` with shipping dictionary calls and `createExternalParcel()` (`POST /shipping/external`). `ErliExternalShipmentService` registers a local package in Erli only after `shipment_packages.tracking_number` exists; failures are activity-log warnings and do not block local labels. Phase 131 wires the same service into `shipment_tracking_sync` as a retry path when tracking appears after the initial create/label flow. +8. **Tracking + automation hooks** - Phase 131 deliberately follows the working Allegro model: no separate `ErliTrackingService` and no Erli-only delivery-status UI. Local provider tracking remains source of truth, `ShipmentTrackingHandler` emits shared `shipment.status_changed` context (`source`, `package_id`, `provider`, `tracking_number`, status fields), `ShipmentController` emits `shipment.created` with the same generic context, and e-mail/SMS variables continue to use `ShipmentPackageRepository::findLatestByOrderId()` plus `DeliveryStatus::trackingUrl()`. ## Dependency Injection diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index 1139de0..8e99b0a 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,22 @@ # Technical Changelog +## 2026-05-16 - Phase 131 Plan 01: Erli Tracking + Automation Hooks + +**Co zrobiono:** +- `ShipmentTrackingHandler` po udanym odczycie statusu lokalnego providera probuje ponownie zarejestrowac paczke Erli przez `ErliExternalShipmentService::syncPackage()`, jesli tracking pojawil sie dopiero po pierwszym flow tworzenia etykiety. +- Retry synchronizacji Erli jest niekrytyczny: bledy `POST /shipping/external` trafiaja do logiki serwisu/activity log, ale nie blokuja lokalnego `delivery_status`, crona trackingowego ani automatyzacji. +- `CronHandlerFactory` wstrzykuje `ErliExternalShipmentService` do `shipment_tracking_sync` razem ze wspolnym `ShipmentPackageRepository` i `OrdersRepository`. +- Kontekst zdarzen `shipment.created` i `shipment.status_changed` zostal uzupelniony o wspolne pola `source`, `tracking_number`, `package_id`, `provider` oraz statusy dostawy, bez tworzenia eventow specyficznych dla Erli. +- Rozszerzono testy `ErliExternalShipmentServiceTest` o skip dla nie-Erli, pustego trackingu, idempotencje po tym samym tracking number i niekrytyczny blad API; dodano test automatyzacji dla Erli na wspolnym `shipment.status_changed`. +- Potwierdzono, ze e-mail/SMS uzywaja wspolnego resolvera przesylki (`ShipmentPackageRepository::findLatestByOrderId()` + `DeliveryStatus::trackingUrl()`), wiec nie dodano osobnej logiki Erli. + +**Dlaczego:** +- Operator wskazal integracje Allegro jako dzialajacy wzorzec. Erli ma wiec korzystac z lokalnego trackingu providerow i wspolnych automatyzacji, a marketplace dostaje tylko niekrytyczna informacje o zewnetrznej paczce/tracking number. +- Brak osobnego `ErliTrackingService` i brak nowego UI statusow dostawy zmniejsza ryzyko rozjazdu kontraktow oraz utrzymuje jeden model regul automatyzacji dla Allegro/shopPRO/Erli. + +**BREAKING / migracja:** +- Brak migracji i brak breaking changes. Zmiana uzywa istniejacych pol `shipment_packages.payload_json`, `delivery_status`, `delivery_status_raw` oraz istniejacych eventow automatyzacji. + ## 2026-05-16 - Phase 130 Plan 01: Erli Shipments + Labels **Co zrobiono:** diff --git a/src/Modules/Automation/AutomationService.php b/src/Modules/Automation/AutomationService.php index 360d9ef..f9aa923 100644 --- a/src/Modules/Automation/AutomationService.php +++ b/src/Modules/Automation/AutomationService.php @@ -538,7 +538,9 @@ final class AutomationService $context, [ 'package_id' => $packageId, + 'source' => (string) ($context['source'] ?? ''), 'provider' => (string) ($targetPackage['provider'] ?? ''), + 'tracking_number' => (string) ($targetPackage['tracking_number'] ?? ''), 'delivery_status' => $targetStatus, 'delivery_status_raw' => '', 'previous_status' => $previousStatus, diff --git a/src/Modules/Cron/CronHandlerFactory.php b/src/Modules/Cron/CronHandlerFactory.php index 8bf370f..92b9f76 100644 --- a/src/Modules/Cron/CronHandlerFactory.php +++ b/src/Modules/Cron/CronHandlerFactory.php @@ -30,10 +30,12 @@ use App\Modules\Settings\AllegroStatusSyncService; use App\Modules\Settings\AllegroTokenManager; use App\Modules\Settings\ApaczkaApiClient; use App\Modules\Settings\ApaczkaIntegrationRepository; +use App\Modules\Settings\CarrierDeliveryMethodMappingRepository; use App\Modules\Settings\CompanySettingsRepository; use App\Modules\Settings\EmailMailboxRepository; use App\Modules\Settings\EmailTemplateRepository; use App\Modules\Settings\ErliApiClient; +use App\Modules\Settings\ErliExternalShipmentService; use App\Modules\Settings\ErliIntegrationRepository; use App\Modules\Settings\ErliOrderMapper; use App\Modules\Settings\ErliOrderSyncStateRepository; @@ -144,6 +146,8 @@ final class CronHandlerFactory $erliApiClient = new ErliApiClient(); $erliPullStatusMappingRepository = new ErliPullStatusMappingRepository($this->db); $erliStatusMappingRepository = new ErliStatusMappingRepository($this->db); + $carrierDeliveryMappings = new CarrierDeliveryMethodMappingRepository($this->db); + $shipmentPackageRepository = new ShipmentPackageRepository($this->db); $erliOrdersSyncService = new ErliOrdersSyncService( $erliIntegrationRepository, $erliSyncStateRepository, @@ -219,9 +223,17 @@ final class CronHandlerFactory new DeliveryStatusMappingRepository($this->db) ), ]), - new ShipmentPackageRepository($this->db), + $shipmentPackageRepository, $automationService, - new DeliveryStatusMappingRepository($this->db) + new DeliveryStatusMappingRepository($this->db), + new ErliExternalShipmentService( + $erliIntegrationRepository, + $erliApiClient, + $carrierDeliveryMappings, + $shipmentPackageRepository, + $ordersRepository + ), + $ordersRepository ), 'automation_history_cleanup' => new AutomationHistoryCleanupHandler( new AutomationExecutionLogRepository($this->db) diff --git a/src/Modules/Cron/ShipmentTrackingHandler.php b/src/Modules/Cron/ShipmentTrackingHandler.php index d1e53f2..cf94bac 100644 --- a/src/Modules/Cron/ShipmentTrackingHandler.php +++ b/src/Modules/Cron/ShipmentTrackingHandler.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Modules\Cron; use App\Modules\Automation\AutomationService; +use App\Modules\Orders\OrdersRepository; +use App\Modules\Settings\ErliExternalShipmentService; use App\Modules\Shipments\DeliveryStatus; use App\Modules\Shipments\DeliveryStatusMappingRepository; use App\Modules\Shipments\ShipmentPackageRepository; @@ -18,7 +20,9 @@ final class ShipmentTrackingHandler private readonly ShipmentTrackingRegistry $registry, private readonly ShipmentPackageRepository $repository, private readonly AutomationService $automationService, - private readonly ?DeliveryStatusMappingRepository $mappingRepository = null + private readonly ?DeliveryStatusMappingRepository $mappingRepository = null, + private readonly ?ErliExternalShipmentService $erliExternalShipmentService = null, + private readonly ?OrdersRepository $ordersRepository = null ) { } @@ -65,6 +69,8 @@ final class ShipmentTrackingHandler $result = $service->getDeliveryStatus($package); if ($result !== null) { + $this->syncErliExternalShipment($packageId); + $previousStatus = trim((string) ($package['delivery_status'] ?? 'unknown')); if ($previousStatus === '') { $previousStatus = 'unknown'; @@ -99,7 +105,9 @@ final class ShipmentTrackingHandler if ($orderId > 0) { $this->automationService->trigger('shipment.status_changed', $orderId, [ 'package_id' => $packageId, + 'source' => $this->resolveOrderSource($orderId), 'provider' => $provider, + 'tracking_number' => (string) ($package['tracking_number'] ?? ''), 'delivery_status' => $newStatus, 'delivery_status_raw' => $newStatusRaw, 'previous_status' => $previousStatus, @@ -119,4 +127,33 @@ final class ShipmentTrackingHandler 'errors' => $errors, ]; } + + private function syncErliExternalShipment(int $packageId): void + { + if ($this->erliExternalShipmentService === null || $packageId <= 0) { + return; + } + + try { + $this->erliExternalShipmentService->syncPackage($packageId); + } catch (Throwable) { + // Synchronizacja marketplace nie moze blokowac lokalnego trackingu. + } + } + + private function resolveOrderSource(int $orderId): string + { + if ($this->ordersRepository === null || $orderId <= 0) { + return ''; + } + + try { + $details = $this->ordersRepository->findDetails($orderId); + } catch (Throwable) { + return ''; + } + + $order = is_array($details['order'] ?? null) ? $details['order'] : []; + return strtolower(trim((string) ($order['source'] ?? ''))); + } } diff --git a/src/Modules/Shipments/ShipmentController.php b/src/Modules/Shipments/ShipmentController.php index 85e1215..93507cf 100644 --- a/src/Modules/Shipments/ShipmentController.php +++ b/src/Modules/Shipments/ShipmentController.php @@ -494,14 +494,18 @@ final class ShipmentController } $package = $this->packageRepository->findById($packageId); + $details = $this->ordersRepository->findDetails($orderId); + $order = is_array($details['order'] ?? null) ? $details['order'] : []; $context = [ 'package_id' => $packageId, + 'source' => strtolower(trim((string) ($order['source'] ?? ''))), 'provider' => $providerCode, 'package_status' => is_array($package) ? (string) ($package['status'] ?? '') : '', 'tracking_number' => is_array($package) ? (string) ($package['tracking_number'] ?? '') : '', 'delivery_status' => is_array($package) ? (string) ($package['delivery_status'] ?? DeliveryStatus::UNKNOWN) : DeliveryStatus::UNKNOWN, + 'delivery_status_raw' => is_array($package) ? (string) ($package['delivery_status_raw'] ?? '') : '', ]; try { diff --git a/tests/Unit/AutomationServiceTest.php b/tests/Unit/AutomationServiceTest.php index 0da613a..534cee7 100644 --- a/tests/Unit/AutomationServiceTest.php +++ b/tests/Unit/AutomationServiceTest.php @@ -213,4 +213,71 @@ final class AutomationServiceTest extends TestCase 'current_status' => 'w_realizacji', ]); } + + public function testShipmentStatusChangedForErliUsesSharedContext(): void + { + $rule = [ + 'id' => 10, + 'name' => 'Regula Erli', + 'conditions' => [[ + 'id' => 31, + 'condition_type' => 'shipment_status', + 'condition_value' => [ + 'status_keys' => ['delivered'], + ], + ]], + 'actions' => [[ + 'id' => 14, + 'action_type' => 'send_email', + 'action_config' => [ + 'template_id' => 6, + 'recipient' => 'client', + 'send_once_per_order' => 0, + ], + ]], + ]; + + $this->automationRepository + ->expects($this->once()) + ->method('findActiveByEvent') + ->with('shipment.status_changed') + ->willReturn([$rule]); + + $this->ordersRepository + ->expects($this->once()) + ->method('findDetails') + ->with(123) + ->willReturn(['order' => ['id' => 123, 'integration_id' => 1, 'source' => 'erli']]); + + $this->emailService + ->expects($this->once()) + ->method('send') + ->with(123, 6, null, 'Automatyzacja: Regula Erli') + ->willReturn(['success' => true, 'error' => null, 'log_id' => 4]); + + $this->executionLogRepository + ->expects($this->once()) + ->method('create') + ->with($this->callback(static function (array $payload): bool { + $context = is_array($payload['context'] ?? null) ? $payload['context'] : []; + return ($payload['event_type'] ?? '') === 'shipment.status_changed' + && ($context['source'] ?? '') === 'erli' + && ($context['package_id'] ?? 0) === 77 + && ($context['provider'] ?? '') === 'inpost' + && ($context['tracking_number'] ?? '') === '1234567890' + && ($context['delivery_status'] ?? '') === 'delivered' + && ($context['previous_status'] ?? '') === 'in_transit'; + })); + + $this->service->trigger('shipment.status_changed', 123, [ + 'source' => 'erli', + 'package_id' => 77, + 'provider' => 'inpost', + 'tracking_number' => '1234567890', + 'delivery_status' => 'delivered', + 'delivery_status_raw' => 'DELIVERED', + 'previous_status' => 'in_transit', + 'previous_status_raw' => 'IN_TRANSIT', + ]); + } } diff --git a/tests/Unit/ErliExternalShipmentServiceTest.php b/tests/Unit/ErliExternalShipmentServiceTest.php index 3ed95be..745ed06 100644 --- a/tests/Unit/ErliExternalShipmentServiceTest.php +++ b/tests/Unit/ErliExternalShipmentServiceTest.php @@ -117,6 +117,106 @@ final class ErliExternalShipmentServiceTest extends TestCase self::assertTrue($result['skipped']); } + public function testSyncPackageSkipsNonErliOrders(): void + { + $this->packageRepository + ->method('findById') + ->with(55) + ->willReturn([ + 'id' => 55, + 'order_id' => 1001, + 'tracking_number' => '1234567890', + ]); + $this->ordersRepository + ->method('findDetails') + ->with(1001) + ->willReturn(['order' => ['source' => 'allegro']]); + $this->apiClient + ->expects($this->never()) + ->method('createExternalParcel'); + + $result = $this->createService()->syncPackage(55); + + self::assertTrue($result['ok']); + self::assertTrue($result['skipped']); + } + + public function testSyncPackageSkipsAlreadySyncedTrackingNumber(): void + { + $this->packageRepository + ->method('findById') + ->with(55) + ->willReturn([ + 'id' => 55, + 'order_id' => 1001, + 'tracking_number' => '1234567890', + 'payload_json' => '{"erli_external_parcel":{"trackingNumber":"1234567890"}}', + ]); + $this->ordersRepository + ->method('findDetails') + ->with(1001) + ->willReturn(['order' => ['source' => 'erli']]); + $this->apiClient + ->expects($this->never()) + ->method('createExternalParcel'); + + $result = $this->createService()->syncPackage(55); + + self::assertTrue($result['ok']); + self::assertTrue($result['skipped']); + } + + public function testSyncPackageReturnsNonCriticalFailureWhenApiFails(): void + { + $this->packageRepository + ->method('findById') + ->with(55) + ->willReturn([ + 'id' => 55, + 'order_id' => 1001, + 'provider' => 'inpost', + 'tracking_number' => '1234567890', + 'payload_json' => '{}', + ]); + $this->ordersRepository + ->method('findDetails') + ->with(1001) + ->willReturn([ + 'order' => [ + 'source' => 'erli', + 'source_order_id' => '240101x12345', + 'external_carrier_id' => 'Erli Paczkomat', + ], + ]); + $this->deliveryMappings + ->method('findByOrderMethod') + ->with('erli', 0, 'Erli Paczkomat') + ->willReturn(['source_vendor_code' => 'inpost']); + $this->erliRepository + ->method('getCredentials') + ->willReturn([ + 'integration_id' => 12, + 'base_url' => 'https://erli.test', + 'api_key' => 'token', + ]); + $this->apiClient + ->expects($this->once()) + ->method('createExternalParcel') + ->willReturn(['ok' => false, 'http_code' => 500, 'items' => [], 'message' => 'API down']); + $this->packageRepository + ->expects($this->never()) + ->method('update'); + $this->ordersRepository + ->expects($this->once()) + ->method('recordActivity'); + + $result = $this->createService()->syncPackage(55); + + self::assertFalse($result['ok']); + self::assertFalse($result['skipped']); + self::assertSame('API down', $result['message']); + } + private function createService(): ErliExternalShipmentService { return new ErliExternalShipmentService(