diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md
index 4b72d7a..7cb7131 100644
--- a/.paul/PROJECT.md
+++ b/.paul/PROJECT.md
@@ -13,8 +13,8 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
| Attribute | Value |
|-----------|-------|
| Version | 3.8.0-dev |
-| Status | v3.8 Erli Marketplace Integration in progress — Phase 131 shipped (Erli tracking + automation hooks); Phase 132 next |
-| Last Updated | 2026-05-16 (Phase 131 closed) |
+| Status | v3.8 Erli Marketplace Integration complete in code — Phases 127-132 shipped; live Erli smoke/migrations remain operator follow-up |
+| Last Updated | 2026-05-16 (Phase 132 closed) |
## Requirements
@@ -131,6 +131,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
- [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] Hardening Erli: spojna diagnostyka importu/ACK w `integration_order_sync_state.last_error`, brak ACK po blednym batchu, testy jednostkowe import/status sync i dokumentacja obserwowalnosci bez nowej migracji — Phase 132
- [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
@@ -143,11 +144,10 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
### Active (In Progress)
-- [ ] v3.8 Erli Marketplace Integration — Phase 132 next: hardening, observability and docs.
+- [ ] Next milestone selection pending after v3.8 Erli closure.
### Planned (Next)
-- [ ] Erli hardening, observability + docs — Phase 132
- [ ] ZarzÄ…dzanie produktami
- [ ] ZarzÄ…dzanie stanami magazynowymi
- [ ] Mobile Orders List / Mobile Order Details / Mobile Settings — pelna wersja mobilna pozostalych ekranow
@@ -258,6 +258,7 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
| Erli settings korzysta z zakladek Integracja/Statusy/Ustawienia | Po dodaniu mapowan strona wymagala parytetu UX z Allegro/shopPRO | 2026-05-16 | Active |
| Erli etykiety uzywaja lokalnych providerow, a Erli dostaje paczke zewnetrzna przez `POST /shipping/external` | Operator nie chce nadawac na umowie Erli; API wspiera zewnetrzne paczki/tracking | 2026-05-16 | Active |
| `carrier_delivery_method_mappings` przechowuje `source_vendor_code`/`source_service_id` dla Erli | Vendor Erli i lokalny provider to osobne kontrakty, nie nalezy ich mieszac w polach Apaczki/InPost | 2026-05-16 | Active |
+| Erli hardening uzywa istniejacych powierzchni obserwowalnosci zamiast nowej tabeli logow | Operator wybral ujednolicenie istniejacych miejsc; `integration_order_sync_state.last_error`, wynik crona i activity log wystarczaja dla Phase 132 | 2026-05-16 | Active |
| polkurier startuje jako jedna globalna konfiguracja (single-instance, mirror Apaczka/HostedSMS/SMSPLANET) z realnym testowym wywolaniem `apimetod=test_auth_api` | Operator ma jedno konto polkurier; fundament musi byc zweryfikowany na zywym API zanim dolozymy `PolkurierShipmentService` | 2026-05-14 | Active |
| polkurier wymaga `login + token` razem w body `authorization` (nie samego tokena) | Zweryfikowane w SDK polkurier-sdk (`Auth.php`/`Request.php`); kolumna `login VARCHAR(190)` w `polkurier_integration_settings` mimo ze PLAN tego nie wymagal — kontrakt API to dyktuje | 2026-05-14 | Active |
| polkurier API: top-level `status` === `'success'` (nie `'ok'`), tresc bledu w polu `response` envelope'a | `ResponseStatus::SUCCESS = 'success'` z `src/Type/ResponseStatus.php` SDK; bledy rzucane przez `ErrorException($response->get('response'))` w `PolkurierWebService.php`. Pattern dla wszystkich przyszlych metod polkurier API (`createShipment`, `getLabel`, `getStatus`, `cancelOrder`, etc.) | 2026-05-14 | Active |
@@ -279,8 +280,8 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
| Metric | Target | Current | Status |
|--------|--------|---------|--------|
-| Liczba zintegrowanych źródeł zamówień | ≥3 | 3 zrodla importu (Allegro, shopPRO, Erli); Erli wymaga manualnego smoke po migracji | In progress |
-| Generowanie etykiet | Działa | InPost + Erli przez lokalne providery po mapowaniu; Erli wymaga manualnego smoke po migracji | In progress |
+| Liczba zintegrowanych źródeł zamówień | ≥3 | 3 zrodla importu (Allegro, shopPRO, Erli); Erli complete in code, live smoke pending operator | Complete in code |
+| Generowanie etykiet | Działa | InPost + Erli przez lokalne providery po mapowaniu; live smoke pending operator | Complete in code |
## Tech Stack
@@ -305,6 +306,6 @@ Quick Reference:
---
*PROJECT.md — Updated when requirements or context change*
-*Last updated: 2026-05-16 after Phase 131 (Erli Tracking + Automation Hooks) closure; v3.8 milestone in progress*
+*Last updated: 2026-05-16 after Phase 132 (Erli Hardening, Observability + Docs) closure; v3.8 milestone complete in code*
diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 9e81964..46c1dc1 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -6,11 +6,11 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
## Current Milestone
-v3.8 Erli Marketplace Integration — In progress
+v3.8 Erli Marketplace Integration — Complete in code
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: 5 of 6 phases complete (83%).
+Progress: 6 of 6 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
@@ -19,7 +19,7 @@ Progress: 5 of 6 phases complete (83%).
| 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 | 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 | 1/1 | Complete (2026-05-16; PHPUnit/Sonar env gaps documented) |
### Phase 127: Erli Integration Foundation
@@ -49,7 +49,7 @@ Plans: 131-01 (complete)
### Phase 132: Erli Hardening, Observability + Docs
Focus: Testy jednostkowe mapperow/klientow, logi integracji i bledow API, retry/idempotencja, manual smoke checklist na zywej konfiguracji oraz aktualizacja `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md`.
-Plans: TBD (defined during $paul-plan)
+Plans: 132-01 (complete)
## Previous Milestone (transition pending)
@@ -561,4 +561,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
---
*Roadmap created: 2026-03-12*
-*Last updated: 2026-05-16 - Phase 131 complete, ready for Phase 132 planning*
+*Last updated: 2026-05-16 - Phase 132 complete; v3.8 complete in code*
diff --git a/.paul/STATE.md b/.paul/STATE.md
index 65cc08d..ff3e154 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -5,42 +5,42 @@
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 132 ready to plan.
+**Current focus:** v3.8 Erli Marketplace Integration complete in code; next milestone selection pending.
## Current Position
Milestone: v3.8 Erli Marketplace Integration
-Phase: 132 of 132 (Erli Hardening, Observability + Docs) - Not started
-Plan: Not started
-Status: Ready to plan
-Last activity: 2026-05-16 15:36 - Phase 131 complete, transitioned to Phase 132
+Phase: 132 of 132 (Erli Hardening, Observability + Docs) - Complete
+Plan: 132-01 unified
+Status: Loop complete; v3.8 complete in code
+Last activity: 2026-05-16 15:51 - Unified .paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md
Progress:
-- Milestone v3.8: [########--] 83% (Phases 127-131 complete; Phase 132 next)
-- Phase 132: [----------] 0% (not planned)
+- Milestone v3.8: [##########] 100% (Phases 127-132 complete in code)
+- Phase 132: [##########] 100% (complete)
## Loop Position
Current loop state:
```
PLAN -> APPLY -> UNIFY
- done done done [Loop complete, ready for next PLAN]
+ done done done [Loop complete - ready for next milestone]
```
## Session Continuity
-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
+Last session: 2026-05-16 15:51
+Stopped at: Phase 132 complete; v3.8 complete in code
+Next action: Choose next milestone or run $paul-complete-milestone for v3.8 archival/release closure
+Resume file: .paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md
## Pending parallel work
- None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1).
## Git State
-Last phase commit: feat(131): erli tracking automation hooks (created during UNIFY)
-Previous: 13f570e feat(130): erli shipments and labels
+Last phase commit: feat(132): erli hardening observability docs
+Previous: feat(131): erli tracking automation hooks
Branch: main
### Skill Audit (Phase 129)
@@ -61,6 +61,12 @@ Branch: main
|----------|---------|-------|
| `sonar-scanner` | gap documented | Attempted after APPLY; CLI is not available in PATH. |
+### Skill Audit (Phase 132)
+
+| 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).
@@ -94,6 +100,8 @@ Branch: main
- 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 132 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, a globalny XAMPP PHPUnit jest niekompatybilny z PHP (`each()` removed), wiec testy `ErliOrdersSyncServiceTest`, `ErliStatusSyncServiceTest` i `ErliOrderMapperTest` nie zostaly uruchomione przez PHPUnit; wykonano `php -l` i `git diff --check`.
+- Phase 132 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony.
- 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).
@@ -115,4 +123,4 @@ Branch: main
## Skill Requirements
-- `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.
+- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128, Phase 129, Phase 130, Phase 131 and Phase 132 gaps documented because CLI was not available in PATH.
diff --git a/.paul/changelog/2026-05-16.md b/.paul/changelog/2026-05-16.md
index d9edb0e..93ca171 100644
--- a/.paul/changelog/2026-05-16.md
+++ b/.paul/changelog/2026-05-16.md
@@ -17,6 +17,11 @@
- 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.
+- [Phase 132, Plan 01] Domknieto hardening Erli bez nowej migracji: import/status sync uzywa istniejacych punktow obserwowalnosci.
+- `ErliOrdersSyncService` zwraca `last_error`, normalizuje bledy per-message do `message_id/type/error` i nie wykonuje ACK po blednym batchu.
+- Blad `POST /inbox/mark-read` jest zapisywany przez istniejacy state failure path z czytelnym komunikatem.
+- Dodano `ErliOrdersSyncServiceTest` oraz rozszerzono `ErliStatusSyncServiceTest` o diagnostyke nieudanego push statusu.
+- Udokumentowano gapy srodowiskowe Phase 132: brak `vendor/bin/phpunit`, globalny XAMPP PHPUnit niekompatybilny z PHP, brak `sonar-scanner` w PATH.
## Zmienione pliki
@@ -61,3 +66,6 @@
- `.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md`
- `src/Modules/Cron/ShipmentTrackingHandler.php`
- `tests/Unit/AutomationServiceTest.php`
+- `.paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md`
+- `.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md`
+- `tests/Unit/ErliOrdersSyncServiceTest.php`
diff --git a/.paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md b/.paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md
new file mode 100644
index 0000000..ae0e0c3
--- /dev/null
+++ b/.paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md
@@ -0,0 +1,191 @@
+---
+phase: 132-erli-hardening-observability-docs
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - src/Modules/Settings/ErliOrdersSyncService.php
+ - src/Modules/Settings/ErliStatusSyncService.php
+ - src/Modules/Settings/ErliApiClient.php
+ - tests/Unit/ErliOrdersSyncServiceTest.php
+ - tests/Unit/ErliStatusSyncServiceTest.php
+ - tests/Unit/ErliOrderMapperTest.php
+ - DOCS/DB_SCHEMA.md
+ - DOCS/ARCHITECTURE.md
+ - DOCS/TECH_CHANGELOG.md
+autonomous: true
+delegation: auto
+---
+
+
+## Goal
+Utwadzic import i synchronizacje statusow Erli bez dodawania nowych tabel: ujednolicic obsluge bledow, wynikow i istniejacych pol obserwowalnosci dla `/inbox`, ACK i push statusow.
+
+## Purpose
+Phase 127-131 dostarczyly pelny przeplyw Erli. Ostatni krok v3.8 ma zmniejszyc ryzyko utraty zdarzen, cichych bledow i nieczytelnych wynikow crona przed zamknieciem milestone.
+
+## Output
+Kod import/status sync Erli bedzie mial spojne wyniki i diagnostyke w istniejacych mechanizmach (`integration_order_sync_state.last_error`, wyniki cron, `order_activity_log` tam gdzie juz istnieje). Powstana lub zostana rozszerzone testy jednostkowe oraz sekcje dokumentacji w istniejacych plikach DOCS.
+
+
+
+
+- **Logi Erli** - Czy dodac nowa trwala tabele logow integracji Erli/API, czy ujednolicic istniejace miejsca?
+ -> Odpowiedz: Istniejaca, do ujednolicenia.
+- **Priorytet** - Czy retry/idempotencja ma dotyczyc najpierw import/status sync, czy wysylek?
+ -> Odpowiedz: To pierwsze.
+- **Smoke docs** - Czy checklist smoke ma byc osobnym dokumentem, czy sekcja w istniejacych DOCS?
+ -> Odpowiedz: sekcja.
+- **Testy** - Czy plan obejmuje tylko testy jednostkowe, czy tez manualny checkpoint live?
+ -> Odpowiedz: Tylko jednostkowe.
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+@.paul/STATE.md
+@.paul/codebase/architecture.md
+@.paul/codebase/db_schema.md
+@.paul/phases/131-erli-tracking-automation-hooks/131-01-SUMMARY.md
+
+## Source Files
+@src/Modules/Settings/ErliApiClient.php
+@src/Modules/Settings/ErliOrdersSyncService.php
+@src/Modules/Settings/ErliStatusSyncService.php
+@src/Modules/Settings/ErliOrderSyncStateRepository.php
+@tests/Unit/ErliOrderMapperTest.php
+@tests/Unit/ErliStatusSyncServiceTest.php
+@tests/Unit/ErliExternalShipmentServiceTest.php
+@DOCS/DB_SCHEMA.md
+@DOCS/ARCHITECTURE.md
+@DOCS/TECH_CHANGELOG.md
+
+
+
+## Required Skills (from SPECIAL-FLOWS.md)
+
+| Skill | Priority | When to Invoke | Loaded? |
+|-------|----------|----------------|---------|
+| sonar-scanner | required | Po APPLY, przed UNIFY | o |
+
+**BLOCKING:** `sonar-scanner` jest wymagany przez `.paul/SPECIAL-FLOWS.md`. Jezeli CLI nadal nie jest dostepny w PATH, udokumentuj gap w SUMMARY/STATE tak jak w Phases 129-131.
+
+
+
+
+## AC-1: Spojne wyniki importu Erli
+```gherkin
+Given cron albo reczny import Erli pobiera wiadomosci z `/inbox`
+When czesc wiadomosci zostanie pominieta, przetworzona albo zakonczy sie bledem
+Then wynik sync zawiera spojne liczniki, maksymalnie 20 diagnostycznych bledow z message_id/type/error i zapisuje zwiezly `last_error` tylko przy realnym bledzie
+```
+
+## AC-2: ACK pozostaje bezpieczny i idempotentny
+```gherkin
+Given batch inbox Erli zawiera poprawne oraz bledne zdarzenia
+When przetwarzanie ma co najmniej jeden blad
+Then `/inbox/mark-read` nie jest wywolywany, cursor sukcesu nie przesuwa sie, a bledy sa widoczne w wyniku i `last_error`
+```
+
+## AC-3: Push statusow Erli nie gubi kursora
+```gherkin
+Given kierunek synchronizacji to `orderpro_to_erli`
+When wysylka statusu dla pozniejszego zamowienia nie powiedzie sie
+Then `last_status_pushed_at` przesuwa sie najwyzej do ostatniej udanej zmiany przed bledem, a wynik zawiera source_order_id, status orderPRO, status Erli i komunikat bledu
+```
+
+## AC-4: Dokumentacja zamyka Phase 132 bez nowego schematu
+```gherkin
+Given Phase 132 nie dodaje nowych tabel ani kolumn
+When plan zostanie wykonany
+Then `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md` opisuja brak migracji, istniejace punkty obserwowalnosci oraz jednostkowy smoke checklist Erli jako sekcje
+```
+
+
+
+
+
+
+ Task 1: Ujednolicic diagnostyke importu i ACK Erli
+ src/Modules/Settings/ErliOrdersSyncService.php, src/Modules/Settings/ErliApiClient.php
+
+ Doprecyzuj wynik `ErliOrdersSyncService::sync()` bez zmiany kontraktu publicznego:
+ - zachowaj obecne liczniki `processed/imported_created/imported_updated/failed/skipped/acknowledged`;
+ - dopilnuj, aby bledy per-message zawsze mialy `message_id`, `type` i `error`;
+ - przy blednym batchu nie wolaj ACK i zapisz czytelny, krotki `last_error`;
+ - przy bledzie ACK zapisz czytelny `last_error` i rzuc istniejacy `IntegrationConfigException`;
+ - nie dodawaj nowej tabeli logow ani nowego runtime env.
+ Jezeli `ErliApiClient` wymaga drobnej normalizacji komunikatow API, trzymaj ja lokalnie w kliencie i zachowaj obecne nazwy metod.
+
+ php -l src/Modules/Settings/ErliOrdersSyncService.php; php -l src/Modules/Settings/ErliApiClient.php
+ AC-1 i AC-2 satisfied: wyniki oraz bledy importu/ACK sa testowalne i nie potwierdzaja blednego batcha.
+
+
+
+ Task 2: Utrwalic unit tests dla import/status sync
+ tests/Unit/ErliOrdersSyncServiceTest.php, tests/Unit/ErliStatusSyncServiceTest.php, tests/Unit/ErliOrderMapperTest.php
+
+ Dodaj testy jednostkowe bez zewnetrznego API:
+ - `ErliOrdersSyncServiceTest`: happy path z ACK po zero-failure batchu, brak ACK przy bledzie pojedynczej wiadomosci, blad ACK zapisuje failed state;
+ - `ErliStatusSyncServiceTest`: zachowaj obecne testy i dodaj przypadek, ze cursor push nie przeskakuje za nieudany status;
+ - `ErliOrderMapperTest`: uzupelnij tylko jezeli potrzebny jest edge case statusu/payloadu odkryty podczas tasku 1.
+ Uzywaj mockow PHPUnit i istniejacych wzorcow z testow Erli. Nie dodawaj live API, env sekretow ani zaleznosci sieciowych.
+
+ php -l tests/Unit/ErliOrdersSyncServiceTest.php; php -l tests/Unit/ErliStatusSyncServiceTest.php; php -l tests/Unit/ErliOrderMapperTest.php; vendor/bin/phpunit tests/Unit/ErliOrdersSyncServiceTest.php tests/Unit/ErliStatusSyncServiceTest.php tests/Unit/ErliOrderMapperTest.php
+ AC-1, AC-2 i AC-3 satisfied: najwazniejsze sciezki hardeningu sa pokryte unit tests albo gap PHPUnit jest jawnie udokumentowany, jesli binary nadal nie istnieje.
+
+
+
+ Task 3: Udokumentowac observability i smoke checklist Erli
+ DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md
+
+ Zaktualizuj dokumentacje techniczna:
+ - `DOCS/DB_SCHEMA.md`: dopisz przy Erli/integration sync state, ze Phase 132 nie dodaje migracji i uzywa istniejacych pol obserwowalnosci;
+ - `DOCS/ARCHITECTURE.md`: dodaj sekcje Phase 132 opisujaca import/status hardening, bezpieczny ACK, push cursor i gdzie operator widzi bledy;
+ - `DOCS/TECH_CHANGELOG.md`: dodaj wpis chronologiczny z "co" i "dlaczego";
+ - dodaj sekcje jednostkowej smoke checklist w istniejacej dokumentacji, bez tworzenia osobnego pliku.
+
+ git diff --check -- DOCS/DB_SCHEMA.md DOCS/ARCHITECTURE.md DOCS/TECH_CHANGELOG.md
+ AC-4 satisfied: dokumentacja opisuje Phase 132 i potwierdza brak zmian schematu.
+
+
+
+
+
+
+## DO NOT CHANGE
+- Nie dodawaj nowych tabel logow ani migracji dla Phase 132.
+- Nie podpinaj `DB_HOST_REMOTE` do runtime aplikacji.
+- Nie zmieniaj kontraktu runtime Erli credentials ani `.env.example`, chyba ze kod naprawiany wprost wymaga nowej zmiennej; obecny plan tego nie zaklada.
+- Nie zmieniaj flow przesylek Erli (`ErliExternalShipmentService`, `ShipmentTrackingHandler`) poza testami pomocniczymi, bo priorytetem tej fazy jest import/status sync.
+- Nie dodawaj native `alert()` / `confirm()` ani inline CSS w widokach.
+
+## SCOPE LIMITS
+- Brak nowych widokow UI.
+- Brak live smoke i brak wywolan do prawdziwego API Erli w testach.
+- Brak nowego systemu retry jobow; hardening ma dzialac w istniejacych cronach i state repository.
+- Brak refaktoru ogolnej architektury cronow poza minimalnymi zmianami wymaganymi przez Erli.
+
+
+
+
+Before declaring plan complete:
+- [ ] `php -l` dla wszystkich zmienionych plikow PHP.
+- [ ] Unit tests Erli uruchomione przez `vendor/bin/phpunit` albo gap jawnie opisany, jezeli binary nadal nie istnieje.
+- [ ] `git diff --check -- . ':!.vscode/ftp-kr.sync.cache.json'`.
+- [ ] `sonar-scanner --version` i `sonar-scanner` uruchomiony albo gap udokumentowany zgodnie z `.paul/SPECIAL-FLOWS.md`.
+- [ ] All acceptance criteria met.
+
+
+
+- Import Erli i status sync maja spojna diagnostyke w istniejacych mechanizmach.
+- Bledny batch inbox nie wykonuje ACK.
+- Push statusow nie przesuwa kursora poza ostatnia udana zmiane.
+- Unit tests pokrywaja najwazniejsze sciezki bez API live.
+- Dokumentacja DOCS zostala zaktualizowana bez nowej migracji.
+
+
+
diff --git a/.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md b/.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md
new file mode 100644
index 0000000..5b6e63d
--- /dev/null
+++ b/.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md
@@ -0,0 +1,93 @@
+---
+phase: 132-erli-hardening-observability-docs
+plan: 01
+subsystem: erli, integrations, cron, testing, docs
+tags: [erli, inbox, ack, status-sync, observability, unit-tests]
+requires:
+ - phase: 128-erli-orders-import
+ provides: Erli inbox import and ACK flow
+ - phase: 129-erli-status-mapping-sync
+ provides: Erli pull/push status synchronization
+ - phase: 131-erli-tracking-automation-hooks
+ provides: Completed Erli integration flow before hardening
+provides:
+ - Consistent Erli import diagnostics
+ - Unit-test coverage for ACK safety and push error payloads
+ - Phase 132 no-migration observability documentation
+affects: [erli_orders_import, erli_status_sync, integration_order_sync_state]
+tech-stack:
+ added: []
+ patterns: [existing-state observability, zero-failure ACK, push cursor guard]
+key-files:
+ created:
+ - tests/Unit/ErliOrdersSyncServiceTest.php
+ modified:
+ - src/Modules/Settings/ErliOrdersSyncService.php
+ - tests/Unit/ErliStatusSyncServiceTest.php
+ - DOCS/DB_SCHEMA.md
+ - DOCS/ARCHITECTURE.md
+ - DOCS/TECH_CHANGELOG.md
+key-decisions:
+ - "No new Erli log table; reuse existing sync state, cron result and activity log surfaces."
+ - "Phase 132 focuses on import/status sync, not shipment retry."
+ - "Verification scope is unit tests only; live API smoke remains outside this plan."
+duration: 10min
+started: 2026-05-16T15:41:00+02:00
+completed: 2026-05-16T15:47:00+02:00
+---
+
+# Phase 132 Plan 01 Summary - Erli Hardening, Observability + Docs
+
+## Status
+
+APPLY complete on 2026-05-16 15:47. Ready for UNIFY.
+
+## Implemented
+
+- Added `last_error` to `ErliOrdersSyncService::sync()` results for enabled and disabled paths.
+- Normalized Erli per-message errors to `message_id`, `type`, and `error`.
+- Changed failed inbox batches to write a concise diagnostic message to `integration_order_sync_state.last_error` and skip ACK.
+- Wrapped ACK failures as `Nie udalo sie potwierdzic inbox Erli: ...`, allowing the existing run-failure state path to persist the issue.
+- Added `tests/Unit/ErliOrdersSyncServiceTest.php` for successful ACK, no ACK on failed message, and failed ACK state behavior.
+- Extended `ErliStatusSyncServiceTest` to assert structured push-error diagnostics.
+- Updated `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md`, and `DOCS/TECH_CHANGELOG.md` with Phase 132 observability, no-migration notes, and unit smoke checklist.
+
+## Acceptance Criteria Results
+
+| AC | Status | Notes |
+|----|--------|-------|
+| AC-1: Spojne wyniki importu Erli | Pass | `sync()` returns `last_error` and normalized per-message diagnostics capped by existing error list behavior. |
+| AC-2: ACK pozostaje bezpieczny i idempotentny | Pass | Failed message batches return before ACK; ACK failures go through the existing failed-run state path. |
+| AC-3: Push statusow Erli nie gubi kursora | Pass | Existing cursor guard preserved and test now asserts structured failed-push diagnostics. |
+| AC-4: Dokumentacja zamyka Phase 132 bez nowego schematu | Pass | DOCS updated; no migration added. |
+
+## Verification
+
+- PASS: `php -l src\Modules\Settings\ErliOrdersSyncService.php`
+- PASS: `php -l src\Modules\Settings\ErliApiClient.php`
+- PASS: `php -l tests\Unit\ErliOrdersSyncServiceTest.php`
+- PASS: `php -l tests\Unit\ErliStatusSyncServiceTest.php`
+- PASS: `php -l tests\Unit\ErliOrderMapperTest.php`
+- PASS: `git diff --check -- . ':!.vscode/ftp-kr.sync.cache.json'` (LF/CRLF warnings only)
+- BLOCKED: `vendor/bin/phpunit` is missing.
+- BLOCKED: global `C:\xampp\php\phpunit.bat` is incompatible with current PHP (`each()` removed).
+- BLOCKED: `sonar-scanner` is not available in PATH.
+
+## Deviations / Gaps
+
+- Unit tests were added and linted, but not executed by PHPUnit because the checkout lacks `vendor/bin/phpunit` and XAMPP's legacy PHPUnit crashes before loading tests.
+- SonarQube scan remains unavailable in PATH, consistent with Phases 129-131.
+- `.vscode/ftp-kr.sync.cache.json` was already dirty and was not touched by this phase.
+
+## Files Changed
+
+- `src/Modules/Settings/ErliOrdersSyncService.php`
+- `tests/Unit/ErliOrdersSyncServiceTest.php`
+- `tests/Unit/ErliStatusSyncServiceTest.php`
+- `DOCS/DB_SCHEMA.md`
+- `DOCS/ARCHITECTURE.md`
+- `DOCS/TECH_CHANGELOG.md`
+- `.paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md`
+- `.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md`
+- `.paul/STATE.md`
+- `.paul/ROADMAP.md`
diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md
index 19590eb..c2ac862 100644
--- a/DOCS/ARCHITECTURE.md
+++ b/DOCS/ARCHITECTURE.md
@@ -131,6 +131,7 @@ Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `Sh
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. 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()`.
+9. **Hardening + observability** - Phase 132 keeps the existing schema. `ErliOrdersSyncService` returns consistent counters and per-message diagnostics, writes concise failures to `integration_order_sync_state.last_error`, and never acknowledges `/inbox/mark-read` when any message in the batch failed. `ErliStatusSyncService` keeps the push cursor at the latest successfully pushed manual status change and returns structured error details for failed pushes.
## Dependency Injection
@@ -211,10 +212,11 @@ tests/
- Wysyla `POST /shipping/external` z `orderId`, `vendor`, `status='sent'`, `trackingNumber`; sukces zapisuje w `shipment_packages.payload_json.erli_external_parcel`, blad trafia do activity log jako niekrytyczny.
### ErliOrdersSyncService / ErliOrderMapper (`src/Modules/Settings/`)
-- `ErliOrdersSyncService::sync()` jest wspolnym entrypointem dla crona i importu recznego. Zwraca liczniki `processed`, `imported_created`, `imported_updated`, `failed`, `skipped`, `acknowledged`.
+- `ErliOrdersSyncService::sync()` jest wspolnym entrypointem dla crona i importu recznego. Zwraca liczniki `processed`, `imported_created`, `imported_updated`, `failed`, `skipped`, `acknowledged`, `acknowledged_count`, `latest_message_id`, `last_error` i maksymalnie 20 wpisow `errors`.
- Obsluguje tylko zdarzenia order inbox (`orderCreated`, `orderStatusChanged`, `orderSellerStatusChanged`); wiadomosci produktowe sa pomijane do przyszlych faz.
- `ErliOrderMapper` mapuje statusy przez `ErliPullStatusMappingRepository` gdy istnieje konfiguracja; w przeciwnym razie zachowuje fallbacki `pending -> nieoplacone`, `purchased -> nowe`, `cancelled/returned -> anulowane`.
- `ErliOrdersSyncService` odkrywa surowe statusy Erli z inboxa i dopisuje je do `erli_order_status_pull_mappings`, zeby operator mogl je zmapowac w UI.
+- Phase 132: blad pojedynczej wiadomosci dopisuje diagnostyke `message_id/type/error`, blokuje ACK calego batcha i zapisuje krotki komunikat w `integration_order_sync_state.last_error`. Blad samego ACK rzuca `IntegrationConfigException` i rowniez trafia do `last_error`.
- Nowe zamowienia z invoice/company/tax id ustawiają `orders.invoice_requested=1`; re-import korzysta z istniejacego delta-only kontraktu `OrderImportRepository`.
- Automatyzacje: `order.imported` dla nowych zamowien i `payment.status_changed` przy tranzycji platnosci na re-imporcie.
@@ -226,6 +228,7 @@ tests/
- Kierunek `orderpro_to_erli` wybiera tylko zamowienia `source='erli'` z reczna zmiana statusu (`order_status_history.change_source='manual'`) po `integration_order_sync_state.last_status_pushed_at`.
- Push korzysta z `erli_order_status_mappings` i `ErliApiClient::updateOrderStatus()`. Brak mapowania powoduje `skipped`; blad API powoduje `failed` i nie przesuwa kursora poza ostatni udany timestamp.
- `erli_status_sync` jest seedowany jako disabled; zapis ustawien Erli aktualizuje interwal, kierunek i wlaczenie harmonogramu zgodnie z aktywnoscia integracji.
+- Unit smoke checklist Phase 132: testy powinny potwierdzic happy path importu z ACK, brak ACK przy bledzie mapowania jednej wiadomosci, zapis bledu ACK w state oraz brak przesuniecia `last_status_pushed_at` poza ostatni udany push.
### IntegrationsHubController
- Dodaje wiersz Erli do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
diff --git a/DOCS/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md
index 6881fa4..9dc3326 100644
--- a/DOCS/DB_SCHEMA.md
+++ b/DOCS/DB_SCHEMA.md
@@ -375,10 +375,12 @@ UNIQUE: `(integration_id, shoppro_status_code)`
| `last_run_at` | DATETIME | YES | |
| `last_success_at` | DATETIME | YES | |
| `last_status_pushed_at` | DATETIME | YES | Erli status push cursor for manual local status changes |
-| `last_error` | VARCHAR(500) | YES | |
+| `last_error` | VARCHAR(500) | YES | Phase 132 stores concise Erli import/ACK diagnostics here; no separate Erli log table |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
+Phase 132 does not add migrations. Erli hardening reuses this table for import run timestamps, the acknowledged inbox message cursor, manual status-push cursor, and last error text.
+
**integration_order_status_sync_state** — Track status sync progress per integration and direction
---
@@ -675,7 +677,7 @@ UNIQUE: `(integration_id)` - one global SMSPLANET settings row.
| `created_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP |
| `updated_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
-UNIQUE: `(integration_id)` - one global Erli settings row. Base integration uses `base_url='https://erli.pl/svc/shop-api'`. Phase 127 only stores configuration and test results; order import, status sync, shipments and tracking are deferred to later v3.8 phases.
+UNIQUE: `(integration_id)` - one global Erli settings row. Base integration uses `base_url='https://erli.pl/svc/shop-api'`. Phase 127 stores configuration and test results; Phases 128-131 add order import, status sync, shipments and tracking. Phase 132 adds hardening/observability in existing sync-state and cron/activity-log fields without schema changes.
---
diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md
index 8e99b0a..874e8c8 100644
--- a/DOCS/TECH_CHANGELOG.md
+++ b/DOCS/TECH_CHANGELOG.md
@@ -1,5 +1,21 @@
# Technical Changelog
+## 2026-05-16 - Phase 132 Plan 01: Erli Hardening, Observability + Docs
+
+**Co zrobiono:**
+- `ErliOrdersSyncService::sync()` zwraca teraz jawne `last_error` oraz normalizuje wpisy `errors` do postaci `message_id/type/error`.
+- Blad pojedynczej wiadomosci inbox tworzy zwiezly komunikat w `integration_order_sync_state.last_error` i blokuje ACK calego batcha.
+- Blad `POST /inbox/mark-read` jest opakowany w czytelny komunikat `Nie udalo sie potwierdzic inbox Erli: ...` i zapisywany jako failure run przez istniejacy state repository.
+- Dodano `tests/Unit/ErliOrdersSyncServiceTest.php` z testami happy path ACK, braku ACK przy bledzie wiadomosci oraz failure state przy bledzie ACK.
+- Wzmocniono `ErliStatusSyncServiceTest` o jawne asercje diagnostyki nieudanego push statusu: source order, status orderPRO, status Erli i komunikat bledu.
+- Uaktualniono dokumentacje Erli o istniejace punkty obserwowalnosci i jednostkowy smoke checklist.
+
+**Dlaczego:**
+- Phase 132 zamyka v3.8 przez utwardzenie importu/status sync bez rozbudowy schematu. Operator ma widziec, dlaczego batch sie nie potwierdzil, a system nie moze zgubic zdarzen Erli przez ACK po czesciowej awarii.
+
+**BREAKING / migracja:**
+- Brak migracji i brak breaking changes. Zmiana uzywa istniejacych pol `integration_order_sync_state.last_error`, wynikow crona oraz istniejacych testow jednostkowych.
+
## 2026-05-16 - Phase 131 Plan 01: Erli Tracking + Automation Hooks
**Co zrobiono:**
diff --git a/src/Modules/Settings/ErliOrdersSyncService.php b/src/Modules/Settings/ErliOrdersSyncService.php
index d676ecd..538f45c 100644
--- a/src/Modules/Settings/ErliOrdersSyncService.php
+++ b/src/Modules/Settings/ErliOrdersSyncService.php
@@ -56,6 +56,7 @@ final class ErliOrdersSyncService
'acknowledged' => false,
'acknowledged_count' => 0,
'latest_message_id' => null,
+ 'last_error' => null,
'errors' => [],
];
@@ -119,10 +120,11 @@ final class ErliOrdersSyncService
}
if ((int) $result['failed'] > 0) {
+ $result['last_error'] = $this->buildBatchFailureMessage($result);
$this->syncStateRepository->markRunFailed(
$integrationId,
new DateTimeImmutable('now'),
- 'Erli import zakonczony z bledami: ' . (string) $result['failed']
+ (string) $result['last_error']
);
return $result;
}
@@ -130,7 +132,9 @@ final class ErliOrdersSyncService
if ($latestMessageId !== null) {
$ack = $this->apiClient->markInboxRead($credentials, $latestMessageId);
if (($ack['ok'] ?? false) !== true) {
- throw new IntegrationConfigException('Nie udalo sie potwierdzic inbox Erli: ' . (string) ($ack['message'] ?? ''));
+ $message = $this->buildAckFailureMessage((string) ($ack['message'] ?? ''));
+ $result['last_error'] = $message;
+ throw new IntegrationConfigException($message);
}
$result['acknowledged'] = true;
$result['acknowledged_count'] = (int) ($ack['acknowledged_count'] ?? 0);
@@ -170,6 +174,7 @@ final class ErliOrdersSyncService
'acknowledged' => false,
'acknowledged_count' => 0,
'latest_message_id' => null,
+ 'last_error' => null,
'errors' => [],
];
}
@@ -306,8 +311,48 @@ final class ErliOrdersSyncService
{
$errors = is_array($result['errors'] ?? null) ? $result['errors'] : [];
if (count($errors) < 20) {
- $errors[] = $error;
+ $errors[] = [
+ 'message_id' => trim((string) ($error['message_id'] ?? '')),
+ 'type' => trim((string) ($error['type'] ?? '')),
+ 'error' => trim((string) ($error['error'] ?? 'Nieznany blad Erli.')),
+ ];
}
$result['errors'] = $errors;
}
+
+ /**
+ * @param array $result
+ */
+ private function buildBatchFailureMessage(array $result): string
+ {
+ $failed = (int) ($result['failed'] ?? 0);
+ $errors = is_array($result['errors'] ?? null) ? $result['errors'] : [];
+ $firstError = is_array($errors[0] ?? null) ? $errors[0] : [];
+ $messageId = trim((string) ($firstError['message_id'] ?? ''));
+ $type = trim((string) ($firstError['type'] ?? ''));
+ $error = trim((string) ($firstError['error'] ?? ''));
+
+ $parts = ['Erli import zakonczony z bledami: ' . $failed];
+ if ($messageId !== '') {
+ $parts[] = 'message_id=' . $messageId;
+ }
+ if ($type !== '') {
+ $parts[] = 'type=' . $type;
+ }
+ if ($error !== '') {
+ $parts[] = 'error=' . $error;
+ }
+
+ return mb_substr(implode('; ', $parts), 0, 500);
+ }
+
+ private function buildAckFailureMessage(string $message): string
+ {
+ $trimmed = trim($message);
+ if ($trimmed === '') {
+ $trimmed = 'Brak szczegolow bledu.';
+ }
+
+ return mb_substr('Nie udalo sie potwierdzic inbox Erli: ' . $trimmed, 0, 500);
+ }
}
diff --git a/tests/Unit/ErliOrdersSyncServiceTest.php b/tests/Unit/ErliOrdersSyncServiceTest.php
new file mode 100644
index 0000000..b1a270e
--- /dev/null
+++ b/tests/Unit/ErliOrdersSyncServiceTest.php
@@ -0,0 +1,229 @@
+integrationRepository = $this->createMock(ErliIntegrationRepository::class);
+ $this->syncStateRepository = $this->createMock(ErliOrderSyncStateRepository::class);
+ $this->apiClient = $this->createMock(ErliApiClient::class);
+ $this->orderImportRepository = $this->createMock(OrderImportRepository::class);
+ $this->ordersRepository = $this->createMock(OrdersRepository::class);
+ $this->mapper = $this->createMock(ErliOrderMapper::class);
+ }
+
+ public function testSuccessfulBatchAcknowledgesLatestMessageAndMarksSuccess(): void
+ {
+ $this->prepareCredentials();
+ $this->apiClient
+ ->method('fetchInbox')
+ ->willReturn([
+ 'ok' => true,
+ 'http_code' => 200,
+ 'items' => [
+ $this->message('m1', '2026-05-16T10:00:00+02:00'),
+ $this->message('m2', '2026-05-16T10:10:00+02:00'),
+ ],
+ 'message' => 'OK',
+ ]);
+ $this->mapper
+ ->method('mapInboxMessage')
+ ->willReturn($this->mappedAggregate());
+ $this->orderImportRepository
+ ->method('upsertOrderAggregate')
+ ->willReturn(['order_id' => 0, 'created' => true, 'payment_transition' => false]);
+ $this->apiClient
+ ->expects($this->once())
+ ->method('markInboxRead')
+ ->with($this->anything(), 'm2')
+ ->willReturn(['ok' => true, 'http_code' => 200, 'acknowledged_count' => 2, 'message' => 'OK']);
+ $this->syncStateRepository
+ ->expects($this->once())
+ ->method('markRunSuccess')
+ ->with(12, $this->isInstanceOf(DateTimeImmutable::class), '2026-05-16 10:10:00', 'm2');
+
+ $result = $this->createService()->sync();
+
+ self::assertSame(2, $result['processed']);
+ self::assertSame(2, $result['imported_created']);
+ self::assertSame(0, $result['failed']);
+ self::assertTrue($result['acknowledged']);
+ self::assertSame(2, $result['acknowledged_count']);
+ self::assertNull($result['last_error']);
+ }
+
+ public function testFailedMessageDoesNotAcknowledgeInboxAndPersistsDiagnosticError(): void
+ {
+ $this->prepareCredentials();
+ $this->apiClient
+ ->method('fetchInbox')
+ ->willReturn([
+ 'ok' => true,
+ 'http_code' => 200,
+ 'items' => [
+ $this->message('m1', '2026-05-16T10:00:00+02:00'),
+ $this->message('m2', '2026-05-16T10:10:00+02:00'),
+ ],
+ 'message' => 'OK',
+ ]);
+ $this->mapper
+ ->method('mapInboxMessage')
+ ->willReturnCallback(function (int $integrationId, array $message): array {
+ if (($message['id'] ?? '') === 'm2') {
+ throw new RuntimeException('broken mapping');
+ }
+
+ return $this->mappedAggregate();
+ });
+ $this->orderImportRepository
+ ->method('upsertOrderAggregate')
+ ->willReturn(['order_id' => 0, 'created' => true, 'payment_transition' => false]);
+ $this->apiClient
+ ->expects($this->never())
+ ->method('markInboxRead');
+ $this->syncStateRepository
+ ->expects($this->once())
+ ->method('markRunFailed')
+ ->with(
+ 12,
+ $this->isInstanceOf(DateTimeImmutable::class),
+ $this->callback(static function (string $message): bool {
+ return str_contains($message, 'Erli import zakonczony z bledami: 1')
+ && str_contains($message, 'message_id=m2')
+ && str_contains($message, 'type=orderCreated')
+ && str_contains($message, 'error=broken mapping');
+ })
+ );
+
+ $result = $this->createService()->sync();
+
+ self::assertSame(1, $result['processed']);
+ self::assertSame(1, $result['failed']);
+ self::assertFalse($result['acknowledged']);
+ self::assertSame('m2', $result['errors'][0]['message_id']);
+ self::assertSame('orderCreated', $result['errors'][0]['type']);
+ self::assertSame('broken mapping', $result['errors'][0]['error']);
+ self::assertIsString($result['last_error']);
+ }
+
+ public function testAckFailureIsPersistedAsRunFailure(): void
+ {
+ $this->prepareCredentials();
+ $this->apiClient
+ ->method('fetchInbox')
+ ->willReturn([
+ 'ok' => true,
+ 'http_code' => 200,
+ 'items' => [$this->message('m1', '2026-05-16T10:00:00+02:00')],
+ 'message' => 'OK',
+ ]);
+ $this->mapper
+ ->method('mapInboxMessage')
+ ->willReturn($this->mappedAggregate());
+ $this->orderImportRepository
+ ->method('upsertOrderAggregate')
+ ->willReturn(['order_id' => 0, 'created' => true, 'payment_transition' => false]);
+ $this->apiClient
+ ->expects($this->once())
+ ->method('markInboxRead')
+ ->with($this->anything(), 'm1')
+ ->willReturn(['ok' => false, 'http_code' => 500, 'acknowledged_count' => 0, 'message' => 'ACK down']);
+ $this->syncStateRepository
+ ->expects($this->once())
+ ->method('markRunFailed')
+ ->with(
+ 12,
+ $this->isInstanceOf(DateTimeImmutable::class),
+ 'Nie udalo sie potwierdzic inbox Erli: ACK down'
+ );
+
+ $this->expectException(IntegrationConfigException::class);
+ $this->expectExceptionMessage('Nie udalo sie potwierdzic inbox Erli: ACK down');
+
+ $this->createService()->sync();
+ }
+
+ private function prepareCredentials(): void
+ {
+ $this->integrationRepository
+ ->method('getCredentials')
+ ->willReturn([
+ 'integration_id' => 12,
+ 'base_url' => 'https://erli.test',
+ 'api_key' => 'token',
+ 'orders_fetch_enabled' => true,
+ 'orders_fetch_start_date' => null,
+ ]);
+ }
+
+ /**
+ * @return array
+ */
+ private function message(string $id, string $created): array
+ {
+ return [
+ 'id' => $id,
+ 'created' => $created,
+ 'type' => 'orderCreated',
+ 'payload' => ['status' => 'purchased'],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function mappedAggregate(): array
+ {
+ return [
+ 'message_id' => 'm1',
+ 'invoice_detected' => false,
+ 'order' => [
+ 'integration_id' => 12,
+ 'source_order_id' => 'erli-123',
+ 'source_updated_at' => '2026-05-16 10:00:00',
+ 'payment_status' => 2,
+ ],
+ 'addresses' => [],
+ 'items' => [],
+ 'payments' => [],
+ 'shipments' => [],
+ 'notes' => [],
+ 'status_history' => [],
+ ];
+ }
+
+ private function createService(): ErliOrdersSyncService
+ {
+ return new ErliOrdersSyncService(
+ $this->integrationRepository,
+ $this->syncStateRepository,
+ $this->apiClient,
+ $this->orderImportRepository,
+ $this->ordersRepository,
+ $this->mapper
+ );
+ }
+}
diff --git a/tests/Unit/ErliStatusSyncServiceTest.php b/tests/Unit/ErliStatusSyncServiceTest.php
index 75dc0ac..0428b13 100644
--- a/tests/Unit/ErliStatusSyncServiceTest.php
+++ b/tests/Unit/ErliStatusSyncServiceTest.php
@@ -120,6 +120,10 @@ final class ErliStatusSyncServiceTest extends TestCase
self::assertSame(1, $result['pushed']);
self::assertSame(1, $result['failed']);
self::assertCount(1, $result['errors']);
+ self::assertSame('E2', $result['errors'][0]['source_order_id']);
+ self::assertSame('w_realizacji', $result['errors'][0]['orderpro_status_code']);
+ self::assertSame('inProgress', $result['errors'][0]['erli_status_code']);
+ self::assertSame('Erli error', $result['errors'][0]['error']);
}
public function testPushDirectionReturnsEarlyWithoutCredentials(): void