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. + + + +After completion, create `.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md`. + 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