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:
@@ -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*
|
||||
|
||||
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
244
.paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md
Normal file
244
.paul/phases/131-erli-tracking-automation-hooks/131-01-PLAN.md
Normal 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>
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'] ?? '')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user