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 <noreply@openai.com>
This commit is contained in:
2026-05-16 15:37:07 +02:00
parent 380146bf07
commit f9792a4a3f
14 changed files with 623 additions and 29 deletions

View File

@@ -13,8 +13,8 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
| Attribute | Value | | Attribute | Value |
|-----------|-------| |-----------|-------|
| Version | 3.8.0-dev | | 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 | | Status | v3.8 Erli Marketplace Integration in progress — Phase 131 shipped (Erli tracking + automation hooks); Phase 132 next |
| Last Updated | 2026-05-16 (Phase 130 closed) | | Last Updated | 2026-05-16 (Phase 131 closed) |
## Requirements ## 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] 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] 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] 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] 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] 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 - [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) ### 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) ### Planned (Next)
- [ ] Erli tracking + automation hooks — Phase 131
- [ ] Erli hardening, observability + docs — Phase 132 - [ ] Erli hardening, observability + docs — Phase 132
- [ ] ZarzÄ…dzanie produktami - [ ] ZarzÄ…dzanie produktami
- [ ] ZarzÄ…dzanie stanami magazynowymi - [ ] 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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* *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*

View File

@@ -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. 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 | | 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) | | 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) | | 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) | | 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 | | 132 | Erli Hardening, Observability + Docs | TBD | Not started |
### Phase 127: Erli Integration Foundation ### Phase 127: Erli Integration Foundation
@@ -44,7 +44,7 @@ Plans: 130-01 (complete)
### Phase 131: Erli Tracking + Automation Hooks ### 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. 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 ### Phase 132: Erli Hardening, Observability + Docs
@@ -561,4 +561,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
--- ---
*Roadmap created: 2026-03-12* *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*

View File

@@ -5,19 +5,19 @@
See: .paul/PROJECT.md (updated 2026-05-16) 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. **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 ## Current Position
Milestone: v3.8 Erli Marketplace Integration 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 Plan: Not started
Status: Ready to plan 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: Progress:
- Milestone v3.8: [#######---] 67% (Phases 127-130 complete; Phase 131 next) - Milestone v3.8: [########--] 83% (Phases 127-131 complete; Phase 132 next)
- Phase 131: [----------] 0% (not planned) - Phase 132: [----------] 0% (not planned)
## Loop Position ## Loop Position
@@ -29,9 +29,9 @@ PLAN -> APPLY -> UNIFY
## Session Continuity ## Session Continuity
Last session: 2026-05-16 00:51 Last session: 2026-05-16 15:36
Stopped at: Phase 130 complete, ready to plan Phase 131 Stopped at: Phase 131 complete, ready to plan Phase 132
Next action: $paul-plan for Phase 131 (Erli Tracking + Automation Hooks) Next action: $paul-plan for Phase 132 (Erli Hardening, Observability + Docs)
Resume file: .paul/ROADMAP.md Resume file: .paul/ROADMAP.md
## Pending parallel work ## Pending parallel work
@@ -39,8 +39,8 @@ Resume file: .paul/ROADMAP.md
## Git State ## Git State
Last phase commit: 13f570e feat(130): erli shipments and labels Last phase commit: feat(131): erli tracking automation hooks (created during UNIFY)
Previous: 7972bb9 feat(129): erli status mapping sync Previous: 13f570e feat(130): erli shipments and labels
Branch: main Branch: main
### Skill Audit (Phase 129) ### Skill Audit (Phase 129)
@@ -55,6 +55,12 @@ Branch: main
|----------|---------|-------| |----------|---------|-------|
| `sonar-scanner` | gap documented | Attempted after APPLY; CLI is not available in PATH. | | `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 ## Pending Actions
- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online). - 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 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 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 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: 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: 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). - 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 ## 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.

View File

@@ -12,6 +12,11 @@
- Rozszerzono mapowania dostaw o `source_service_id` i `source_vendor_code`, zeby oddzielic Erli vendor od lokalnego providera etykiety. - 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. - 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. - 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 ## Zmienione pliki
@@ -52,3 +57,7 @@
- `DOCS/DB_SCHEMA.md` - `DOCS/DB_SCHEMA.md`
- `DOCS/ARCHITECTURE.md` - `DOCS/ARCHITECTURE.md`
- `DOCS/TECH_CHANGELOG.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`

View File

@@ -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
---
<objective>
## 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".
</objective>
<context>
<clarifications>
- **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.
</clarifications>
## 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
</context>
<skills>
## 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
</skills>
<acceptance_criteria>
## 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.
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Retry Erli external parcel sync from tracking-safe points</name>
<files>src/Modules/Cron/ShipmentTrackingHandler.php, src/Modules/Cron/CronHandlerFactory.php, src/Modules/Settings/ErliExternalShipmentService.php, tests/Unit/ErliExternalShipmentServiceTest.php</files>
<action>
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.
</action>
<verify>`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.</verify>
<done>AC-1 and AC-2 satisfied; Erli mirrors Allegro's local tracking + non-critical marketplace sync approach.</done>
</task>
<task type="auto">
<name>Task 2: Prove Erli automation event context stays common</name>
<files>src/Modules/Shipments/ShipmentController.php, src/Modules/Automation/AutomationService.php, tests/Unit/AutomationServiceTest.php</files>
<action>
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.
</action>
<verify>`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.</verify>
<done>AC-3 and AC-4 satisfied without creating marketplace-specific automation forks.</done>
</task>
<task type="auto">
<name>Task 3: Document and verify template/statistics compatibility</name>
<files>src/Modules/Sms/SmsVariableResolver.php, src/Modules/Email/VariableResolver.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
<action>
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.
</action>
<verify>`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`.</verify>
<done>AC-5 and AC-6 satisfied; no unnecessary Erli-specific UI or resolver duplication introduced.</done>
</task>
</tasks>
<boundaries>
## 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.
</boundaries>
<verification>
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.
</verification>
<success_criteria>
- 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.
</success_criteria>
<output>
After completion, create `.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md`.
</output>

View File

@@ -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.

View File

@@ -85,9 +85,9 @@ HTTP Request
### Shipment Flow ### 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()` 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 ### Receipt / Invoice
1. **Generate**`ReceiptController::store()``ReceiptService::generateReceipt()``ReceiptRepository::insert()` + Dompdf PDF 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 ### Automation Rules
1. **Setup**`AutomationController``AutomationRepository::insertRule()` 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 3. **Log**`AutomationExecutionLogRepository` tracks every run
### Cron Jobs ### 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 | | `ErliStatusSyncHandler` | Pull Erli status events via inbox or push manual local status changes to Erli |
| `ShopproStatusSyncHandler` | Push status to shopPRO | | `ShopproStatusSyncHandler` | Push status to shopPRO |
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses | | `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 | | `OrderStatusAgedHandler` | Trigger automation for stuck statuses |
| `AutomationHistoryCleanupHandler` | Purge old automation logs | | `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. 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`. 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. 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. 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. **Deferred** - Carrier tracking automation and broader delivery-status hooks are planned for v3.8 Phase 131. 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 ## Dependency Injection

View File

@@ -1,5 +1,22 @@
# Technical Changelog # 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 ## 2026-05-16 - Phase 130 Plan 01: Erli Shipments + Labels
**Co zrobiono:** **Co zrobiono:**

View File

@@ -538,7 +538,9 @@ final class AutomationService
$context, $context,
[ [
'package_id' => $packageId, 'package_id' => $packageId,
'source' => (string) ($context['source'] ?? ''),
'provider' => (string) ($targetPackage['provider'] ?? ''), 'provider' => (string) ($targetPackage['provider'] ?? ''),
'tracking_number' => (string) ($targetPackage['tracking_number'] ?? ''),
'delivery_status' => $targetStatus, 'delivery_status' => $targetStatus,
'delivery_status_raw' => '', 'delivery_status_raw' => '',
'previous_status' => $previousStatus, 'previous_status' => $previousStatus,

View File

@@ -30,10 +30,12 @@ use App\Modules\Settings\AllegroStatusSyncService;
use App\Modules\Settings\AllegroTokenManager; use App\Modules\Settings\AllegroTokenManager;
use App\Modules\Settings\ApaczkaApiClient; use App\Modules\Settings\ApaczkaApiClient;
use App\Modules\Settings\ApaczkaIntegrationRepository; use App\Modules\Settings\ApaczkaIntegrationRepository;
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
use App\Modules\Settings\CompanySettingsRepository; use App\Modules\Settings\CompanySettingsRepository;
use App\Modules\Settings\EmailMailboxRepository; use App\Modules\Settings\EmailMailboxRepository;
use App\Modules\Settings\EmailTemplateRepository; use App\Modules\Settings\EmailTemplateRepository;
use App\Modules\Settings\ErliApiClient; use App\Modules\Settings\ErliApiClient;
use App\Modules\Settings\ErliExternalShipmentService;
use App\Modules\Settings\ErliIntegrationRepository; use App\Modules\Settings\ErliIntegrationRepository;
use App\Modules\Settings\ErliOrderMapper; use App\Modules\Settings\ErliOrderMapper;
use App\Modules\Settings\ErliOrderSyncStateRepository; use App\Modules\Settings\ErliOrderSyncStateRepository;
@@ -144,6 +146,8 @@ final class CronHandlerFactory
$erliApiClient = new ErliApiClient(); $erliApiClient = new ErliApiClient();
$erliPullStatusMappingRepository = new ErliPullStatusMappingRepository($this->db); $erliPullStatusMappingRepository = new ErliPullStatusMappingRepository($this->db);
$erliStatusMappingRepository = new ErliStatusMappingRepository($this->db); $erliStatusMappingRepository = new ErliStatusMappingRepository($this->db);
$carrierDeliveryMappings = new CarrierDeliveryMethodMappingRepository($this->db);
$shipmentPackageRepository = new ShipmentPackageRepository($this->db);
$erliOrdersSyncService = new ErliOrdersSyncService( $erliOrdersSyncService = new ErliOrdersSyncService(
$erliIntegrationRepository, $erliIntegrationRepository,
$erliSyncStateRepository, $erliSyncStateRepository,
@@ -219,9 +223,17 @@ final class CronHandlerFactory
new DeliveryStatusMappingRepository($this->db) new DeliveryStatusMappingRepository($this->db)
), ),
]), ]),
new ShipmentPackageRepository($this->db), $shipmentPackageRepository,
$automationService, $automationService,
new DeliveryStatusMappingRepository($this->db) new DeliveryStatusMappingRepository($this->db),
new ErliExternalShipmentService(
$erliIntegrationRepository,
$erliApiClient,
$carrierDeliveryMappings,
$shipmentPackageRepository,
$ordersRepository
),
$ordersRepository
), ),
'automation_history_cleanup' => new AutomationHistoryCleanupHandler( 'automation_history_cleanup' => new AutomationHistoryCleanupHandler(
new AutomationExecutionLogRepository($this->db) new AutomationExecutionLogRepository($this->db)

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Modules\Cron; namespace App\Modules\Cron;
use App\Modules\Automation\AutomationService; use App\Modules\Automation\AutomationService;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\ErliExternalShipmentService;
use App\Modules\Shipments\DeliveryStatus; use App\Modules\Shipments\DeliveryStatus;
use App\Modules\Shipments\DeliveryStatusMappingRepository; use App\Modules\Shipments\DeliveryStatusMappingRepository;
use App\Modules\Shipments\ShipmentPackageRepository; use App\Modules\Shipments\ShipmentPackageRepository;
@@ -18,7 +20,9 @@ final class ShipmentTrackingHandler
private readonly ShipmentTrackingRegistry $registry, private readonly ShipmentTrackingRegistry $registry,
private readonly ShipmentPackageRepository $repository, private readonly ShipmentPackageRepository $repository,
private readonly AutomationService $automationService, 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); $result = $service->getDeliveryStatus($package);
if ($result !== null) { if ($result !== null) {
$this->syncErliExternalShipment($packageId);
$previousStatus = trim((string) ($package['delivery_status'] ?? 'unknown')); $previousStatus = trim((string) ($package['delivery_status'] ?? 'unknown'));
if ($previousStatus === '') { if ($previousStatus === '') {
$previousStatus = 'unknown'; $previousStatus = 'unknown';
@@ -99,7 +105,9 @@ final class ShipmentTrackingHandler
if ($orderId > 0) { if ($orderId > 0) {
$this->automationService->trigger('shipment.status_changed', $orderId, [ $this->automationService->trigger('shipment.status_changed', $orderId, [
'package_id' => $packageId, 'package_id' => $packageId,
'source' => $this->resolveOrderSource($orderId),
'provider' => $provider, 'provider' => $provider,
'tracking_number' => (string) ($package['tracking_number'] ?? ''),
'delivery_status' => $newStatus, 'delivery_status' => $newStatus,
'delivery_status_raw' => $newStatusRaw, 'delivery_status_raw' => $newStatusRaw,
'previous_status' => $previousStatus, 'previous_status' => $previousStatus,
@@ -119,4 +127,33 @@ final class ShipmentTrackingHandler
'errors' => $errors, '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'] ?? '')));
}
} }

View File

@@ -494,14 +494,18 @@ final class ShipmentController
} }
$package = $this->packageRepository->findById($packageId); $package = $this->packageRepository->findById($packageId);
$details = $this->ordersRepository->findDetails($orderId);
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
$context = [ $context = [
'package_id' => $packageId, 'package_id' => $packageId,
'source' => strtolower(trim((string) ($order['source'] ?? ''))),
'provider' => $providerCode, 'provider' => $providerCode,
'package_status' => is_array($package) ? (string) ($package['status'] ?? '') : '', 'package_status' => is_array($package) ? (string) ($package['status'] ?? '') : '',
'tracking_number' => is_array($package) ? (string) ($package['tracking_number'] ?? '') : '', 'tracking_number' => is_array($package) ? (string) ($package['tracking_number'] ?? '') : '',
'delivery_status' => is_array($package) 'delivery_status' => is_array($package)
? (string) ($package['delivery_status'] ?? DeliveryStatus::UNKNOWN) ? (string) ($package['delivery_status'] ?? DeliveryStatus::UNKNOWN)
: DeliveryStatus::UNKNOWN, : DeliveryStatus::UNKNOWN,
'delivery_status_raw' => is_array($package) ? (string) ($package['delivery_status_raw'] ?? '') : '',
]; ];
try { try {

View File

@@ -213,4 +213,71 @@ final class AutomationServiceTest extends TestCase
'current_status' => 'w_realizacji', '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',
]);
}
} }

View File

@@ -117,6 +117,106 @@ final class ErliExternalShipmentServiceTest extends TestCase
self::assertTrue($result['skipped']); 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 private function createService(): ErliExternalShipmentService
{ {
return new ErliExternalShipmentService( return new ErliExternalShipmentService(