feat(129): erli status mapping sync
Phase 129 complete: - Add Erli pull/push status mapping tables, seeds and repositories - Wire Erli status sync cron for inbox pull and manual-only push - Add tabbed Erli settings UI, tests and documentation Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
|-----------|-------|
|
|-----------|-------|
|
||||||
| Version | 3.8.0-dev |
|
| Version | 3.8.0-dev |
|
||||||
| Status | v3.8 Erli Marketplace Integration in progress — Phase 128 shipped (Erli orders import); Phase 129 next |
|
| Status | v3.8 Erli Marketplace Integration in progress — Phase 129 shipped (Erli status mappings/sync); Phase 130 next |
|
||||||
| Last Updated | 2026-05-15 (Phase 128 closed) |
|
| Last Updated | 2026-05-16 (Phase 129 closed) |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -128,6 +128,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Bugfix detekcji faktury przy imporcie: shopPRO order z `firm_nip` ustawia `invoice_requested=1` (mapper jako jedyne zrodlo heurystyki, sync service propaguje `aggregate['invoice_detected']`); Allegro rozszerzony o `naturalPerson=false`/`address.taxId`/`companyName` (wczesniej tylko `invoice.required`); usunieta legacy kolumna `orders.is_invoice` (Phase 115 dryft) + backfill 7 zamowien — Phase 125
|
- [x] Bugfix detekcji faktury przy imporcie: shopPRO order z `firm_nip` ustawia `invoice_requested=1` (mapper jako jedyne zrodlo heurystyki, sync service propaguje `aggregate['invoice_detected']`); Allegro rozszerzony o `naturalPerson=false`/`address.taxId`/`companyName` (wczesniej tylko `invoice.required`); usunieta legacy kolumna `orders.is_invoice` (Phase 115 dryft) + backfill 7 zamowien — Phase 125
|
||||||
- [x] Fundament integracji Erli: pojedyncza globalna konfiguracja `/settings/integrations/erli`, szyfrowany Bearer API key, realny test `GET /svc/shop-api/inbox`, karta w hubie integracji oraz dokumentacja schematu/architektury — Phase 127
|
- [x] Fundament integracji Erli: pojedyncza globalna konfiguracja `/settings/integrations/erli`, szyfrowany Bearer API key, realny test `GET /svc/shop-api/inbox`, karta w hubie integracji oraz dokumentacja schematu/architektury — Phase 127
|
||||||
- [x] Import zamowien Erli: pobieranie `/inbox` przez cron i recznie, mapper do orderPRO, delta-only re-import, `invoice_requested` z danych firmowych/NIP, bezpieczny ACK `/inbox/mark-read` po bezblednym batchu — Phase 128
|
- [x] Import zamowien Erli: pobieranie `/inbox` przez cron i recznie, mapper do orderPRO, delta-only re-import, `invoice_requested` z danych firmowych/NIP, bezpieczny ACK `/inbox/mark-read` po bezblednym batchu — Phase 128
|
||||||
|
- [x] Mapowanie i synchronizacja statusow Erli: osobne pull/push mappings, discovery statusow z inboxa, reczny-only push `PATCH /orders/{id}/status`, cron `erli_status_sync` i zakladki w ustawieniach Erli — Phase 129
|
||||||
|
|
||||||
### Deferred
|
### Deferred
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|
||||||
- [ ] v3.8 Erli Marketplace Integration — Phase 129 next: mapowanie statusow pull/push Erli i synchronizacja statusow.
|
- [ ] v3.8 Erli Marketplace Integration — Phase 130 next: generowanie etykiet i obsluga przesylek Erli.
|
||||||
|
|
||||||
### Planned (Next)
|
### Planned (Next)
|
||||||
|
|
||||||
@@ -247,6 +248,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
|||||||
| Erli import uzywa `/inbox` jako glownego zrodla zdarzen | Model inbox jest event-driven i pasuje do bezpiecznego przetwarzania batchy oraz przyszlych aktualizacji statusow | 2026-05-15 | Active |
|
| Erli import uzywa `/inbox` jako glownego zrodla zdarzen | Model inbox jest event-driven i pasuje do bezpiecznego przetwarzania batchy oraz przyszlych aktualizacji statusow | 2026-05-15 | Active |
|
||||||
| ACK Erli przez `POST /inbox/mark-read` tylko po bezblednym batchu | Zapobiega utracie zdarzen, gdy lokalny import czesciowo sie nie powiedzie | 2026-05-15 | Active |
|
| ACK Erli przez `POST /inbox/mark-read` tylko po bezblednym batchu | Zapobiega utracie zdarzen, gdy lokalny import czesciowo sie nie powiedzie | 2026-05-15 | Active |
|
||||||
| Phase 128 ma domyslne mapowania statusow, a UI mapowan dopiero Phase 129 | Import ma realnie dzialac teraz, a pelne strojenie pull/push statusow wymaga osobnej fazy | 2026-05-15 | Active |
|
| Phase 128 ma domyslne mapowania statusow, a UI mapowan dopiero Phase 129 | Import ma realnie dzialac teraz, a pelne strojenie pull/push statusow wymaga osobnej fazy | 2026-05-15 | Active |
|
||||||
|
| Push statusow Erli obejmuje tylko reczne zmiany orderPRO (`change_source='manual'`) | Chroni przed petlami po imporcie, automatyzacjach i systemowych zmianach statusu | 2026-05-16 | Active |
|
||||||
|
| Erli -> orderPRO status pull uzywa tego samego inbox + ACK flow co import zamowien | Jedno bezpieczne zrodlo zdarzen Erli; brak osobnego status endpointu do utrzymania | 2026-05-16 | Active |
|
||||||
|
| Erli settings korzysta z zakladek Integracja/Statusy/Ustawienia | Po dodaniu mapowan strona wymagala parytetu UX z Allegro/shopPRO | 2026-05-16 | Active |
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
@@ -278,6 +282,6 @@ Quick Reference:
|
|||||||
|
|
||||||
---
|
---
|
||||||
*PROJECT.md — Updated when requirements or context change*
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
*Last updated: 2026-05-15 after Phase 128 (Erli Orders Import) closure; v3.8 milestone in progress*
|
*Last updated: 2026-05-16 after Phase 129 (Erli Status Mapping + Sync) closure; v3.8 milestone in progress*
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ v3.8 Erli Marketplace Integration — In progress
|
|||||||
|
|
||||||
Pelna integracja z erli.pl wzorowana na istniejacej integracji Allegro: konfiguracja konta/API, pobieranie zamowien, mapowanie i synchronizacja statusow, generowanie etykiet, tracking oraz wlaczenie Erli w istniejace przeplywy automatyzacji, statystyk i obslugi zamowien.
|
Pelna integracja z erli.pl wzorowana na istniejacej integracji Allegro: konfiguracja konta/API, pobieranie zamowien, mapowanie i synchronizacja statusow, generowanie etykiet, tracking oraz wlaczenie Erli w istniejace przeplywy automatyzacji, statystyk i obslugi zamowien.
|
||||||
|
|
||||||
|
Progress: 3 of 6 phases complete (50%).
|
||||||
|
|
||||||
| Phase | Name | Plans | Status |
|
| Phase | Name | Plans | Status |
|
||||||
|-------|------|-------|--------|
|
|-------|------|-------|--------|
|
||||||
| 127 | Erli Integration Foundation | 1/1 | Complete (2026-05-15; migration/manual Erli API smoke pending operator) |
|
| 127 | Erli Integration Foundation | 1/1 | Complete (2026-05-15; migration/manual Erli API smoke pending operator) |
|
||||||
| 128 | Erli Orders Import | 1/1 | Complete (2026-05-15; migration/manual Erli import smoke pending operator) |
|
| 128 | Erli Orders Import | 1/1 | Complete (2026-05-15; migration/manual Erli import smoke pending operator) |
|
||||||
| 129 | Erli Status Mapping + Sync | TBD | Not started |
|
| 129 | Erli Status Mapping + Sync | 1/1 | Complete (2026-05-16; migration/manual Erli status smoke pending operator) |
|
||||||
| 130 | Erli Shipments + Labels | TBD | Not started |
|
| 130 | Erli Shipments + Labels | TBD | Not started |
|
||||||
| 131 | Erli Tracking + Automation Hooks | TBD | Not started |
|
| 131 | Erli Tracking + Automation Hooks | TBD | Not started |
|
||||||
| 132 | Erli Hardening, Observability + Docs | TBD | Not started |
|
| 132 | Erli Hardening, Observability + Docs | TBD | Not started |
|
||||||
@@ -32,7 +34,7 @@ Plans: 128-01 (complete)
|
|||||||
### Phase 129: Erli Status Mapping + Sync
|
### Phase 129: Erli Status Mapping + Sync
|
||||||
|
|
||||||
Focus: Osobne mapowanie pull/push statusow Erli, auto-discovery nieznanych statusow, cron synchronizacji orderPRO -> Erli i ochrona lokalnych statusow przy re-imporcie analogicznie do Allegro/shopPRO.
|
Focus: Osobne mapowanie pull/push statusow Erli, auto-discovery nieznanych statusow, cron synchronizacji orderPRO -> Erli i ochrona lokalnych statusow przy re-imporcie analogicznie do Allegro/shopPRO.
|
||||||
Plans: TBD (defined during $paul-plan)
|
Plans: 129-01 (complete)
|
||||||
|
|
||||||
### Phase 130: Erli Shipments + Labels
|
### Phase 130: Erli Shipments + Labels
|
||||||
|
|
||||||
@@ -553,4 +555,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-03-12*
|
*Roadmap created: 2026-03-12*
|
||||||
*Last updated: 2026-05-15 - Phase 128 UNIFY closed*
|
*Last updated: 2026-05-16 - Phase 129 complete, ready for Phase 130 planning*
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
## Project Reference
|
## Project Reference
|
||||||
|
|
||||||
See: .paul/PROJECT.md (updated 2026-05-07)
|
See: .paul/PROJECT.md (updated 2026-05-16)
|
||||||
|
|
||||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||||
**Current focus:** v3.8 Erli Marketplace Integration - Phase 128 complete; Phase 129 ready to plan.
|
**Current focus:** v3.8 Erli Marketplace Integration - Phase 130 ready to plan.
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.8 Erli Marketplace Integration
|
Milestone: v3.8 Erli Marketplace Integration
|
||||||
Phase: 129 of 132 (Erli Status Mapping + Sync)
|
Phase: 130 of 132 (Erli Shipments + Labels) - Not started
|
||||||
Plan: Not started
|
Plan: Not started
|
||||||
Status: Ready to plan
|
Status: Ready to plan
|
||||||
Last activity: 2026-05-15 23:52 - Phase 128 complete; transitioned to Phase 129
|
Last activity: 2026-05-16 00:23 - Phase 129 complete, transitioned to Phase 130
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone v3.8: [####------] ~33% (Phases 127-128 complete)
|
- Milestone v3.8: [#####-----] 50% (Phases 127-129 complete; Phase 130 next)
|
||||||
- Phase 129: [----------] 0% (not planned)
|
- Phase 130: [----------] 0% (not planned)
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
@@ -29,20 +29,26 @@ PLAN -> APPLY -> UNIFY
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-05-15 23:52
|
Last session: 2026-05-16 00:23
|
||||||
Stopped at: Phase 128 complete
|
Stopped at: Phase 129 complete, ready to plan Phase 130
|
||||||
Next action: $paul-plan for Phase 129 (Erli Status Mapping + Sync)
|
Next action: $paul-plan for Phase 130 (Erli Shipments + Labels)
|
||||||
Resume file: .paul/phases/128-erli-orders-import/128-01-SUMMARY.md
|
Resume file: .paul/ROADMAP.md
|
||||||
|
|
||||||
## Pending parallel work
|
## Pending parallel work
|
||||||
- None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1).
|
- None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1).
|
||||||
|
|
||||||
## Git State
|
## Git State
|
||||||
|
|
||||||
Last phase commit: 2565d9b feat(128): erli orders import
|
Last phase commit: b371420 feat(129): erli status mapping sync
|
||||||
Previous: d6b18a6 feat(127): erli integration foundation
|
Previous: 2565d9b feat(128): erli orders import
|
||||||
Branch: main
|
Branch: main
|
||||||
|
|
||||||
|
### Skill Audit (Phase 129)
|
||||||
|
|
||||||
|
| Expected | Invoked | Notes |
|
||||||
|
|----------|---------|-------|
|
||||||
|
| `sonar-scanner` | gap documented | Attempted before UNIFY; CLI is not available in PATH. |
|
||||||
|
|
||||||
## Pending Actions
|
## Pending Actions
|
||||||
|
|
||||||
- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online).
|
- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online).
|
||||||
@@ -68,6 +74,8 @@ Branch: main
|
|||||||
- Phase 127 follow-up: uruchom `php bin/migrate.php` gdy lokalny MySQL/XAMPP jest online, zapisz prawdziwy klucz Erli w `/settings/integrations/erli`, wykonaj realny test polaczenia i potwierdz wpis w hubie integracji.
|
- Phase 127 follow-up: uruchom `php bin/migrate.php` gdy lokalny MySQL/XAMPP jest online, zapisz prawdziwy klucz Erli w `/settings/integrations/erli`, wykonaj realny test polaczenia i potwierdz wpis w hubie integracji.
|
||||||
- Phase 128 follow-up: uruchom `php bin/migrate.php`, wlacz import Erli w `/settings/integrations/erli`, kliknij `Importuj zamowienia teraz`, potwierdz `orders.source='erli'` i sprawdz, ze przy bezblednym batchu inbox ACK `POST /inbox/mark-read` nie zostawia nieprzeczytanych zdarzen.
|
- Phase 128 follow-up: uruchom `php bin/migrate.php`, wlacz import Erli w `/settings/integrations/erli`, kliknij `Importuj zamowienia teraz`, potwierdz `orders.source='erli'` i sprawdz, ze przy bezblednym batchu inbox ACK `POST /inbox/mark-read` nie zostawia nieprzeczytanych zdarzen.
|
||||||
- Phase 128 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec test `tests/Unit/ErliOrderMapperTest.php` nie zostal uruchomiony przez PHPUnit; wykonano `php -l` i runtime smoke mappera.
|
- Phase 128 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec test `tests/Unit/ErliOrderMapperTest.php` nie zostal uruchomiony przez PHPUnit; wykonano `php -l` i runtime smoke mappera.
|
||||||
|
- Phase 129 follow-up: uruchom `php bin/migrate.php`, sprawdz `/settings/integrations/erli` mapowania pull/push i zakladki, zapisz mapowania, ustaw `orderPRO -> Erli`, zmien recznie status zamowienia Erli i uruchom cron `erli_status_sync`.
|
||||||
|
- Phase 129 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, a globalny XAMPP PHPUnit jest niekompatybilny z PHP (`each()` removed), wiec testy `ErliOrderMapperTest` i `ErliStatusSyncServiceTest` nie zostaly uruchomione przez PHPUnit; wykonano `php -l`, runtime smoke mappera i `git diff --check`.
|
||||||
|
|
||||||
## Deferred to Next Milestones
|
## Deferred to Next Milestones
|
||||||
|
|
||||||
@@ -78,4 +86,4 @@ Branch: main
|
|||||||
|
|
||||||
## Skill Requirements
|
## Skill Requirements
|
||||||
|
|
||||||
- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122 and Phase 128 gaps documented because CLI was not available in PATH.
|
- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128 and Phase 129 gaps documented because CLI was not available in PATH.
|
||||||
|
|||||||
40
.paul/changelog/2026-05-16.md
Normal file
40
.paul/changelog/2026-05-16.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 2026-05-16
|
||||||
|
|
||||||
|
## Co zrobiono
|
||||||
|
|
||||||
|
- [Phase 129, Plan 01] Wdrozono mapowanie i synchronizacje statusow Erli w obu kierunkach: Erli -> orderPRO przez inbox oraz orderPRO -> Erli przez `PATCH /orders/{id}/status`.
|
||||||
|
- Dodano tabele pull/push mapowan statusow Erli, seed statusow, kursor `last_status_pushed_at`, ustawienia `erli_status_sync_*` i cron `erli_status_sync`.
|
||||||
|
- Dodano repozytoria mapowan, `ErliStatusSyncService`, `ErliStatusSyncHandler`, discovery nieznanych statusow Erli i testy jednostkowe dla mappera/status sync.
|
||||||
|
- Ujednolicono `/settings/integrations/erli` z innymi integracjami przez zakladki Integracja, Statusy i Ustawienia.
|
||||||
|
- Udokumentowano gapy srodowiskowe: brak `vendor/bin/phpunit`, globalny XAMPP PHPUnit niekompatybilny z PHP, brak `sonar-scanner` w PATH.
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `.paul/phases/129-erli-status-mapping-sync/129-01-PLAN.md`
|
||||||
|
- `.paul/phases/129-erli-status-mapping-sync/129-01-SUMMARY.md`
|
||||||
|
- `.paul/ROADMAP.md`
|
||||||
|
- `.paul/STATE.md`
|
||||||
|
- `.paul/PROJECT.md`
|
||||||
|
- `.paul/codebase/architecture.md`
|
||||||
|
- `.paul/codebase/db_schema.md`
|
||||||
|
- `.paul/codebase/tech_changelog.md`
|
||||||
|
- `.paul/changelog/2026-05-16.md`
|
||||||
|
- `database/migrations/20260515_000116_add_erli_status_mapping_sync.sql`
|
||||||
|
- `src/Modules/Settings/ErliApiClient.php`
|
||||||
|
- `src/Modules/Settings/ErliIntegrationController.php`
|
||||||
|
- `src/Modules/Settings/ErliOrderMapper.php`
|
||||||
|
- `src/Modules/Settings/ErliOrderSyncStateRepository.php`
|
||||||
|
- `src/Modules/Settings/ErliOrdersSyncService.php`
|
||||||
|
- `src/Modules/Settings/ErliPullStatusMappingRepository.php`
|
||||||
|
- `src/Modules/Settings/ErliStatusMappingRepository.php`
|
||||||
|
- `src/Modules/Settings/ErliStatusSyncService.php`
|
||||||
|
- `src/Modules/Cron/ErliStatusSyncHandler.php`
|
||||||
|
- `src/Modules/Cron/CronHandlerFactory.php`
|
||||||
|
- `routes/web.php`
|
||||||
|
- `resources/views/settings/erli.php`
|
||||||
|
- `resources/lang/pl.php`
|
||||||
|
- `tests/Unit/ErliOrderMapperTest.php`
|
||||||
|
- `tests/Unit/ErliStatusSyncServiceTest.php`
|
||||||
|
- `DOCS/DB_SCHEMA.md`
|
||||||
|
- `DOCS/ARCHITECTURE.md`
|
||||||
|
- `DOCS/TECH_CHANGELOG.md`
|
||||||
@@ -69,7 +69,7 @@ HTTP Request
|
|||||||
1. **Import** — Cron handler → API client → `OrderImportService` → `OrdersRepository::insertOrder()` → `AutomationService::executeForNewOrder()`
|
1. **Import** — Cron handler → API client → `OrderImportService` → `OrdersRepository::insertOrder()` → `AutomationService::executeForNewOrder()`
|
||||||
2. **Re-import (Phase 111 + 112 + 119)** — `OrderImportRepository::upsertOrderAggregate` wykrywa tranzycje `payment_status` z 0/1 na 2 i zwraca `payment_transition=true`. `AllegroOrderImportService` i `ShopproOrdersSyncService` na tej fladze emituja `payment.status_changed`, co przez chain reguly automatyzacji #7 zmienia `status_code` na `w_realizacji`. Logika preservacji `status_code` z Phase 62 pozostaje rozdzielona (`statusOverwriteAllowed` = `currentStatus='nieoplacone' && newPaymentStatus===2`). **Phase 112-01 (delta-only re-import):** przy `created=false` repo nie wywoluje `replaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistory` — `order_items.id` i flagi lokalne (np. `project_generated` z Phase 97) pozostaja stabilne. `updateOrderDelta()` aktualizuje wylacznie `status_code` (warunkowo, z propagacja anulowania), `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`. Anulowanie ze zrodla (`is_canceled_by_buyer=1` lub zmapowany pull `status_code='anulowane'`) nadpisuje preservacje statusu. Identical-payload guard (`normalizePayloadJson`) pomija UPDATE gdy znormalizowany payload nie rozni sie od DB i brak innych tranzycji. **Phase 119-01 (total_paid protection):** gdy `paymentStatusUnchanged=true` (`oldPaymentStatus === newPaymentStatus`), `updateOrderDelta()` nie dolacza `total_paid` do UPDATE — chroni reczne korekty kwoty (np. zwroty czesciowe). `is_canceled_by_buyer` jest pomijane analogicznie, chyba ze `cancelledBySource=true` (cancel propagation ze zrodla zawsze wymusza wpis flagi). Pozostale pola (`status_code`, `payment_status`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`) zachowuja niezmieniony kontrakt z Phase 112-01.
|
2. **Re-import (Phase 111 + 112 + 119)** — `OrderImportRepository::upsertOrderAggregate` wykrywa tranzycje `payment_status` z 0/1 na 2 i zwraca `payment_transition=true`. `AllegroOrderImportService` i `ShopproOrdersSyncService` na tej fladze emituja `payment.status_changed`, co przez chain reguly automatyzacji #7 zmienia `status_code` na `w_realizacji`. Logika preservacji `status_code` z Phase 62 pozostaje rozdzielona (`statusOverwriteAllowed` = `currentStatus='nieoplacone' && newPaymentStatus===2`). **Phase 112-01 (delta-only re-import):** przy `created=false` repo nie wywoluje `replaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistory` — `order_items.id` i flagi lokalne (np. `project_generated` z Phase 97) pozostaja stabilne. `updateOrderDelta()` aktualizuje wylacznie `status_code` (warunkowo, z propagacja anulowania), `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`. Anulowanie ze zrodla (`is_canceled_by_buyer=1` lub zmapowany pull `status_code='anulowane'`) nadpisuje preservacje statusu. Identical-payload guard (`normalizePayloadJson`) pomija UPDATE gdy znormalizowany payload nie rozni sie od DB i brak innych tranzycji. **Phase 119-01 (total_paid protection):** gdy `paymentStatusUnchanged=true` (`oldPaymentStatus === newPaymentStatus`), `updateOrderDelta()` nie dolacza `total_paid` do UPDATE — chroni reczne korekty kwoty (np. zwroty czesciowe). `is_canceled_by_buyer` jest pomijane analogicznie, chyba ze `cancelledBySource=true` (cancel propagation ze zrodla zawsze wymusza wpis flagi). Pozostale pola (`status_code`, `payment_status`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`) zachowuja niezmieniony kontrakt z Phase 112-01.
|
||||||
3. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
3. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
||||||
4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
|
4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` / `ErliStatusSyncService` → marketplace API
|
||||||
|
|
||||||
### Statistics Summary
|
### Statistics Summary
|
||||||
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
||||||
@@ -97,6 +97,7 @@ HTTP Request
|
|||||||
|---------|------|
|
|---------|------|
|
||||||
| `AllegroOrdersImportHandler` | Fetch new Allegro orders |
|
| `AllegroOrdersImportHandler` | Fetch new Allegro orders |
|
||||||
| `AllegroStatusSyncHandler` | Push status changes to Allegro |
|
| `AllegroStatusSyncHandler` | Push status changes to Allegro |
|
||||||
|
| `ErliStatusSyncHandler` | Pull Erli status events via inbox or push manual local status changes to Erli |
|
||||||
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
|
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
|
||||||
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
|
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
|
||||||
| `ShopproStatusSyncHandler` | Push status to shopPRO |
|
| `ShopproStatusSyncHandler` | Push status to shopPRO |
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ UNIQUE: `(order_id, source_payment_id)`
|
|||||||
UNIQUE: `(integration_id, shoppro_status_code)`
|
UNIQUE: `(integration_id, shoppro_status_code)`
|
||||||
|
|
||||||
**integration_order_sync_state** — Track order fetch progress per integration
|
**integration_order_sync_state** — Track order fetch progress per integration
|
||||||
|
- Phase 129 adds `last_status_pushed_at` for Erli manual status push cursor.
|
||||||
|
|
||||||
**integration_order_status_sync_state** — Track status sync progress per integration and direction
|
**integration_order_status_sync_state** — Track status sync progress per integration and direction
|
||||||
|
|
||||||
@@ -517,6 +518,14 @@ UNIQUE: `(type, name)`
|
|||||||
| `created_at` | DATETIME | |
|
| `created_at` | DATETIME | |
|
||||||
| `updated_at` | DATETIME | |
|
| `updated_at` | DATETIME | |
|
||||||
|
|
||||||
|
**erli_order_status_mappings** — orderPRO status → Erli status used for status push
|
||||||
|
- Seeded official Erli statuses: `created`, `canceled`, `readyToProcess`, `inProgress`, `sent`, `readyToPickup`, `received`, `returned`, `returningToSender`, `unknown`.
|
||||||
|
- `orderpro_status_code` is nullable; unmapped status rows are skipped by push sync.
|
||||||
|
|
||||||
|
**erli_order_status_pull_mappings** — Erli status → orderPRO status used during inbox import
|
||||||
|
- Seeded defaults: `pending -> nieoplacone`, `purchased -> nowe`, `cancelled -> anulowane`.
|
||||||
|
- Unknown Erli statuses are discovered from `/inbox` and inserted with nullable mapping.
|
||||||
|
|
||||||
**allegro_delivery_method_mappings** — Map order delivery method strings to Allegro services
|
**allegro_delivery_method_mappings** — Map order delivery method strings to Allegro services
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Technical Changelog
|
# Technical Changelog
|
||||||
|
|
||||||
|
## 2026-05-16 - Phase 129 Plan 01: Erli Status Mapping + Sync
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Dodano mapowania statusow Erli pull/push, `ErliStatusSyncService`, `ErliStatusSyncHandler`, endpoint `PATCH /orders/{id}/status` w kliencie API oraz UI mapowan w `/settings/integrations/erli`.
|
||||||
|
- Import Erli odkrywa surowe statusy z inboxa i uzywa `erli_order_status_pull_mappings`; push obejmuje tylko reczne zmiany statusu z `order_status_history.change_source='manual'`.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Phase 128 miala domyslne statusy; Phase 129 daje operatorowi kontrolowane mapowanie i bezpieczny push bez petli automatyzacji.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-05-13 - Phase 126 Plan 01: Invoice GUS Field Mapping Fix (KRS heuristic)
|
## 2026-05-13 - Phase 126 Plan 01: Invoice GUS Field Mapping Fix (KRS heuristic)
|
||||||
|
|
||||||
**Co zrobiono:**
|
**Co zrobiono:**
|
||||||
|
|||||||
304
.paul/phases/129-erli-status-mapping-sync/129-01-PLAN.md
Normal file
304
.paul/phases/129-erli-status-mapping-sync/129-01-PLAN.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
---
|
||||||
|
phase: 129-erli-status-mapping-sync
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260515_000116_add_erli_status_mapping_sync.sql
|
||||||
|
- src/Modules/Settings/ErliApiClient.php
|
||||||
|
- src/Modules/Settings/ErliIntegrationController.php
|
||||||
|
- src/Modules/Settings/ErliIntegrationRepository.php
|
||||||
|
- src/Modules/Settings/ErliOrderMapper.php
|
||||||
|
- src/Modules/Settings/ErliOrdersSyncService.php
|
||||||
|
- src/Modules/Settings/ErliStatusMappingRepository.php
|
||||||
|
- src/Modules/Settings/ErliPullStatusMappingRepository.php
|
||||||
|
- src/Modules/Settings/ErliStatusSyncService.php
|
||||||
|
- src/Modules/Cron/ErliStatusSyncHandler.php
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/settings/erli.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- tests/Unit/ErliOrderMapperTest.php
|
||||||
|
- tests/Unit/ErliStatusSyncServiceTest.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Wdrozyc mapowanie i synchronizacje statusow Erli w obu kierunkach: Erli -> orderPRO przy imporcie `/inbox` oraz orderPRO -> Erli przez cron push na `PATCH /orders/{id}/status`.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Phase 128 importuje zamowienia Erli, ale statusy sa jeszcze mapowane sztywnymi defaultami. Phase 129 ma dac operatorowi kontrolowane mapowania pull/push, bezpieczne odkrywanie nowych statusow z inboxa oraz automatyczny push recznych zmian statusu z orderPRO do Erli.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
Nowe tabele mapowan statusow Erli, UI w ustawieniach Erli, endpointy zapisu mapowan, serwis synchronizacji statusow, handler crona `erli_status_sync`, rozszerzony mapper importu oraz testy i dokumentacja.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
<clarifications>
|
||||||
|
- **Zakres** — Czy Phase 129 ma objac od razu oba kierunki synchronizacji statusow Erli?
|
||||||
|
-> Odpowiedz: Tak, oba kierunki.
|
||||||
|
- **Statusy** — Jak traktowac liste statusow Erli w UI mapowan?
|
||||||
|
-> Odpowiedz: Seed + discovery.
|
||||||
|
- **Push** — Ktore lokalne zmiany statusu orderPRO maja byc wysylane do Erli?
|
||||||
|
-> Odpowiedz: Tylko reczne zmiany statusu (`order_status_history.change_source='manual'`).
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
@DOCS/DB_SCHEMA.md
|
||||||
|
@DOCS/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/127-erli-integration-foundation/127-01-SUMMARY.md
|
||||||
|
@.paul/phases/128-erli-orders-import/128-01-SUMMARY.md
|
||||||
|
|
||||||
|
## External API Context
|
||||||
|
- Official Erli docs checked 2026-05-15: `https://erli.pl/svc/shop-api/doc/`
|
||||||
|
- Relevant Erli contract:
|
||||||
|
- `/inbox` contains new orders and order status changes.
|
||||||
|
- Base order status values documented for import: `pending`, `purchased`, `cancelled`.
|
||||||
|
- Swagger exposes `PATCH /orders/{id}/status` with body `{"status": "created|canceled|readyToProcess|inProgress|sent|readyToPickup|received|returned|returningToSender|unknown"}`.
|
||||||
|
- Erli docs state that order update for Erli shipments should not send `trackingNumber`; shipment/tracking remains Phase 130-131.
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Settings/ErliApiClient.php
|
||||||
|
@src/Modules/Settings/ErliIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/ErliIntegrationController.php
|
||||||
|
@src/Modules/Settings/ErliOrderMapper.php
|
||||||
|
@src/Modules/Settings/ErliOrdersSyncService.php
|
||||||
|
@src/Modules/Settings/AllegroStatusMappingRepository.php
|
||||||
|
@src/Modules/Settings/AllegroPullStatusMappingRepository.php
|
||||||
|
@src/Modules/Settings/AllegroStatusMappingController.php
|
||||||
|
@src/Modules/Settings/AllegroStatusSyncService.php
|
||||||
|
@src/Modules/Settings/ShopproStatusSyncService.php
|
||||||
|
@src/Modules/Settings/OrderStatusRepository.php
|
||||||
|
@src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
@src/Modules/Cron/AllegroStatusSyncHandler.php
|
||||||
|
@resources/views/settings/erli.php
|
||||||
|
@resources/views/settings/allegro.php
|
||||||
|
@routes/web.php
|
||||||
|
@resources/lang/pl.php
|
||||||
|
@tests/Unit/AllegroStatusSyncServiceTest.php
|
||||||
|
@tests/Unit/ErliOrderMapperTest.php
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
## Required Skills (from SPECIAL-FLOWS.md)
|
||||||
|
|
||||||
|
| Skill | Priority | When to Invoke | Loaded? |
|
||||||
|
|-------|----------|----------------|---------|
|
||||||
|
| `sonar-scanner` | required | After APPLY, before UNIFY | ○ |
|
||||||
|
| `/feature-dev` | optional | New marketplace integration feature | ○ |
|
||||||
|
| `/code-review` | optional | Before UNIFY if broad risks remain | ○ |
|
||||||
|
| `/frontend-design` | optional | If Erli settings UI needs significant redesign | ○ |
|
||||||
|
|
||||||
|
**BLOCKING:** Required skills MUST be attempted before UNIFY. If `sonar-scanner` is unavailable in PATH, document the gap in SUMMARY and STATE as in Phase 128.
|
||||||
|
|
||||||
|
## Skill Invocation Checklist
|
||||||
|
- [ ] `sonar-scanner` run or documented unavailable
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Erli status mapping schema and seeds
|
||||||
|
```gherkin
|
||||||
|
Given Phase 129 migration is applied
|
||||||
|
When the database is inspected
|
||||||
|
Then Erli has separate pull and push mapping tables, seeded with documented Erli statuses, and `erli_status_sync` exists as a disabled-by-default or settings-controlled cron schedule
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Erli settings UI exposes status mappings
|
||||||
|
```gherkin
|
||||||
|
Given an operator opens `/settings/integrations/erli`
|
||||||
|
When they review the settings page
|
||||||
|
Then they can configure Erli -> orderPRO pull mappings, orderPRO -> Erli push mappings, status sync direction, and status sync interval without editing code
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Import uses configurable pull mappings with discovery
|
||||||
|
```gherkin
|
||||||
|
Given an Erli inbox message contains status `pending`, `purchased`, `cancelled`, or a new status
|
||||||
|
When `ErliOrdersSyncService` imports the message
|
||||||
|
Then `ErliOrderMapper` uses the configured pull mapping when present, falls back to safe defaults when absent, and stores unknown Erli statuses for later mapping
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Cron pushes only manual orderPRO status changes to Erli
|
||||||
|
```gherkin
|
||||||
|
Given an Erli order has a manual status change in `order_status_history`
|
||||||
|
When the `erli_status_sync` cron handler runs in orderPRO -> Erli direction
|
||||||
|
Then the service maps the current orderPRO status to an Erli status and calls `PATCH /orders/{id}/status` only for mapped manual changes after the last pushed cursor
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Push is safe and observable
|
||||||
|
```gherkin
|
||||||
|
Given Erli credentials are missing, mappings are incomplete, or Erli API returns an error
|
||||||
|
When status sync runs
|
||||||
|
Then the result reports pushed/skipped/failed counts, records bounded errors, and advances the push cursor only for successfully processed change timestamps
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Documentation and tests cover status behavior
|
||||||
|
```gherkin
|
||||||
|
Given Phase 129 implementation is complete
|
||||||
|
When verification runs
|
||||||
|
Then mapper/status-sync unit tests, PHP lint, docs, and PAUL summary describe the new status mapping and sync behavior
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add Erli status mapping persistence, seeds, API client method</name>
|
||||||
|
<files>
|
||||||
|
database/migrations/20260515_000116_add_erli_status_mapping_sync.sql,
|
||||||
|
src/Modules/Settings/ErliApiClient.php,
|
||||||
|
src/Modules/Settings/ErliIntegrationRepository.php,
|
||||||
|
src/Modules/Settings/ErliStatusMappingRepository.php,
|
||||||
|
src/Modules/Settings/ErliPullStatusMappingRepository.php,
|
||||||
|
DOCS/DB_SCHEMA.md
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create the persistence foundation:
|
||||||
|
- Add `erli_order_status_mappings` for push mappings (`orderpro_status_code` -> `erli_status_code`, name, timestamps).
|
||||||
|
- Add `erli_order_status_pull_mappings` for pull mappings (`erli_status_code` -> `orderpro_status_code`, name, timestamps).
|
||||||
|
- Seed documented pull statuses: `pending -> nieoplacone`, `purchased -> nowe`, `cancelled -> anulowane`.
|
||||||
|
- Seed documented push status options from official swagger: `created`, `canceled`, `readyToProcess`, `inProgress`, `sent`, `readyToPickup`, `received`, `returned`, `returningToSender`, `unknown`. Keep orderPRO mappings nullable where no safe default exists.
|
||||||
|
- Seed `app_settings` keys `erli_status_sync_direction=erli_to_orderpro` and `erli_status_sync_interval_minutes=15`.
|
||||||
|
- Seed `cron_schedules.job_type='erli_status_sync'` idempotently; prefer disabled until operator enables/import settings are confirmed, unless existing local pattern makes enabled safer.
|
||||||
|
- Add repositories mirroring Allegro style: list, replaceAll, find mapped status, upsertDiscoveredStatus, build reverse map.
|
||||||
|
- Add `ErliApiClient::updateOrderStatus(array $credentials, string $orderId, string $erliStatus): array` calling `PATCH /orders/{id}/status` with JSON `{status: ...}`.
|
||||||
|
- Extend repository credentials/settings only as needed for status sync settings; do not introduce per-account Erli complexity.
|
||||||
|
- Update DB schema docs.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliApiClient.php`
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliStatusMappingRepository.php`
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliPullStatusMappingRepository.php`
|
||||||
|
Inspect migration for idempotent DDL/seed and no raw runtime use of `DB_HOST_REMOTE`.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 foundation is implemented and AC-5 has API error result shape available.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add Erli settings UI and routes for pull/push mappings</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Settings/ErliIntegrationController.php,
|
||||||
|
routes/web.php,
|
||||||
|
resources/views/settings/erli.php,
|
||||||
|
resources/lang/pl.php,
|
||||||
|
DOCS/ARCHITECTURE.md
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Extend the Erli settings page without creating inline CSS or native alert/confirm:
|
||||||
|
- Inject `OrderStatusRepository`, `ErliStatusMappingRepository`, and `ErliPullStatusMappingRepository`.
|
||||||
|
- Pass orderPRO statuses, Erli push statuses, Erli pull mappings, current sync direction and interval to the view.
|
||||||
|
- Add POST handlers for saving pull mappings and push mappings with CSRF validation.
|
||||||
|
- Add status sync direction and interval fields to the existing Erli settings form, validating direction against `erli_to_orderpro` / `orderpro_to_erli` and interval 1-1440.
|
||||||
|
- Add routes under `/settings/integrations/erli/statuses/save-pull` and `/settings/integrations/erli/statuses/save-push` (or one clear equivalent route if controller shape is cleaner).
|
||||||
|
- Render compact mapping tables similar to Allegro, but keep Erli page simple; do not build a landing/marketing page.
|
||||||
|
- Add Polish translations for labels, hints, empty states, validation and flash messages.
|
||||||
|
- Preserve existing test/import flash behavior from Phase 128.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliIntegrationController.php`
|
||||||
|
`C:\xampp\php\php.exe -l routes/web.php`
|
||||||
|
`C:\xampp\php\php.exe -l resources/views/settings/erli.php`
|
||||||
|
`C:\xampp\php\php.exe -l resources/lang/pl.php`
|
||||||
|
Manual inspect that forms include `_token` and use escaped output via `$e()`.
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 is satisfied and existing Phase 127/128 settings actions remain reachable.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Wire pull discovery and push cron sync</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Settings/ErliOrderMapper.php,
|
||||||
|
src/Modules/Settings/ErliOrdersSyncService.php,
|
||||||
|
src/Modules/Settings/ErliStatusSyncService.php,
|
||||||
|
src/Modules/Cron/ErliStatusSyncHandler.php,
|
||||||
|
src/Modules/Cron/CronHandlerFactory.php,
|
||||||
|
tests/Unit/ErliOrderMapperTest.php,
|
||||||
|
tests/Unit/ErliStatusSyncServiceTest.php,
|
||||||
|
DOCS/ARCHITECTURE.md,
|
||||||
|
DOCS/TECH_CHANGELOG.md
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Implement runtime behavior:
|
||||||
|
- Let `ErliOrderMapper` accept an optional pull mapping dependency or mapping array while preserving the existing no-dependency unit-test usage.
|
||||||
|
- During import, discover any raw Erli order status from inbox and upsert it into the pull mapping repository before/while mapping.
|
||||||
|
- Use configured pull mapping when available; otherwise keep Phase 128 defaults (`pending`, `purchased`, `cancelled`) and safe fallback.
|
||||||
|
- Add `ErliStatusSyncService` similar to Allegro/shopPRO:
|
||||||
|
- Direction `erli_to_orderpro`: run Erli inbox import with `ignore_orders_fetch_enabled=true` so status-change messages are pulled via the same safe ACK path.
|
||||||
|
- Direction `orderpro_to_erli`: find only Erli orders with `order_status_history.change_source='manual'` newer than `last_status_pushed_at` for the active Erli integration.
|
||||||
|
- Use push mapping to call `ErliApiClient::updateOrderStatus()`.
|
||||||
|
- Count `pushed`, `skipped`, `failed`; keep bounded errors; advance cursor only to latest successfully processed change timestamp.
|
||||||
|
- Add `ErliStatusSyncHandler` and register `erli_status_sync` in `CronHandlerFactory`.
|
||||||
|
- Add/update unit tests for pull mapping override, unknown status discovery behavior, skipped unmapped push, successful push, and failed push cursor behavior.
|
||||||
|
- Update architecture/changelog docs.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliOrderMapper.php`
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliOrdersSyncService.php`
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Settings/ErliStatusSyncService.php`
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Cron/ErliStatusSyncHandler.php`
|
||||||
|
`C:\xampp\php\php.exe -l src/Modules/Cron/CronHandlerFactory.php`
|
||||||
|
`C:\xampp\php\php.exe -l tests/Unit/ErliStatusSyncServiceTest.php`
|
||||||
|
If available: `vendor/bin/phpunit tests/Unit/ErliOrderMapperTest.php tests/Unit/ErliStatusSyncServiceTest.php`
|
||||||
|
Required skill: `sonar-scanner` or document unavailable.
|
||||||
|
</verify>
|
||||||
|
<done>AC-3, AC-4, AC-5 and AC-6 are satisfied.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Do not change shipment/label generation flows; Erli shipment creation belongs to Phase 130.
|
||||||
|
- Do not send Erli `trackingNumber` in status sync; official docs say shipment number is handled after shipment generation.
|
||||||
|
- Do not broaden push to automation/system/import changes; Phase 129 push covers only manual `order_status_history` rows.
|
||||||
|
- Do not alter Allegro/shopPRO mapping semantics except for reading patterns.
|
||||||
|
- Do not use `DB_HOST_REMOTE` in runtime code.
|
||||||
|
- Do not add native `alert()`/`confirm()` or inline CSS in Erli settings.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- No Erli webhook registration in this plan; inbox/cron remains the source.
|
||||||
|
- No product stock adjustment on Erli cancellation; document if needed for a later products/inventory phase.
|
||||||
|
- No manual live Erli API call is required during APPLY; live smoke is an operator follow-up unless credentials and DB are ready.
|
||||||
|
- No redesign of the whole integrations UI; keep changes scoped to Erli settings.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] PHP lint passes for all changed PHP/view/lang/test files.
|
||||||
|
- [ ] Migration is idempotent and documented in `DOCS/DB_SCHEMA.md`.
|
||||||
|
- [ ] Erli settings page renders pull/push mapping forms with CSRF and escaped values.
|
||||||
|
- [ ] Mapper tests cover configured mapping fallback and unknown status handling.
|
||||||
|
- [ ] Status sync tests cover successful push, skipped unmapped push, failed push, and cursor behavior.
|
||||||
|
- [ ] `vendor/bin/phpunit tests/Unit/ErliOrderMapperTest.php tests/Unit/ErliStatusSyncServiceTest.php` run if available; otherwise gap documented.
|
||||||
|
- [ ] `sonar-scanner` attempted per SPECIAL-FLOWS; unavailable CLI documented as gap.
|
||||||
|
- [ ] `git diff --check` passes.
|
||||||
|
- [ ] All acceptance criteria met.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Erli status pull/push mappings can be configured from UI.
|
||||||
|
- Erli import uses configured pull mapping and discovers unknown raw statuses.
|
||||||
|
- Erli status cron can push only manual orderPRO status changes to Erli using `PATCH /orders/{id}/status`.
|
||||||
|
- Failures are observable and do not incorrectly advance push cursor.
|
||||||
|
- Docs and tests reflect the new behavior.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/129-erli-status-mapping-sync/129-01-SUMMARY.md`.
|
||||||
|
</output>
|
||||||
198
.paul/phases/129-erli-status-mapping-sync/129-01-SUMMARY.md
Normal file
198
.paul/phases/129-erli-status-mapping-sync/129-01-SUMMARY.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
phase: 129-erli-status-mapping-sync
|
||||||
|
plan: 01
|
||||||
|
subsystem: settings, integrations, cron, database
|
||||||
|
tags: [erli, status-mapping, status-sync, cron, marketplace]
|
||||||
|
requires:
|
||||||
|
- phase: 127-erli-integration-foundation
|
||||||
|
provides: global Erli credentials, API client, settings page and hub row
|
||||||
|
- phase: 128-erli-orders-import
|
||||||
|
provides: Erli inbox import, order mapper, sync state and safe ACK flow
|
||||||
|
provides:
|
||||||
|
- Configurable Erli -> orderPRO pull status mappings with discovery
|
||||||
|
- Configurable orderPRO -> Erli push status mappings
|
||||||
|
- Cron-driven Erli status sync in pull or push direction
|
||||||
|
- Tabbed Erli settings UI consistent with other integrations
|
||||||
|
affects: [phase-130-erli-shipments-labels, phase-131-erli-tracking-automation, erli-settings]
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [separate pull/push marketplace status mappings, manual-only push cursor, tabbed integration settings]
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260515_000116_add_erli_status_mapping_sync.sql
|
||||||
|
- src/Modules/Settings/ErliStatusMappingRepository.php
|
||||||
|
- src/Modules/Settings/ErliPullStatusMappingRepository.php
|
||||||
|
- src/Modules/Settings/ErliStatusSyncService.php
|
||||||
|
- src/Modules/Cron/ErliStatusSyncHandler.php
|
||||||
|
- tests/Unit/ErliStatusSyncServiceTest.php
|
||||||
|
modified:
|
||||||
|
- src/Modules/Settings/ErliApiClient.php
|
||||||
|
- src/Modules/Settings/ErliIntegrationController.php
|
||||||
|
- src/Modules/Settings/ErliOrderMapper.php
|
||||||
|
- src/Modules/Settings/ErliOrdersSyncService.php
|
||||||
|
- src/Modules/Settings/ErliOrderSyncStateRepository.php
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/settings/erli.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
key-decisions:
|
||||||
|
- "Push to Erli uses only manual orderPRO status changes from order_status_history.change_source='manual'."
|
||||||
|
- "Pull status changes reuse the existing Erli inbox import and ACK flow instead of a separate status endpoint."
|
||||||
|
- "Erli settings use the same tabbed UI pattern as Allegro/shopPRO."
|
||||||
|
patterns-established:
|
||||||
|
- "Erli has separate pull and push mapping repositories/tables to avoid mixing import and outbound sync semantics."
|
||||||
|
- "Unknown Erli statuses discovered in inbox are stored for later operator mapping."
|
||||||
|
duration: 35min
|
||||||
|
started: 2026-05-16T00:00:00+02:00
|
||||||
|
completed: 2026-05-16T00:23:35+02:00
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 129 Plan 01: Erli Status Mapping + Sync Summary
|
||||||
|
|
||||||
|
Erli now supports configurable pull/push status mapping, status discovery from inbox, and cron-based status synchronization with a tabbed settings UI.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~35min |
|
||||||
|
| Started | 2026-05-16T00:00:00+02:00 |
|
||||||
|
| Completed | 2026-05-16T00:23:35+02:00 |
|
||||||
|
| Tasks | 3 completed |
|
||||||
|
| Files modified | 25 phase files, excluding unrelated `.vscode/ftp-kr.sync.cache.json` |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Erli status mapping schema and seeds | Pass | Migration adds pull/push mapping tables, `integration_order_sync_state.last_status_pushed_at`, app settings, seed status options, and disabled `erli_status_sync` schedule. |
|
||||||
|
| AC-2: Erli settings UI exposes status mappings | Pass | `/settings/integrations/erli` exposes tabbed panels for integration, statuses and settings; operators can save pull/push mappings, direction and interval with CSRF-protected forms. |
|
||||||
|
| AC-3: Import uses configurable pull mappings with discovery | Pass | `ErliOrderMapper` uses configured pull mappings with safe fallbacks; `ErliOrdersSyncService` stores newly seen raw Erli statuses for later mapping. |
|
||||||
|
| AC-4: Cron pushes only manual orderPRO status changes to Erli | Pass | `ErliStatusSyncService` selects only Erli orders with manual status history newer than `last_status_pushed_at` and calls `PATCH /orders/{id}/status` only when mapped. |
|
||||||
|
| AC-5: Push is safe and observable | Pass | Sync result reports pushed/skipped/failed and bounded errors; cursor advances only through successfully processed timestamps. |
|
||||||
|
| AC-6: Documentation and tests cover status behavior | Pass with env gaps | PHP lint and diff checks passed; PHPUnit and Sonar are documented gaps because local CLIs are unavailable/broken. Docs were updated. |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Added Erli status persistence for both directions with seed statuses from the documented Erli status contract.
|
||||||
|
- Extended Erli API client with `PATCH /orders/{id}/status`.
|
||||||
|
- Added repositories and controller endpoints for saving pull and push mappings.
|
||||||
|
- Reused the existing inbox import path for Erli -> orderPRO status pulls.
|
||||||
|
- Added `ErliStatusSyncService` and cron handler for configurable pull/push sync.
|
||||||
|
- Updated Erli settings UI to match other integrations with tabs.
|
||||||
|
- Added mapper and status sync unit test coverage files.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
No per-task commits were created during APPLY. Phase transition creates the scoped phase commit.
|
||||||
|
|
||||||
|
| Task | Commit | Type | Description |
|
||||||
|
|------|--------|------|-------------|
|
||||||
|
| Task 1: Persistence/API foundation | pending phase commit | feat | Mapping tables, seeds, repositories and API status update method. |
|
||||||
|
| Task 2: Settings UI/routes | pending phase commit | feat | Erli settings status controls, mapping forms, routes and translations. |
|
||||||
|
| Task 3: Runtime sync/tests/docs | pending phase commit | feat/test/docs | Discovery, pull/push sync service, cron handler, tests and docs. |
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `database/migrations/20260515_000116_add_erli_status_mapping_sync.sql` | Created | Status mapping tables, sync cursor, seeds and cron/app settings. |
|
||||||
|
| `src/Modules/Settings/ErliStatusMappingRepository.php` | Created | Push mapping persistence for orderPRO -> Erli. |
|
||||||
|
| `src/Modules/Settings/ErliPullStatusMappingRepository.php` | Created | Pull mapping persistence and discovery for Erli -> orderPRO. |
|
||||||
|
| `src/Modules/Settings/ErliStatusSyncService.php` | Created | Pull/push status sync orchestration and result counters. |
|
||||||
|
| `src/Modules/Cron/ErliStatusSyncHandler.php` | Created | Cron entrypoint for `erli_status_sync`. |
|
||||||
|
| `tests/Unit/ErliStatusSyncServiceTest.php` | Created | Unit tests for push success, skipped unmapped changes and failure cursor behavior. |
|
||||||
|
| `src/Modules/Settings/ErliApiClient.php` | Modified | Added authenticated Erli status update request. |
|
||||||
|
| `src/Modules/Settings/ErliIntegrationController.php` | Modified | Added tab state, status settings and mapping save handlers. |
|
||||||
|
| `src/Modules/Settings/ErliOrderMapper.php` | Modified | Added configurable pull mapping with safe fallback. |
|
||||||
|
| `src/Modules/Settings/ErliOrdersSyncService.php` | Modified | Added raw Erli status discovery during inbox import. |
|
||||||
|
| `src/Modules/Settings/ErliOrderSyncStateRepository.php` | Modified | Added push cursor support. |
|
||||||
|
| `src/Modules/Cron/CronHandlerFactory.php` | Modified | Registered `erli_status_sync`. |
|
||||||
|
| `routes/web.php` | Modified | Wired repositories, status sync service and mapping routes. |
|
||||||
|
| `resources/views/settings/erli.php` | Modified | Added tabbed UI and pull/push mapping forms. |
|
||||||
|
| `resources/lang/pl.php` | Modified | Added Erli status mapping/sync/tabs translations. |
|
||||||
|
| `DOCS/DB_SCHEMA.md` | Modified | Documented new tables/settings/cursor. |
|
||||||
|
| `DOCS/ARCHITECTURE.md` | Modified | Documented Erli status sync flow and tabbed settings. |
|
||||||
|
| `DOCS/TECH_CHANGELOG.md` | Modified | Logged Phase 129 and Erli settings tabs fix. |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Push only manual status changes | Prevents loops from imports, automation and system updates. | Operator intent is explicit; automated changes stay local unless manually changed. |
|
||||||
|
| Pull uses inbox import | Erli status events arrive through inbox and the Phase 128 ACK flow is already safe. | One source of truth for Erli event processing. |
|
||||||
|
| Separate pull/push mapping tables | Import statuses and outbound Erli status values have different semantics. | Safer UI and easier future extension. |
|
||||||
|
| Unknown pull statuses are discovered | Erli can introduce or send statuses not present in seed data. | Operator can map new statuses without code changes. |
|
||||||
|
| Erli settings use tabs | Page grew after Phase 129 and needed parity with Allegro/shopPRO. | Better scanability, same interaction model as other integrations. |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | UI parity fix, no backend contract change. |
|
||||||
|
| Scope additions | 1 | Tabbed Erli settings page added during final verification. |
|
||||||
|
| Deferred | 2 | Environment-dependent verification remains operator follow-up. |
|
||||||
|
|
||||||
|
**Total impact:** Positive UI consistency improvement; no expansion into shipments/labels/tracking.
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. Erli settings were missing tabs**
|
||||||
|
- **Found during:** Post-APPLY manual inspection.
|
||||||
|
- **Issue:** Erli settings displayed all integration/status/import/test sections in one vertical page, unlike Allegro/shopPRO.
|
||||||
|
- **Fix:** Added active `tab` handling, `return_to` for forms and standard `content-tabs-nav` / `content-tab-panel` markup.
|
||||||
|
- **Files:** `resources/views/settings/erli.php`, `src/Modules/Settings/ErliIntegrationController.php`, `resources/lang/pl.php`, docs.
|
||||||
|
- **Verification:** PHP lint for view/controller/lang and `git diff --check`.
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
- Phase 129 follow-up: run `php bin/migrate.php`, verify `/settings/integrations/erli` mappings, set `orderPRO -> Erli`, manually change an Erli order status and run `erli_status_sync`.
|
||||||
|
- Phase 129 verification gap: `vendor/bin/phpunit` is absent in this checkout and global XAMPP PHPUnit is incompatible with the current PHP, so PHPUnit tests were not executed.
|
||||||
|
- Phase 129 skill gap: `sonar-scanner` is not available in PATH, so Sonar scan could not run.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| `vendor/bin/phpunit` missing | Documented as verification gap; PHP lint and diff checks were run. |
|
||||||
|
| Global XAMPP `phpunit` crashes on removed PHP `each()` | Documented as verification gap. |
|
||||||
|
| `sonar-scanner` unavailable | Documented in SUMMARY and STATE skill audit. |
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| `php -l resources/views/settings/erli.php` | Pass |
|
||||||
|
| `php -l src/Modules/Settings/ErliIntegrationController.php` | Pass |
|
||||||
|
| `php -l resources/lang/pl.php` | Pass |
|
||||||
|
| Phase APPLY PHP lints for changed PHP/view/test files | Pass |
|
||||||
|
| `git diff --check` | Pass |
|
||||||
|
| `vendor/bin/phpunit tests/Unit/ErliOrderMapperTest.php tests/Unit/ErliStatusSyncServiceTest.php` | Not run: `vendor/bin/phpunit` missing |
|
||||||
|
| `phpunit --version` | Failed: old XAMPP PHPUnit uses removed `each()` |
|
||||||
|
| `sonar-scanner --version` | Failed: command not found |
|
||||||
|
|
||||||
|
Skill audit: required `sonar-scanner` was attempted and documented unavailable.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Erli status mapping and sync foundation is in place for later shipment/tracking work.
|
||||||
|
- Erli settings now has the tab structure needed to host future shipment/label settings.
|
||||||
|
- Push sync cursor and manual-change filtering provide a safe outbound pattern for future Erli API writes.
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Live Erli behavior still needs migration and production credential smoke testing.
|
||||||
|
- PHPUnit dev dependencies are missing locally, so unit tests need a `composer install` or CI run.
|
||||||
|
- Sonar scan still depends on installing/configuring `sonar-scanner` in PATH.
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None for planning Phase 130. Manual smoke remains required before production confidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 129-erli-status-mapping-sync, Plan: 01*
|
||||||
|
*Completed: 2026-05-16*
|
||||||
@@ -39,7 +39,7 @@ HTTP Request
|
|||||||
| **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export |
|
| **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export |
|
||||||
| **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments |
|
| **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments |
|
||||||
| **Automation** | 6 | `AutomationService` (834 LOC), `AutomationRepository`, `AutomationExecutionLogRepository` | Event→condition→action rules, email triggers |
|
| **Automation** | 6 | `AutomationService` (834 LOC), `AutomationRepository`, `AutomationExecutionLogRepository` | Event→condition→action rules, email triggers |
|
||||||
| **Settings** | 57+ | Integration controllers, OAuth clients, API clients, mappers | Allegro/shopPRO/Erli/Apaczka/InPost config, status mappings |
|
| **Settings** | 60+ | Integration controllers, OAuth clients, API clients, mappers | Allegro/shopPRO/Erli/Apaczka/InPost config, status mappings |
|
||||||
| **Sms** | 3 | `SmsMessageRepository`, `SmsConversationService`, `SmsplanetWebhookController` | SMSPLANET outbound order SMS, inbound webhook parsing, order matching |
|
| **Sms** | 3 | `SmsMessageRepository`, `SmsConversationService`, `SmsplanetWebhookController` | SMSPLANET outbound order SMS, inbound webhook parsing, order matching |
|
||||||
| **Notifications** | 3 | `NotificationRepository`, `NotificationController`, `NotificationApiController` | Global notification history, unread polling API, mark-read actions |
|
| **Notifications** | 3 | `NotificationRepository`, `NotificationController`, `NotificationApiController` | Global notification history, unread polling API, mark-read actions |
|
||||||
| **Cron** | 12 | `CronRepository`, `CronHandlerFactory`, handler classes | Scheduled imports, syncs, token refresh |
|
| **Cron** | 12 | `CronRepository`, `CronHandlerFactory`, handler classes | Scheduled imports, syncs, token refresh |
|
||||||
@@ -74,7 +74,7 @@ HTTP Request
|
|||||||
### Order Lifecycle
|
### Order Lifecycle
|
||||||
1. **Import** — Cron handler → API client → `OrderImportService` → `OrdersRepository::insertOrder()` → `AutomationService::executeForNewOrder()`
|
1. **Import** — Cron handler → API client → `OrderImportService` → `OrdersRepository::insertOrder()` → `AutomationService::executeForNewOrder()`
|
||||||
2. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
2. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
||||||
3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
|
3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` / `ErliStatusSyncService` → marketplace API
|
||||||
|
|
||||||
### Statistics Summary
|
### Statistics Summary
|
||||||
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
||||||
@@ -112,6 +112,7 @@ HTTP Request
|
|||||||
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
|
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
|
||||||
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
|
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
|
||||||
| `ErliOrdersImportHandler` | Fetch unread Erli inbox order events |
|
| `ErliOrdersImportHandler` | Fetch unread Erli inbox order events |
|
||||||
|
| `ErliStatusSyncHandler` | Pull Erli status events via inbox or push manual local status changes to Erli |
|
||||||
| `ShopproStatusSyncHandler` | Push status to shopPRO |
|
| `ShopproStatusSyncHandler` | Push status to shopPRO |
|
||||||
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses |
|
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses |
|
||||||
| `ShipmentTrackingHandler` | Poll carrier tracking APIs |
|
| `ShipmentTrackingHandler` | Poll carrier tracking APIs |
|
||||||
@@ -120,11 +121,12 @@ HTTP Request
|
|||||||
|
|
||||||
### Erli Integration Foundation
|
### Erli Integration Foundation
|
||||||
|
|
||||||
1. **Settings** - `/settings/integrations/erli` stores one global Erli API key encrypted via `IntegrationSecretCipher`, an optional account label, active flag, and last connection-test result.
|
1. **Settings** - `/settings/integrations/erli` renders tabbed integration/status/settings panels and stores one global Erli API key encrypted via `IntegrationSecretCipher`, an optional account label, active flag, and last connection-test result.
|
||||||
2. **Connection test** - `ErliIntegrationController::test()` loads active credentials, calls `ErliApiClient::testConnection()`, performs a real authenticated `GET https://erli.pl/svc/shop-api/inbox`, and stores the result in `integrations.last_test_*`.
|
2. **Connection test** - `ErliIntegrationController::test()` loads active credentials, calls `ErliApiClient::testConnection()`, performs a real authenticated `GET https://erli.pl/svc/shop-api/inbox`, and stores the result in `integrations.last_test_*`.
|
||||||
3. **Hub** - `IntegrationsHubController::buildErliRow()` adds Erli to `/settings/integrations` with configured/missing secret status, active status, last test timestamp, and configure URL.
|
3. **Hub** - `IntegrationsHubController::buildErliRow()` adds Erli to `/settings/integrations` with configured/missing secret status, active status, last test timestamp, and configure URL.
|
||||||
4. **Order import** - Phase 128 adds `/settings/integrations/erli/import` and cron `erli_orders_import`. Both call `ErliOrdersSyncService`, which fetches unread `/inbox` messages, maps supported order events through `ErliOrderMapper`, persists via `OrderImportRepository::upsertOrderAggregate()`, emits existing automation events, and acknowledges `POST /inbox/mark-read` only after a zero-failure batch.
|
4. **Order import** - Phase 128 adds `/settings/integrations/erli/import` and cron `erli_orders_import`. Both call `ErliOrdersSyncService`, which fetches unread `/inbox` messages, maps supported order events through `ErliOrderMapper`, persists via `OrderImportRepository::upsertOrderAggregate()`, emits existing automation events, and acknowledges `POST /inbox/mark-read` only after a zero-failure batch.
|
||||||
5. **Deferred** - Phase 128 does not implement status push mappings, label generation, shipment creation, or tracking. Those flows are planned for v3.8 Phases 129-131.
|
5. **Status mapping/sync** - Phase 129 adds pull/push status mapping tables, status controls in Erli settings, and cron `erli_status_sync`. Pull reuses inbox import; push sends manual orderPRO status changes to `PATCH /orders/{id}/status`.
|
||||||
|
6. **Deferred** - Label generation, shipment creation, and tracking are planned for v3.8 Phases 130-131.
|
||||||
|
|
||||||
## Dependency Injection
|
## Dependency Injection
|
||||||
|
|
||||||
@@ -184,25 +186,33 @@ tests/
|
|||||||
### ErliApiClient (`src/Modules/Settings/ErliApiClient.php`)
|
### ErliApiClient (`src/Modules/Settings/ErliApiClient.php`)
|
||||||
- `testConnection()` wykonuje realny `GET /inbox` do Erli z naglowkiem `Authorization: Bearer ...`.
|
- `testConnection()` wykonuje realny `GET /inbox` do Erli z naglowkiem `Authorization: Bearer ...`.
|
||||||
- Phase 128: `fetchInbox()` pobiera do 500 nieprzeczytanych wiadomosci; `markInboxRead()` potwierdza `POST /inbox/mark-read` z `lastMessageId` dopiero po udanym batchu.
|
- Phase 128: `fetchInbox()` pobiera do 500 nieprzeczytanych wiadomosci; `markInboxRead()` potwierdza `POST /inbox/mark-read` z `lastMessageId` dopiero po udanym batchu.
|
||||||
|
- Phase 129: `updateOrderStatus()` wysyla `PATCH /orders/{id}/status` z body `{"status": "..."}` dla recznych zmian statusu orderPRO mapowanych na status Erli.
|
||||||
- Wysyla `Accept: application/json` i `User-Agent: orderPRO/1.0 (erli-integration)`.
|
- Wysyla `Accept: application/json` i `User-Agent: orderPRO/1.0 (erli-integration)`.
|
||||||
- Traktuje HTTP 2xx jako sukces; 401/403 jako blad autoryzacji, 429 jako limit zapytan, pozostale bledy jako czytelny komunikat z odpowiedzi.
|
- Traktuje HTTP 2xx jako sukces; 401/403 jako blad autoryzacji, 429 jako limit zapytan, pozostale bledy jako czytelny komunikat z odpowiedzi.
|
||||||
- Uzywa `SslCertificateResolver` i nie wywoluje `curl_close()` (PHP 8.5 compatible).
|
- Uzywa `SslCertificateResolver` i nie wywoluje `curl_close()` (PHP 8.5 compatible).
|
||||||
|
|
||||||
### ErliIntegrationController (`src/Modules/Settings/ErliIntegrationController.php`)
|
### ErliIntegrationController (`src/Modules/Settings/ErliIntegrationController.php`)
|
||||||
- Endpointy: `GET /settings/integrations/erli`, `POST /settings/integrations/erli/save`, `POST /settings/integrations/erli/test`, `POST /settings/integrations/erli/import`.
|
- Endpointy: `GET /settings/integrations/erli`, `POST /settings/integrations/erli/save`, `POST /settings/integrations/erli/test`, `POST /settings/integrations/erli/import`, `POST /settings/integrations/erli/statuses/save-pull`, `POST /settings/integrations/erli/statuses/save-push`.
|
||||||
- `save` zapisuje label, aktywnosc, sekret i ustawienia importu (`orders_fetch_enabled`, `orders_fetch_start_date`, interwal crona); `test` wykonuje realny test API i zapisuje wynik przez `IntegrationsRepository::updateTestResult()`.
|
- `save` zapisuje label, aktywnosc, sekret, ustawienia importu (`orders_fetch_enabled`, `orders_fetch_start_date`, interwal crona) oraz kierunek/interwal `erli_status_sync`; `test` wykonuje realny test API i zapisuje wynik przez `IntegrationsRepository::updateTestResult()`.
|
||||||
- `importNow()` uruchamia reczny import Erli z pominieciem flagi cron enable, ale nadal wymaga aktywnych credentials.
|
- `importNow()` uruchamia reczny import Erli z pominieciem flagi cron enable, ale nadal wymaga aktywnych credentials.
|
||||||
|
|
||||||
### ErliOrdersSyncService / ErliOrderMapper (`src/Modules/Settings/`)
|
### 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`.
|
||||||
- Obsluguje tylko zdarzenia order inbox (`orderCreated`, `orderStatusChanged`, `orderSellerStatusChanged`); wiadomosci produktowe sa pomijane do przyszlych faz.
|
- Obsluguje tylko zdarzenia order inbox (`orderCreated`, `orderStatusChanged`, `orderSellerStatusChanged`); wiadomosci produktowe sa pomijane do przyszlych faz.
|
||||||
- `ErliOrderMapper` mapuje statusy bazowe: `pending -> nieoplacone`, `purchased -> nowe`, `cancelled/returned -> anulowane`. Konfigurowalne pull/push status mappings sa odlozone do Phase 129.
|
- `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.
|
||||||
- Nowe zamowienia z invoice/company/tax id ustawiają `orders.invoice_requested=1`; re-import korzysta z istniejacego delta-only kontraktu `OrderImportRepository`.
|
- 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.
|
- Automatyzacje: `order.imported` dla nowych zamowien i `payment.status_changed` przy tranzycji platnosci na re-imporcie.
|
||||||
|
|
||||||
### ErliOrdersImportHandler (`src/Modules/Cron/ErliOrdersImportHandler.php`)
|
### ErliOrdersImportHandler (`src/Modules/Cron/ErliOrdersImportHandler.php`)
|
||||||
- Handler crona `erli_orders_import`, domyslnie seedowany jako disabled. Operator wlacza go z ustawien Erli.
|
- Handler crona `erli_orders_import`, domyslnie seedowany jako disabled. Operator wlacza go z ustawien Erli.
|
||||||
|
|
||||||
|
### ErliStatusSyncService / ErliStatusSyncHandler (`src/Modules/Settings/`, `src/Modules/Cron/`)
|
||||||
|
- Kierunek `erli_to_orderpro` wywoluje `ErliOrdersSyncService::sync()` z `ignore_orders_fetch_enabled=true`, czyli statusy przychodzace z Erli przechodza tym samym bezpiecznym `/inbox` + ACK flow co import.
|
||||||
|
- 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.
|
||||||
|
|
||||||
### IntegrationsHubController
|
### IntegrationsHubController
|
||||||
- Dodaje wiersz Erli do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
- Dodaje wiersz Erli do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ UNIQUE: `(integration_id, shoppro_status_code)`
|
|||||||
| `last_synced_external_order_id` | VARCHAR(128) | YES | Legacy/source-specific cursor |
|
| `last_synced_external_order_id` | VARCHAR(128) | YES | Legacy/source-specific cursor |
|
||||||
| `last_run_at` | DATETIME | YES | |
|
| `last_run_at` | DATETIME | YES | |
|
||||||
| `last_success_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 | |
|
||||||
| `created_at` | DATETIME | NO | |
|
| `created_at` | DATETIME | NO | |
|
||||||
| `updated_at` | DATETIME | NO | |
|
| `updated_at` | DATETIME | NO | |
|
||||||
@@ -524,6 +525,26 @@ UNIQUE: `(type, name)`
|
|||||||
| `created_at` | DATETIME | |
|
| `created_at` | DATETIME | |
|
||||||
| `updated_at` | DATETIME | |
|
| `updated_at` | DATETIME | |
|
||||||
|
|
||||||
|
**erli_order_status_mappings** — orderPRO status → Erli status used for status push
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| `id` | INT UNSIGNED | PK |
|
||||||
|
| `erli_status_code` | VARCHAR(64) | UNIQUE; official values include `created`, `canceled`, `readyToProcess`, `inProgress`, `sent`, `readyToPickup`, `received`, `returned`, `returningToSender`, `unknown` |
|
||||||
|
| `erli_status_name` | VARCHAR(120) | Optional display label |
|
||||||
|
| `orderpro_status_code` | VARCHAR(64) | Nullable; empty mapping means push skips that orderPRO status |
|
||||||
|
| `created_at` | DATETIME | |
|
||||||
|
| `updated_at` | DATETIME | |
|
||||||
|
|
||||||
|
**erli_order_status_pull_mappings** — Erli status → orderPRO status used during `/inbox` import
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| `id` | INT UNSIGNED | PK |
|
||||||
|
| `erli_status_code` | VARCHAR(64) | UNIQUE; seeded with `pending`, `purchased`, `cancelled` and extended by discovery |
|
||||||
|
| `erli_status_name` | VARCHAR(120) | Optional display label |
|
||||||
|
| `orderpro_status_code` | VARCHAR(64) | Nullable; if missing, mapper uses safe Phase 128 defaults |
|
||||||
|
| `created_at` | DATETIME | |
|
||||||
|
| `updated_at` | DATETIME | |
|
||||||
|
|
||||||
**allegro_delivery_method_mappings** — Map order delivery method strings to Allegro services
|
**allegro_delivery_method_mappings** — Map order delivery method strings to Allegro services
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
@@ -952,7 +973,7 @@ Index: `(status, priority, scheduled_at)`
|
|||||||
| `created_at` | DATETIME | NO | |
|
| `created_at` | DATETIME | NO | |
|
||||||
| `updated_at` | DATETIME | NO | |
|
| `updated_at` | DATETIME | NO | |
|
||||||
|
|
||||||
Seeded recurring jobs include `shoppro_orders_import`, `allegro_orders_import`, `shoppro_order_status_sync`, `shoppro_payment_status_sync`, `allegro_status_sync`, `shipment_tracking_sync`, `automation_history_cleanup`, `order_status_aged`, and `erli_orders_import` (Phase 128; default disabled until Erli order import is enabled).
|
Seeded recurring jobs include `shoppro_orders_import`, `allegro_orders_import`, `shoppro_order_status_sync`, `shoppro_payment_status_sync`, `allegro_status_sync`, `shipment_tracking_sync`, `automation_history_cleanup`, `order_status_aged`, `erli_orders_import` (Phase 128; default disabled until Erli order import is enabled), and `erli_status_sync` (Phase 129; default disabled until Erli settings save enables it).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
# Technical Changelog
|
# Technical Changelog
|
||||||
|
|
||||||
|
## 2026-05-16 - Erli Settings Tabs UI Fix
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Ujednolicono `/settings/integrations/erli` z innymi integracjami przez dodanie zakladek Integracja, Statusy i Ustawienia.
|
||||||
|
- Dodano obsluge aktywnej zakladki przez parametr `tab` oraz `return_to` w formularzach Erli, zeby po zapisie wracac do wlasciwego panelu.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Ekran Erli po Phase 129 mial wszystkie sekcje jedna pod druga, przez co odstawal od Allegro/shopPRO i byl trudniejszy do skanowania.
|
||||||
|
|
||||||
|
## 2026-05-16 - Phase 129 Plan 01: Erli Status Mapping + Sync
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Dodano migracje `20260515_000116_add_erli_status_mapping_sync.sql` z tabelami `erli_order_status_mappings`, `erli_order_status_pull_mappings`, kolumna `integration_order_sync_state.last_status_pushed_at`, seedami statusow Erli oraz cronem `erli_status_sync`.
|
||||||
|
- Rozszerzono ustawienia Erli o mapowania Erli -> orderPRO, orderPRO -> Erli, kierunek synchronizacji statusow i interwal crona.
|
||||||
|
- Dodano `ErliStatusMappingRepository`, `ErliPullStatusMappingRepository`, `ErliStatusSyncService` oraz `ErliStatusSyncHandler`.
|
||||||
|
- Rozszerzono `ErliApiClient` o `PATCH /orders/{id}/status` oraz `ErliOrdersSyncService` o discovery surowych statusow z inboxa.
|
||||||
|
- `ErliOrderMapper` korzysta teraz z konfigurowalnych pull mappings, zachowujac fallbacki z Phase 128.
|
||||||
|
- Dodano testy jednostkowe `tests/Unit/ErliStatusSyncServiceTest.php` i rozszerzono `ErliOrderMapperTest`.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Phase 128 importowala zamowienia Erli, ale statusy byly mapowane domyslnie. Operator potrzebuje kontrolowac pull/push statusow analogicznie do Allegro/shopPRO.
|
||||||
|
- Push jest ograniczony do recznych zmian statusu (`order_status_history.change_source='manual'`), zeby uniknac petli po automatyzacjach i reimportach.
|
||||||
|
|
||||||
|
**BREAKING / migracja:**
|
||||||
|
- Brak breaking changes. Nowy cron jest domyslnie wylaczony w migracji; zapis ustawien Erli wlacza go zgodnie z aktywnoscia integracji.
|
||||||
|
- Manual smoke po wdrozeniu: `php bin/migrate.php`, sprawdz widok `/settings/integrations/erli`, zapisz mapowania, ustaw `orderPRO -> Erli`, zmien recznie status zamowienia Erli i uruchom cron `erli_status_sync`.
|
||||||
|
|
||||||
## 2026-05-15 - Phase 128 Plan 01: Erli Orders Import
|
## 2026-05-15 - Phase 128 Plan 01: Erli Orders Import
|
||||||
|
|
||||||
**Co zrobiono:**
|
**Co zrobiono:**
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS erli_order_status_mappings (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
erli_status_code VARCHAR(64) NOT NULL,
|
||||||
|
erli_status_name VARCHAR(120) NULL,
|
||||||
|
orderpro_status_code VARCHAR(64) NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY erli_order_status_mappings_code_unique (erli_status_code),
|
||||||
|
KEY erli_order_status_mappings_orderpro_code_idx (orderpro_status_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS erli_order_status_pull_mappings (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
erli_status_code VARCHAR(64) NOT NULL,
|
||||||
|
erli_status_name VARCHAR(120) NULL,
|
||||||
|
orderpro_status_code VARCHAR(64) NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY erli_order_status_pull_mappings_code_unique (erli_status_code),
|
||||||
|
KEY erli_order_status_pull_mappings_orderpro_code_idx (orderpro_status_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
ALTER TABLE integration_order_sync_state
|
||||||
|
ADD COLUMN IF NOT EXISTS last_status_pushed_at DATETIME NULL AFTER last_success_at;
|
||||||
|
|
||||||
|
INSERT INTO erli_order_status_pull_mappings (
|
||||||
|
erli_status_code, erli_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES
|
||||||
|
('pending', 'Oczekuje', 'nieoplacone', NOW(), NOW()),
|
||||||
|
('purchased', 'Kupione', 'nowe', NOW(), NOW()),
|
||||||
|
('cancelled', 'Anulowane', 'anulowane', NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
erli_status_name = VALUES(erli_status_name),
|
||||||
|
orderpro_status_code = COALESCE(erli_order_status_pull_mappings.orderpro_status_code, VALUES(orderpro_status_code)),
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
INSERT INTO erli_order_status_mappings (
|
||||||
|
erli_status_code, erli_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES
|
||||||
|
('created', 'Utworzone', NULL, NOW(), NOW()),
|
||||||
|
('canceled', 'Anulowane', 'anulowane', NOW(), NOW()),
|
||||||
|
('readyToProcess', 'Gotowe do realizacji', 'nowe', NOW(), NOW()),
|
||||||
|
('inProgress', 'W realizacji', 'w_realizacji', NOW(), NOW()),
|
||||||
|
('sent', 'Wyslane', 'wyslane', NOW(), NOW()),
|
||||||
|
('readyToPickup', 'Gotowe do odbioru', NULL, NOW(), NOW()),
|
||||||
|
('received', 'Odebrane', NULL, NOW(), NOW()),
|
||||||
|
('returned', 'Zwrocone', 'anulowane', NOW(), NOW()),
|
||||||
|
('returningToSender', 'Wraca do nadawcy', NULL, NOW(), NOW()),
|
||||||
|
('unknown', 'Nieznane', NULL, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
erli_status_name = VALUES(erli_status_name),
|
||||||
|
orderpro_status_code = COALESCE(erli_order_status_mappings.orderpro_status_code, VALUES(orderpro_status_code)),
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('erli_status_sync_direction', 'erli_to_orderpro', NOW(), NOW()),
|
||||||
|
('erli_status_sync_interval_minutes', '15', NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
setting_value = setting_value,
|
||||||
|
updated_at = updated_at;
|
||||||
|
|
||||||
|
INSERT INTO cron_schedules (
|
||||||
|
job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
'erli_status_sync', 900, 45, 3, NULL, 0, NULL, NOW(), NOW(), NOW()
|
||||||
|
) ON DUPLICATE KEY UPDATE
|
||||||
|
interval_seconds = VALUES(interval_seconds),
|
||||||
|
priority = VALUES(priority),
|
||||||
|
max_attempts = VALUES(max_attempts),
|
||||||
|
updated_at = NOW();
|
||||||
@@ -863,6 +863,12 @@ return [
|
|||||||
'erli' => [
|
'erli' => [
|
||||||
'title' => 'Integracja Erli',
|
'title' => 'Integracja Erli',
|
||||||
'description' => 'Konfiguracja globalnego polaczenia z marketplace Erli.',
|
'description' => 'Konfiguracja globalnego polaczenia z marketplace Erli.',
|
||||||
|
'tabs' => [
|
||||||
|
'label' => 'Zakladki integracji Erli',
|
||||||
|
'integration' => 'Integracja',
|
||||||
|
'statuses' => 'Statusy',
|
||||||
|
'settings' => 'Ustawienia',
|
||||||
|
],
|
||||||
'config' => [
|
'config' => [
|
||||||
'title' => 'Konfiguracja API',
|
'title' => 'Konfiguracja API',
|
||||||
],
|
],
|
||||||
@@ -882,6 +888,10 @@ return [
|
|||||||
'orders_fetch_enabled' => 'Wlacz automatyczny import zamowien',
|
'orders_fetch_enabled' => 'Wlacz automatyczny import zamowien',
|
||||||
'orders_fetch_start_date' => 'Data startu importu',
|
'orders_fetch_start_date' => 'Data startu importu',
|
||||||
'orders_import_interval_minutes' => 'Interwal importu (minuty)',
|
'orders_import_interval_minutes' => 'Interwal importu (minuty)',
|
||||||
|
'status_sync_direction' => 'Kierunek synchronizacji statusow',
|
||||||
|
'status_sync_direction_pull' => 'Erli -> orderPRO',
|
||||||
|
'status_sync_direction_push' => 'orderPRO -> Erli',
|
||||||
|
'status_sync_interval_minutes' => 'Interwal synchronizacji statusow (minuty)',
|
||||||
],
|
],
|
||||||
'api_key' => [
|
'api_key' => [
|
||||||
'saved' => 'Klucz API jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
'saved' => 'Klucz API jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
||||||
@@ -891,6 +901,29 @@ return [
|
|||||||
'account_label' => 'Opcjonalna nazwa widoczna w hubie integracji.',
|
'account_label' => 'Opcjonalna nazwa widoczna w hubie integracji.',
|
||||||
'orders_fetch_start_date' => 'Opcjonalnie pominie zdarzenia starsze niz podana data, jesli payload Erli zawiera date zamowienia.',
|
'orders_fetch_start_date' => 'Opcjonalnie pominie zdarzenia starsze niz podana data, jesli payload Erli zawiera date zamowienia.',
|
||||||
'orders_import_interval_minutes' => 'Dotyczy zadania cron `erli_orders_import`. Zakres: 1-1440 minut.',
|
'orders_import_interval_minutes' => 'Dotyczy zadania cron `erli_orders_import`. Zakres: 1-1440 minut.',
|
||||||
|
'status_sync_direction' => 'Pull pobiera statusy przez inbox. Push wysyla reczne zmiany statusu z orderPRO do Erli.',
|
||||||
|
'status_sync_interval_minutes' => 'Dotyczy zadania cron `erli_status_sync`. Zakres: 1-1440 minut.',
|
||||||
|
],
|
||||||
|
'statuses' => [
|
||||||
|
'pull_title' => 'Mapowanie przy imporcie (Erli -> orderPRO)',
|
||||||
|
'pull_description' => 'Przypisz surowe statusy Erli do statusow orderPRO. Nieznane statusy z inboxa beda dopisywane automatycznie po imporcie.',
|
||||||
|
'push_title' => 'Mapowanie przy wysylce (orderPRO -> Erli)',
|
||||||
|
'push_description' => 'Przypisz statusy orderPRO do statusow Erli uzywanych przy recznej zmianie statusu i cron push.',
|
||||||
|
'empty_pull' => 'Brak statusow Erli do mapowania. Uruchom migracje lub import, aby zasilic liste.',
|
||||||
|
'empty_push' => 'Brak statusow Erli do mapowania. Uruchom migracje, aby zasilic liste.',
|
||||||
|
'fields' => [
|
||||||
|
'erli_status' => 'Status Erli',
|
||||||
|
'orderpro_status' => 'Status orderPRO',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'save_pull' => 'Zapisz mapowanie importu',
|
||||||
|
'save_push' => 'Zapisz mapowanie wysylki',
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'saved_pull' => 'Mapowanie importu statusow Erli zostalo zapisane.',
|
||||||
|
'saved_push' => 'Mapowanie wysylki statusow Erli zostalo zapisane.',
|
||||||
|
'save_failed' => 'Nie udalo sie zapisac mapowan statusow Erli.',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'status' => [
|
'status' => [
|
||||||
'secret' => 'Sekret API',
|
'secret' => 'Sekret API',
|
||||||
@@ -914,6 +947,7 @@ return [
|
|||||||
],
|
],
|
||||||
'validation' => [
|
'validation' => [
|
||||||
'orders_fetch_start_date_invalid' => 'Data startu importu musi miec format RRRR-MM-DD.',
|
'orders_fetch_start_date_invalid' => 'Data startu importu musi miec format RRRR-MM-DD.',
|
||||||
|
'status_sync_direction_invalid' => 'Wybierz poprawny kierunek synchronizacji statusow Erli.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'inpost' => [
|
'inpost' => [
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ $lastTestHttpCode = $settings['last_test_http_code'] ?? null;
|
|||||||
$ordersFetchEnabled = (bool) ($settings['orders_fetch_enabled'] ?? false);
|
$ordersFetchEnabled = (bool) ($settings['orders_fetch_enabled'] ?? false);
|
||||||
$ordersFetchStartDate = trim((string) ($settings['orders_fetch_start_date'] ?? ''));
|
$ordersFetchStartDate = trim((string) ($settings['orders_fetch_start_date'] ?? ''));
|
||||||
$ordersImportIntervalMinutes = (int) ($ordersImportIntervalMinutes ?? 5);
|
$ordersImportIntervalMinutes = (int) ($ordersImportIntervalMinutes ?? 5);
|
||||||
|
$statusSyncDirection = (string) ($statusSyncDirection ?? 'erli_to_orderpro');
|
||||||
|
$statusSyncIntervalMinutes = (int) ($statusSyncIntervalMinutes ?? 15);
|
||||||
|
$orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : [];
|
||||||
|
$erliStatusMappings = is_array($erliStatusMappings ?? null) ? $erliStatusMappings : [];
|
||||||
|
$erliPullStatusMappings = is_array($erliPullStatusMappings ?? null) ? $erliPullStatusMappings : [];
|
||||||
|
$activeTab = (string) ($activeTab ?? 'integration');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -34,6 +40,20 @@ $ordersImportIntervalMinutes = (int) ($ordersImportIntervalMinutes ?? 5);
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card mt-16">
|
<section class="card mt-16">
|
||||||
|
<nav class="content-tabs-nav" aria-label="<?= $e($t('settings.erli.tabs.label')) ?>">
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'integration' ? ' is-active' : '' ?>" data-tab-target="erli-tab-integration">
|
||||||
|
<?= $e($t('settings.erli.tabs.integration')) ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-target="erli-tab-statuses">
|
||||||
|
<?= $e($t('settings.erli.tabs.statuses')) ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-target="erli-tab-settings">
|
||||||
|
<?= $e($t('settings.erli.tabs.settings')) ?>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'integration' ? ' is-active' : '' ?>" data-tab-panel="erli-tab-integration">
|
||||||
|
<section class="mt-16">
|
||||||
<h3 class="section-title"><?= $e($t('settings.erli.config.title')) ?></h3>
|
<h3 class="section-title"><?= $e($t('settings.erli.config.title')) ?></h3>
|
||||||
|
|
||||||
<div class="muted mt-12">
|
<div class="muted mt-12">
|
||||||
@@ -46,6 +66,7 @@ $ordersImportIntervalMinutes = (int) ($ordersImportIntervalMinutes ?? 5);
|
|||||||
|
|
||||||
<form class="statuses-form mt-16" action="/settings/integrations/erli/save" method="post" novalidate>
|
<form class="statuses-form mt-16" action="/settings/integrations/erli/save" method="post" novalidate>
|
||||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="return_to" value="/settings/integrations/erli?tab=integration">
|
||||||
|
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span class="field-label"><?= $e($t('settings.erli.fields.account_label')) ?></span>
|
<span class="field-label"><?= $e($t('settings.erli.fields.account_label')) ?></span>
|
||||||
@@ -85,30 +106,182 @@ $ordersImportIntervalMinutes = (int) ($ordersImportIntervalMinutes ?? 5);
|
|||||||
<span class="muted"><?= $e($t('settings.erli.hints.orders_import_interval_minutes')) ?></span>
|
<span class="muted"><?= $e($t('settings.erli.hints.orders_import_interval_minutes')) ?></span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.erli.fields.status_sync_direction')) ?></span>
|
||||||
|
<select class="form-control" name="status_sync_direction">
|
||||||
|
<option value="erli_to_orderpro"<?= $statusSyncDirection === 'erli_to_orderpro' ? ' selected' : '' ?>>
|
||||||
|
<?= $e($t('settings.erli.fields.status_sync_direction_pull')) ?>
|
||||||
|
</option>
|
||||||
|
<option value="orderpro_to_erli"<?= $statusSyncDirection === 'orderpro_to_erli' ? ' selected' : '' ?>>
|
||||||
|
<?= $e($t('settings.erli.fields.status_sync_direction_push')) ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span class="muted"><?= $e($t('settings.erli.hints.status_sync_direction')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.erli.fields.status_sync_interval_minutes')) ?></span>
|
||||||
|
<input class="form-control" type="number" min="1" max="1440" name="status_sync_interval_minutes" value="<?= $e((string) $statusSyncIntervalMinutes) ?>">
|
||||||
|
<span class="muted"><?= $e($t('settings.erli.hints.status_sync_interval_minutes')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="form-actions mt-16">
|
<div class="form-actions mt-16">
|
||||||
<button type="submit" class="btn btn--primary"><?= $e($t('settings.erli.actions.save')) ?></button>
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.erli.actions.save')) ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="card mt-16">
|
<div class="content-tab-panel<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-panel="erli-tab-statuses">
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.erli.statuses.pull_title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.erli.statuses.pull_description')) ?></p>
|
||||||
|
|
||||||
|
<form action="/settings/integrations/erli/statuses/save-pull" method="post" class="mt-12">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="return_to" value="/settings/integrations/erli?tab=statuses">
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.erli.statuses.fields.erli_status')) ?></th>
|
||||||
|
<th><?= $e($t('settings.erli.statuses.fields.orderpro_status')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($erliPullStatusMappings === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="muted"><?= $e($t('settings.erli.statuses.empty_pull')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($erliPullStatusMappings as $mapping): ?>
|
||||||
|
<?php
|
||||||
|
$erliCode = strtolower(trim((string) ($mapping['erli_status_code'] ?? '')));
|
||||||
|
if ($erliCode === '') continue;
|
||||||
|
$erliName = trim((string) ($mapping['erli_status_name'] ?? ''));
|
||||||
|
$selectedOrderproCode = strtolower(trim((string) ($mapping['orderpro_status_code'] ?? '')));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?= $e($erliName !== '' ? $erliName : $erliCode) ?> <code class="muted"><?= $e($erliCode) ?></code>
|
||||||
|
<input type="hidden" name="erli_status_code[]" value="<?= $e($erliCode) ?>">
|
||||||
|
<input type="hidden" name="erli_status_name[]" value="<?= $e($erliName) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="orderpro_status_code[]">
|
||||||
|
<option value=""><?= $e($t('settings.order_statuses.fields.no_mapping')) ?></option>
|
||||||
|
<?php foreach ($orderproStatuses as $status): ?>
|
||||||
|
<?php
|
||||||
|
$opCode = strtolower(trim((string) ($status['code'] ?? '')));
|
||||||
|
if ($opCode === '') continue;
|
||||||
|
$opName = (string) ($status['name'] ?? $opCode);
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($opCode) ?>"<?= $selectedOrderproCode === $opCode ? ' selected' : '' ?>>
|
||||||
|
<?= $e($opName) ?> (<?= $e($opCode) ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if ($erliPullStatusMappings !== []): ?>
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.erli.statuses.actions.save_pull')) ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.erli.statuses.push_title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.erli.statuses.push_description')) ?></p>
|
||||||
|
|
||||||
|
<form action="/settings/integrations/erli/statuses/save-push" method="post" class="mt-12">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="return_to" value="/settings/integrations/erli?tab=statuses">
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.erli.statuses.fields.erli_status')) ?></th>
|
||||||
|
<th><?= $e($t('settings.erli.statuses.fields.orderpro_status')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($erliStatusMappings === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="muted"><?= $e($t('settings.erli.statuses.empty_push')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($erliStatusMappings as $mapping): ?>
|
||||||
|
<?php
|
||||||
|
$erliCode = trim((string) ($mapping['erli_status_code'] ?? ''));
|
||||||
|
if ($erliCode === '') continue;
|
||||||
|
$erliName = trim((string) ($mapping['erli_status_name'] ?? ''));
|
||||||
|
$selectedOrderproCode = strtolower(trim((string) ($mapping['orderpro_status_code'] ?? '')));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?= $e($erliName !== '' ? $erliName : $erliCode) ?> <code class="muted"><?= $e($erliCode) ?></code>
|
||||||
|
<input type="hidden" name="erli_status_code[]" value="<?= $e($erliCode) ?>">
|
||||||
|
<input type="hidden" name="erli_status_name[]" value="<?= $e($erliName) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="orderpro_status_code[]">
|
||||||
|
<option value=""><?= $e($t('settings.order_statuses.fields.no_mapping')) ?></option>
|
||||||
|
<?php foreach ($orderproStatuses as $status): ?>
|
||||||
|
<?php
|
||||||
|
$opCode = strtolower(trim((string) ($status['code'] ?? '')));
|
||||||
|
if ($opCode === '') continue;
|
||||||
|
$opName = (string) ($status['name'] ?? $opCode);
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($opCode) ?>"<?= $selectedOrderproCode === $opCode ? ' selected' : '' ?>>
|
||||||
|
<?= $e($opName) ?> (<?= $e($opCode) ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if ($erliStatusMappings !== []): ?>
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.erli.statuses.actions.save_push')) ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-panel="erli-tab-settings">
|
||||||
|
<section class="mt-16">
|
||||||
<h3 class="section-title"><?= $e($t('settings.erli.import.title')) ?></h3>
|
<h3 class="section-title"><?= $e($t('settings.erli.import.title')) ?></h3>
|
||||||
<p class="muted mt-12"><?= $e($t('settings.erli.import.description')) ?></p>
|
<p class="muted mt-12"><?= $e($t('settings.erli.import.description')) ?></p>
|
||||||
|
|
||||||
<form class="statuses-form mt-16" action="/settings/integrations/erli/import" method="post">
|
<form class="statuses-form mt-16" action="/settings/integrations/erli/import" method="post">
|
||||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="return_to" value="/settings/integrations/erli?tab=settings">
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.erli.actions.import_now')) ?></button>
|
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.erli.actions.import_now')) ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card mt-16">
|
<section class="mt-16">
|
||||||
<h3 class="section-title"><?= $e($t('settings.erli.test.title')) ?></h3>
|
<h3 class="section-title"><?= $e($t('settings.erli.test.title')) ?></h3>
|
||||||
<p class="muted mt-12"><?= $e($t('settings.erli.test.description')) ?></p>
|
<p class="muted mt-12"><?= $e($t('settings.erli.test.description')) ?></p>
|
||||||
|
|
||||||
<form class="statuses-form mt-16" action="/settings/integrations/erli/test" method="post">
|
<form class="statuses-form mt-16" action="/settings/integrations/erli/test" method="post">
|
||||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="return_to" value="/settings/integrations/erli?tab=settings">
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.erli.actions.test')) ?></button>
|
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.erli.actions.test')) ?></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,3 +301,40 @@ $ordersImportIntervalMinutes = (int) ($ordersImportIntervalMinutes ?? 5);
|
|||||||
?></div>
|
?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var tabs = document.querySelectorAll('[data-tab-target]');
|
||||||
|
var panels = document.querySelectorAll('[data-tab-panel]');
|
||||||
|
if (tabs.length === 0 || panels.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tabNameMap = {
|
||||||
|
'erli-tab-integration': 'integration',
|
||||||
|
'erli-tab-statuses': 'statuses',
|
||||||
|
'erli-tab-settings': 'settings'
|
||||||
|
};
|
||||||
|
|
||||||
|
tabs.forEach(function (tab) {
|
||||||
|
tab.addEventListener('click', function () {
|
||||||
|
var target = tab.getAttribute('data-tab-target');
|
||||||
|
var tabName = tabNameMap[target] || 'integration';
|
||||||
|
var url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('tab', tabName);
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
|
||||||
|
tabs.forEach(function (node) { node.classList.remove('is-active'); });
|
||||||
|
panels.forEach(function (panel) { panel.classList.remove('is-active'); });
|
||||||
|
tab.classList.add('is-active');
|
||||||
|
|
||||||
|
var panel = document.querySelector('[data-tab-panel="' + target + '"]');
|
||||||
|
if (panel) {
|
||||||
|
panel.classList.add('is-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ use App\Modules\Settings\ErliIntegrationRepository;
|
|||||||
use App\Modules\Settings\ErliOrderMapper;
|
use App\Modules\Settings\ErliOrderMapper;
|
||||||
use App\Modules\Settings\ErliOrderSyncStateRepository;
|
use App\Modules\Settings\ErliOrderSyncStateRepository;
|
||||||
use App\Modules\Settings\ErliOrdersSyncService;
|
use App\Modules\Settings\ErliOrdersSyncService;
|
||||||
|
use App\Modules\Settings\ErliPullStatusMappingRepository;
|
||||||
|
use App\Modules\Settings\ErliStatusMappingRepository;
|
||||||
use App\Modules\Settings\FakturowniaApiClient;
|
use App\Modules\Settings\FakturowniaApiClient;
|
||||||
use App\Modules\Settings\FakturowniaIntegrationController;
|
use App\Modules\Settings\FakturowniaIntegrationController;
|
||||||
use App\Modules\Settings\FakturowniaIntegrationRepository;
|
use App\Modules\Settings\FakturowniaIntegrationRepository;
|
||||||
@@ -239,6 +241,8 @@ return static function (Application $app): void {
|
|||||||
$app->db(),
|
$app->db(),
|
||||||
(string) $app->config('app.integrations.secret', '')
|
(string) $app->config('app.integrations.secret', '')
|
||||||
);
|
);
|
||||||
|
$erliPullStatusMappingRepository = new ErliPullStatusMappingRepository($app->db());
|
||||||
|
$erliStatusMappingRepository = new ErliStatusMappingRepository($app->db());
|
||||||
$notificationRepository = new NotificationRepository($app->db());
|
$notificationRepository = new NotificationRepository($app->db());
|
||||||
$smsMessageRepository = new SmsMessageRepository($app->db());
|
$smsMessageRepository = new SmsMessageRepository($app->db());
|
||||||
$smsConversationService = new SmsConversationService(
|
$smsConversationService = new SmsConversationService(
|
||||||
@@ -389,8 +393,9 @@ return static function (Application $app): void {
|
|||||||
new ErliApiClient(),
|
new ErliApiClient(),
|
||||||
new OrderImportRepository($app->db()),
|
new OrderImportRepository($app->db()),
|
||||||
new OrdersRepository($app->db()),
|
new OrdersRepository($app->db()),
|
||||||
new ErliOrderMapper(),
|
new ErliOrderMapper($erliPullStatusMappingRepository),
|
||||||
$automationService
|
$automationService,
|
||||||
|
$erliPullStatusMappingRepository
|
||||||
);
|
);
|
||||||
$erliIntegrationController = new ErliIntegrationController(
|
$erliIntegrationController = new ErliIntegrationController(
|
||||||
$template,
|
$template,
|
||||||
@@ -400,7 +405,10 @@ return static function (Application $app): void {
|
|||||||
new ErliApiClient(),
|
new ErliApiClient(),
|
||||||
new IntegrationsRepository($app->db()),
|
new IntegrationsRepository($app->db()),
|
||||||
$cronRepository,
|
$cronRepository,
|
||||||
$erliOrdersSyncService
|
$erliOrdersSyncService,
|
||||||
|
$app->orderStatuses(),
|
||||||
|
$erliStatusMappingRepository,
|
||||||
|
$erliPullStatusMappingRepository
|
||||||
);
|
);
|
||||||
$allegroIntegrationController = new AllegroIntegrationController(
|
$allegroIntegrationController = new AllegroIntegrationController(
|
||||||
$template,
|
$template,
|
||||||
@@ -650,6 +658,8 @@ return static function (Application $app): void {
|
|||||||
$router->post('/settings/integrations/erli/save', [$erliIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/erli/save', [$erliIntegrationController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/erli/test', [$erliIntegrationController, 'test'], [$authMiddleware]);
|
$router->post('/settings/integrations/erli/test', [$erliIntegrationController, 'test'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/erli/import', [$erliIntegrationController, 'importNow'], [$authMiddleware]);
|
$router->post('/settings/integrations/erli/import', [$erliIntegrationController, 'importNow'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/erli/statuses/save-pull', [$erliIntegrationController, 'savePullStatusMappings'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/erli/statuses/save-push', [$erliIntegrationController, 'savePushStatusMappings'], [$authMiddleware]);
|
||||||
$router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]);
|
$router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]);
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ use App\Modules\Settings\ErliIntegrationRepository;
|
|||||||
use App\Modules\Settings\ErliOrderMapper;
|
use App\Modules\Settings\ErliOrderMapper;
|
||||||
use App\Modules\Settings\ErliOrderSyncStateRepository;
|
use App\Modules\Settings\ErliOrderSyncStateRepository;
|
||||||
use App\Modules\Settings\ErliOrdersSyncService;
|
use App\Modules\Settings\ErliOrdersSyncService;
|
||||||
|
use App\Modules\Settings\ErliPullStatusMappingRepository;
|
||||||
|
use App\Modules\Settings\ErliStatusMappingRepository;
|
||||||
|
use App\Modules\Settings\ErliStatusSyncService;
|
||||||
use App\Modules\Settings\InpostIntegrationRepository;
|
use App\Modules\Settings\InpostIntegrationRepository;
|
||||||
use App\Modules\Settings\IntegrationSecretCipher;
|
use App\Modules\Settings\IntegrationSecretCipher;
|
||||||
use App\Modules\Settings\ReceiptConfigRepository;
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
@@ -133,14 +136,20 @@ final class CronHandlerFactory
|
|||||||
$this->db,
|
$this->db,
|
||||||
$automationService
|
$automationService
|
||||||
);
|
);
|
||||||
|
$erliIntegrationRepository = new ErliIntegrationRepository($this->db, $this->integrationSecret);
|
||||||
|
$erliSyncStateRepository = new ErliOrderSyncStateRepository($this->db);
|
||||||
|
$erliApiClient = new ErliApiClient();
|
||||||
|
$erliPullStatusMappingRepository = new ErliPullStatusMappingRepository($this->db);
|
||||||
|
$erliStatusMappingRepository = new ErliStatusMappingRepository($this->db);
|
||||||
$erliOrdersSyncService = new ErliOrdersSyncService(
|
$erliOrdersSyncService = new ErliOrdersSyncService(
|
||||||
new ErliIntegrationRepository($this->db, $this->integrationSecret),
|
$erliIntegrationRepository,
|
||||||
new ErliOrderSyncStateRepository($this->db),
|
$erliSyncStateRepository,
|
||||||
new ErliApiClient(),
|
$erliApiClient,
|
||||||
new OrderImportRepository($this->db),
|
new OrderImportRepository($this->db),
|
||||||
$ordersRepository,
|
$ordersRepository,
|
||||||
new ErliOrderMapper(),
|
new ErliOrderMapper($erliPullStatusMappingRepository),
|
||||||
$automationService
|
$automationService,
|
||||||
|
$erliPullStatusMappingRepository
|
||||||
);
|
);
|
||||||
|
|
||||||
return new CronRunner(
|
return new CronRunner(
|
||||||
@@ -178,6 +187,17 @@ final class CronHandlerFactory
|
|||||||
'erli_orders_import' => new ErliOrdersImportHandler(
|
'erli_orders_import' => new ErliOrdersImportHandler(
|
||||||
$erliOrdersSyncService
|
$erliOrdersSyncService
|
||||||
),
|
),
|
||||||
|
'erli_status_sync' => new ErliStatusSyncHandler(
|
||||||
|
new ErliStatusSyncService(
|
||||||
|
$cronRepository,
|
||||||
|
$erliIntegrationRepository,
|
||||||
|
$erliOrdersSyncService,
|
||||||
|
$erliApiClient,
|
||||||
|
$erliSyncStateRepository,
|
||||||
|
$erliStatusMappingRepository,
|
||||||
|
$this->db
|
||||||
|
)
|
||||||
|
),
|
||||||
'shipment_tracking_sync' => new ShipmentTrackingHandler(
|
'shipment_tracking_sync' => new ShipmentTrackingHandler(
|
||||||
new ShipmentTrackingRegistry([
|
new ShipmentTrackingRegistry([
|
||||||
new InpostTrackingService(
|
new InpostTrackingService(
|
||||||
|
|||||||
22
src/Modules/Cron/ErliStatusSyncHandler.php
Normal file
22
src/Modules/Cron/ErliStatusSyncHandler.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Cron;
|
||||||
|
|
||||||
|
use App\Modules\Settings\ErliStatusSyncService;
|
||||||
|
|
||||||
|
final class ErliStatusSyncHandler
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ErliStatusSyncService $syncService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function handle(array $payload): array
|
||||||
|
{
|
||||||
|
return $this->syncService->sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,8 +124,35 @@ final class ErliApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{0: string, 1: int, 2: ?string}
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, message: string}
|
||||||
*/
|
*/
|
||||||
|
public function updateOrderStatus(array $credentials, string $orderId, string $erliStatus): array
|
||||||
|
{
|
||||||
|
$baseUrl = rtrim(trim((string) ($credentials['base_url'] ?? '')), '/');
|
||||||
|
$apiKey = trim((string) ($credentials['api_key'] ?? ''));
|
||||||
|
$sourceOrderId = trim($orderId);
|
||||||
|
$status = trim($erliStatus);
|
||||||
|
if ($baseUrl === '' || $apiKey === '' || $sourceOrderId === '' || $status === '') {
|
||||||
|
return ['ok' => false, 'http_code' => 0, 'message' => 'Brak danych do aktualizacji statusu Erli.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$body, $httpCode, $curlError] = $this->httpRequest(
|
||||||
|
'PATCH',
|
||||||
|
$baseUrl . '/orders/' . rawurlencode($sourceOrderId) . '/status',
|
||||||
|
$apiKey,
|
||||||
|
['status' => $status]
|
||||||
|
);
|
||||||
|
if ($curlError !== null) {
|
||||||
|
return ['ok' => false, 'http_code' => $httpCode, 'message' => 'Blad polaczenia: ' . $curlError];
|
||||||
|
}
|
||||||
|
if ($httpCode < 200 || $httpCode >= 300) {
|
||||||
|
return ['ok' => false, 'http_code' => $httpCode, 'message' => $this->resolveFailureMessage($body, $httpCode)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => true, 'http_code' => $httpCode, 'message' => 'OK'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed>|null $payload
|
* @param array<string, mixed>|null $payload
|
||||||
* @return array{0: string, 1: int, 2: ?string}
|
* @return array{0: string, 1: int, 2: ?string}
|
||||||
@@ -151,9 +178,13 @@ final class ErliApiClient
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (strtoupper($method) === 'POST') {
|
$normalizedMethod = strtoupper($method);
|
||||||
|
if ($normalizedMethod === 'POST') {
|
||||||
$opts[CURLOPT_POST] = true;
|
$opts[CURLOPT_POST] = true;
|
||||||
$opts[CURLOPT_POSTFIELDS] = json_encode($payload ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$opts[CURLOPT_POSTFIELDS] = json_encode($payload ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
} elseif ($normalizedMethod === 'PATCH') {
|
||||||
|
$opts[CURLOPT_CUSTOMREQUEST] = 'PATCH';
|
||||||
|
$opts[CURLOPT_POSTFIELDS] = json_encode($payload ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
} else {
|
} else {
|
||||||
$opts[CURLOPT_HTTPGET] = true;
|
$opts[CURLOPT_HTTPGET] = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ final class ErliIntegrationController
|
|||||||
private const ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS = 300;
|
private const ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS = 300;
|
||||||
private const ORDERS_IMPORT_DEFAULT_PRIORITY = 40;
|
private const ORDERS_IMPORT_DEFAULT_PRIORITY = 40;
|
||||||
private const ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS = 3;
|
private const ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
private const STATUS_SYNC_JOB_TYPE = 'erli_status_sync';
|
||||||
|
private const STATUS_SYNC_DEFAULT_INTERVAL_SECONDS = 900;
|
||||||
|
private const STATUS_SYNC_DEFAULT_PRIORITY = 45;
|
||||||
|
private const STATUS_SYNC_DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
private const STATUS_SYNC_DIRECTION_PULL = 'erli_to_orderpro';
|
||||||
|
private const STATUS_SYNC_DIRECTION_PUSH = 'orderpro_to_erli';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Template $template,
|
private readonly Template $template,
|
||||||
@@ -30,20 +36,31 @@ final class ErliIntegrationController
|
|||||||
private readonly ErliApiClient $apiClient,
|
private readonly ErliApiClient $apiClient,
|
||||||
private readonly IntegrationsRepository $integrations,
|
private readonly IntegrationsRepository $integrations,
|
||||||
private readonly CronRepository $cronRepository,
|
private readonly CronRepository $cronRepository,
|
||||||
private readonly ErliOrdersSyncService $ordersSyncService
|
private readonly ErliOrdersSyncService $ordersSyncService,
|
||||||
|
private readonly OrderStatusRepository $orderStatuses,
|
||||||
|
private readonly ErliStatusMappingRepository $statusMappings,
|
||||||
|
private readonly ErliPullStatusMappingRepository $pullStatusMappings
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
|
$activeTab = $this->resolveTab((string) $request->input('tab', 'integration'));
|
||||||
|
|
||||||
$html = $this->template->render('settings/erli', [
|
$html = $this->template->render('settings/erli', [
|
||||||
'title' => $this->translator->get('settings.erli.title'),
|
'title' => $this->translator->get('settings.erli.title'),
|
||||||
'activeMenu' => 'settings',
|
'activeMenu' => 'settings',
|
||||||
'activeSettings' => 'integrations',
|
'activeSettings' => 'integrations',
|
||||||
'user' => $this->auth->user(),
|
'user' => $this->auth->user(),
|
||||||
'csrfToken' => Csrf::token(),
|
'csrfToken' => Csrf::token(),
|
||||||
|
'activeTab' => $activeTab,
|
||||||
'settings' => $this->repository->getSettings(),
|
'settings' => $this->repository->getSettings(),
|
||||||
'ordersImportIntervalMinutes' => $this->currentImportIntervalMinutes(),
|
'ordersImportIntervalMinutes' => $this->currentImportIntervalMinutes(),
|
||||||
|
'statusSyncDirection' => $this->currentStatusSyncDirection(),
|
||||||
|
'statusSyncIntervalMinutes' => $this->currentStatusSyncIntervalMinutes(),
|
||||||
|
'orderproStatuses' => $this->orderStatuses->listStatuses(),
|
||||||
|
'erliStatusMappings' => $this->statusMappings->listAll(),
|
||||||
|
'erliPullStatusMappings' => $this->pullStatusMappings->listAll(),
|
||||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
'testMessage' => (string) Flash::get('erli_test', ''),
|
'testMessage' => (string) Flash::get('erli_test', ''),
|
||||||
@@ -70,6 +87,7 @@ final class ErliIntegrationController
|
|||||||
'orders_fetch_start_date' => $this->validateStartDate((string) $request->input('orders_fetch_start_date', '')),
|
'orders_fetch_start_date' => $this->validateStartDate((string) $request->input('orders_fetch_start_date', '')),
|
||||||
]);
|
]);
|
||||||
$this->upsertImportSchedule($request);
|
$this->upsertImportSchedule($request);
|
||||||
|
$this->saveStatusSyncSettings($request);
|
||||||
Flash::set('settings_success', $this->translator->get('settings.erli.flash.saved'));
|
Flash::set('settings_success', $this->translator->get('settings.erli.flash.saved'));
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
Flash::set(
|
Flash::set(
|
||||||
@@ -81,6 +99,84 @@ final class ErliIntegrationController
|
|||||||
return Response::redirect($redirectTo);
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function savePullStatusMappings(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$erliCodes = $request->input('erli_status_code', []);
|
||||||
|
$erliNames = $request->input('erli_status_name', []);
|
||||||
|
$orderproCodes = $request->input('orderpro_status_code', []);
|
||||||
|
if (!is_array($erliCodes) || !is_array($erliNames) || !is_array($orderproCodes)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.erli.statuses.flash.save_failed'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings = [];
|
||||||
|
foreach ($erliCodes as $index => $rawErliCode) {
|
||||||
|
$erliCode = strtolower(trim((string) $rawErliCode));
|
||||||
|
if ($erliCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$mappings[] = [
|
||||||
|
'erli_status_code' => $erliCode,
|
||||||
|
'erli_status_name' => trim((string) ($erliNames[$index] ?? '')),
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($orderproCodes[$index] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pullStatusMappings->replaceAll($mappings);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.erli.statuses.flash.saved_pull'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.erli.statuses.flash.save_failed') . ' ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function savePushStatusMappings(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$erliCodes = $request->input('erli_status_code', []);
|
||||||
|
$erliNames = $request->input('erli_status_name', []);
|
||||||
|
$orderproCodes = $request->input('orderpro_status_code', []);
|
||||||
|
if (!is_array($erliCodes) || !is_array($erliNames) || !is_array($orderproCodes)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.erli.statuses.flash.save_failed'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings = [];
|
||||||
|
foreach ($erliCodes as $index => $rawErliCode) {
|
||||||
|
$erliCode = trim((string) $rawErliCode);
|
||||||
|
if ($erliCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$mappings[] = [
|
||||||
|
'erli_status_code' => $erliCode,
|
||||||
|
'erli_status_name' => trim((string) ($erliNames[$index] ?? '')),
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($orderproCodes[$index] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->statusMappings->replaceAll($mappings);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.erli.statuses.flash.saved_push'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.erli.statuses.flash.save_failed') . ' ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
public function importNow(Request $request): Response
|
public function importNow(Request $request): Response
|
||||||
{
|
{
|
||||||
$redirectTo = $this->resolveRedirect($request);
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
@@ -145,6 +241,16 @@ final class ErliIntegrationController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveTab(string $tab): string
|
||||||
|
{
|
||||||
|
$normalized = trim($tab);
|
||||||
|
if (in_array($normalized, ['integration', 'statuses', 'settings'], true)) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'integration';
|
||||||
|
}
|
||||||
|
|
||||||
private function validateStartDate(string $value): string
|
private function validateStartDate(string $value): string
|
||||||
{
|
{
|
||||||
$trimmed = trim($value);
|
$trimmed = trim($value);
|
||||||
@@ -173,6 +279,27 @@ final class ErliIntegrationController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function saveStatusSyncSettings(Request $request): void
|
||||||
|
{
|
||||||
|
$direction = trim((string) $request->input('status_sync_direction', self::STATUS_SYNC_DIRECTION_PULL));
|
||||||
|
if (!in_array($direction, [self::STATUS_SYNC_DIRECTION_PULL, self::STATUS_SYNC_DIRECTION_PUSH], true)) {
|
||||||
|
throw new IntegrationConfigException($this->translator->get('settings.erli.validation.status_sync_direction_invalid'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$minutes = max(1, min(1440, (int) $request->input('status_sync_interval_minutes', 15)));
|
||||||
|
$enabled = (string) $request->input('is_active', '') === '1';
|
||||||
|
$this->cronRepository->upsertSetting('erli_status_sync_direction', $direction);
|
||||||
|
$this->cronRepository->upsertSetting('erli_status_sync_interval_minutes', (string) $minutes);
|
||||||
|
$this->cronRepository->upsertSchedule(
|
||||||
|
self::STATUS_SYNC_JOB_TYPE,
|
||||||
|
$minutes * 60,
|
||||||
|
self::STATUS_SYNC_DEFAULT_PRIORITY,
|
||||||
|
self::STATUS_SYNC_DEFAULT_MAX_ATTEMPTS,
|
||||||
|
null,
|
||||||
|
$enabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function currentImportIntervalMinutes(): int
|
private function currentImportIntervalMinutes(): int
|
||||||
{
|
{
|
||||||
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
||||||
@@ -186,6 +313,27 @@ final class ErliIntegrationController
|
|||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function currentStatusSyncDirection(): string
|
||||||
|
{
|
||||||
|
$direction = trim($this->cronRepository->getStringSetting('erli_status_sync_direction', self::STATUS_SYNC_DIRECTION_PULL));
|
||||||
|
return in_array($direction, [self::STATUS_SYNC_DIRECTION_PULL, self::STATUS_SYNC_DIRECTION_PUSH], true)
|
||||||
|
? $direction
|
||||||
|
: self::STATUS_SYNC_DIRECTION_PULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentStatusSyncIntervalMinutes(): int
|
||||||
|
{
|
||||||
|
foreach ($this->cronRepository->listSchedules() as $schedule) {
|
||||||
|
if ((string) ($schedule['job_type'] ?? '') !== self::STATUS_SYNC_JOB_TYPE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seconds = (int) ($schedule['interval_seconds'] ?? self::STATUS_SYNC_DEFAULT_INTERVAL_SECONDS);
|
||||||
|
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->cronRepository->getIntSetting('erli_status_sync_interval_minutes', 15, 1, 1440);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $result
|
* @param array<string, mixed> $result
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ final class ErliOrderMapper
|
|||||||
'orderSellerStatusChanged',
|
'orderSellerStatusChanged',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function __construct(private readonly ?ErliPullStatusMappingRepository $pullStatusMappings = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $message
|
* @param array<string, mixed> $message
|
||||||
* @return array{
|
* @return array{
|
||||||
@@ -299,6 +303,13 @@ final class ErliOrderMapper
|
|||||||
|
|
||||||
private function mapOrderStatus(string $status): string
|
private function mapOrderStatus(string $status): string
|
||||||
{
|
{
|
||||||
|
if ($this->pullStatusMappings !== null) {
|
||||||
|
$mapped = $this->pullStatusMappings->findMappedStatusCode($status);
|
||||||
|
if ($mapped !== null) {
|
||||||
|
return $mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return match ($status) {
|
return match ($status) {
|
||||||
'pending' => 'nieoplacone',
|
'pending' => 'nieoplacone',
|
||||||
'purchased' => 'nowe',
|
'purchased' => 'nowe',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final class ErliOrderSyncStateRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{last_synced_updated_at:?string,last_synced_source_order_id:?string,last_run_at:?string,last_success_at:?string,last_error:?string}
|
* @return array{last_synced_updated_at:?string,last_synced_source_order_id:?string,last_run_at:?string,last_success_at:?string,last_status_pushed_at:?string,last_error:?string}
|
||||||
*/
|
*/
|
||||||
public function getState(int $integrationId): array
|
public function getState(int $integrationId): array
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,7 @@ final class ErliOrderSyncStateRepository
|
|||||||
last_synced_source_order_id,
|
last_synced_source_order_id,
|
||||||
last_run_at,
|
last_run_at,
|
||||||
last_success_at,
|
last_success_at,
|
||||||
|
last_status_pushed_at,
|
||||||
last_error
|
last_error
|
||||||
FROM integration_order_sync_state
|
FROM integration_order_sync_state
|
||||||
WHERE integration_id = :integration_id
|
WHERE integration_id = :integration_id
|
||||||
@@ -50,6 +51,7 @@ final class ErliOrderSyncStateRepository
|
|||||||
'last_synced_source_order_id' => StringHelper::nullableString((string) ($row['last_synced_source_order_id'] ?? '')),
|
'last_synced_source_order_id' => StringHelper::nullableString((string) ($row['last_synced_source_order_id'] ?? '')),
|
||||||
'last_run_at' => StringHelper::nullableString((string) ($row['last_run_at'] ?? '')),
|
'last_run_at' => StringHelper::nullableString((string) ($row['last_run_at'] ?? '')),
|
||||||
'last_success_at' => StringHelper::nullableString((string) ($row['last_success_at'] ?? '')),
|
'last_success_at' => StringHelper::nullableString((string) ($row['last_success_at'] ?? '')),
|
||||||
|
'last_status_pushed_at' => StringHelper::nullableString((string) ($row['last_status_pushed_at'] ?? '')),
|
||||||
'last_error' => StringHelper::nullableString((string) ($row['last_error'] ?? '')),
|
'last_error' => StringHelper::nullableString((string) ($row['last_error'] ?? '')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -84,8 +86,42 @@ final class ErliOrderSyncStateRepository
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastStatusPushedAt(int $integrationId): ?string
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT last_status_pushed_at
|
||||||
|
FROM integration_order_sync_state
|
||||||
|
WHERE integration_id = :integration_id
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_string($value) ? StringHelper::nullableString($value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLastStatusPushedAt(int $integrationId, string $datetime): void
|
||||||
|
{
|
||||||
|
$normalized = StringHelper::normalizeDateTime($datetime);
|
||||||
|
if ($integrationId <= 0 || $normalized === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->upsertState($integrationId, [
|
||||||
|
'last_status_pushed_at' => $normalized,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{last_synced_updated_at:?string,last_synced_source_order_id:?string,last_run_at:?string,last_success_at:?string,last_error:?string}
|
* @return array{last_synced_updated_at:?string,last_synced_source_order_id:?string,last_run_at:?string,last_success_at:?string,last_status_pushed_at:?string,last_error:?string}
|
||||||
*/
|
*/
|
||||||
private function defaultState(): array
|
private function defaultState(): array
|
||||||
{
|
{
|
||||||
@@ -94,6 +130,7 @@ final class ErliOrderSyncStateRepository
|
|||||||
'last_synced_source_order_id' => null,
|
'last_synced_source_order_id' => null,
|
||||||
'last_run_at' => null,
|
'last_run_at' => null,
|
||||||
'last_success_at' => null,
|
'last_success_at' => null,
|
||||||
|
'last_status_pushed_at' => null,
|
||||||
'last_error' => null,
|
'last_error' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -119,6 +156,7 @@ final class ErliOrderSyncStateRepository
|
|||||||
'last_error',
|
'last_error',
|
||||||
'last_synced_order_updated_at',
|
'last_synced_order_updated_at',
|
||||||
'last_synced_source_order_id',
|
'last_synced_source_order_id',
|
||||||
|
'last_status_pushed_at',
|
||||||
];
|
];
|
||||||
if (!in_array($column, $allowed, true)) {
|
if (!in_array($column, $allowed, true)) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ final class ErliOrdersSyncService
|
|||||||
private readonly OrderImportRepository $orderImportRepository,
|
private readonly OrderImportRepository $orderImportRepository,
|
||||||
private readonly OrdersRepository $ordersRepository,
|
private readonly OrdersRepository $ordersRepository,
|
||||||
private readonly ErliOrderMapper $mapper,
|
private readonly ErliOrderMapper $mapper,
|
||||||
private readonly ?AutomationService $automationService = null
|
private readonly ?AutomationService $automationService = null,
|
||||||
|
private readonly ?ErliPullStatusMappingRepository $pullStatusMappings = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ final class ErliOrdersSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$this->discoverStatus($message);
|
||||||
$mapped = $this->mapper->mapInboxMessage($integrationId, $message);
|
$mapped = $this->mapper->mapInboxMessage($integrationId, $message);
|
||||||
if ($mapped === null) {
|
if ($mapped === null) {
|
||||||
$result['skipped'] = (int) $result['skipped'] + 1;
|
$result['skipped'] = (int) $result['skipped'] + 1;
|
||||||
@@ -172,6 +174,25 @@ final class ErliOrdersSyncService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $message
|
||||||
|
*/
|
||||||
|
private function discoverStatus(array $message): void
|
||||||
|
{
|
||||||
|
if ($this->pullStatusMappings === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = is_array($message['payload'] ?? null) ? $message['payload'] : [];
|
||||||
|
$rawStatus = strtolower(trim((string) ($payload['status'] ?? '')));
|
||||||
|
if ($rawStatus === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusName = trim((string) ($payload['statusName'] ?? $payload['statusLabel'] ?? $rawStatus));
|
||||||
|
$this->pullStatusMappings->upsertDiscoveredStatus($rawStatus, $statusName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $mapped
|
* @param array<string, mixed> $mapped
|
||||||
* @param array<string, mixed> $save
|
* @param array<string, mixed> $save
|
||||||
|
|||||||
148
src/Modules/Settings/ErliPullStatusMappingRepository.php
Normal file
148
src/Modules/Settings/ErliPullStatusMappingRepository.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Support\StringHelper;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final class ErliPullStatusMappingRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{erli_status_code:string,erli_status_name:string,orderpro_status_code:string}>
|
||||||
|
*/
|
||||||
|
public function listAll(): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->query(
|
||||||
|
'SELECT erli_status_code, erli_status_name, orderpro_status_code
|
||||||
|
FROM erli_order_status_pull_mappings
|
||||||
|
ORDER BY erli_status_code ASC'
|
||||||
|
);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$erliCode = strtolower(trim((string) ($row['erli_status_code'] ?? '')));
|
||||||
|
if ($erliCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'erli_status_code' => $erliCode,
|
||||||
|
'erli_status_name' => trim((string) ($row['erli_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findMappedStatusCode(string $erliStatusCode): ?string
|
||||||
|
{
|
||||||
|
$code = strtolower(trim($erliStatusCode));
|
||||||
|
if ($code === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT orderpro_status_code
|
||||||
|
FROM erli_order_status_pull_mappings
|
||||||
|
WHERE erli_status_code = :erli_status_code
|
||||||
|
AND orderpro_status_code IS NOT NULL
|
||||||
|
AND orderpro_status_code <> ""
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['erli_status_code' => $code]);
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapped = strtolower(trim($value));
|
||||||
|
return $mapped !== '' ? $mapped : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upsertDiscoveredStatus(string $erliStatusCode, ?string $erliStatusName = null): void
|
||||||
|
{
|
||||||
|
$code = strtolower(trim($erliStatusCode));
|
||||||
|
if ($code === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->pdo->prepare(
|
||||||
|
'SELECT id FROM erli_order_status_pull_mappings
|
||||||
|
WHERE erli_status_code = :erli_status_code
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$existing->execute(['erli_status_code' => $code]);
|
||||||
|
|
||||||
|
if ($existing->fetchColumn() !== false) {
|
||||||
|
if ($erliStatusName !== null && trim($erliStatusName) !== '') {
|
||||||
|
$update = $this->pdo->prepare(
|
||||||
|
'UPDATE erli_order_status_pull_mappings
|
||||||
|
SET erli_status_name = :erli_status_name,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE erli_status_code = :erli_status_code
|
||||||
|
AND (erli_status_name IS NULL OR erli_status_name = "")'
|
||||||
|
);
|
||||||
|
$update->execute([
|
||||||
|
'erli_status_name' => trim($erliStatusName),
|
||||||
|
'erli_status_code' => $code,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insert = $this->pdo->prepare(
|
||||||
|
'INSERT INTO erli_order_status_pull_mappings (
|
||||||
|
erli_status_code, erli_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:erli_status_code, :erli_status_name, NULL, NOW(), NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
$insert->execute([
|
||||||
|
'erli_status_code' => $code,
|
||||||
|
'erli_status_name' => StringHelper::nullableString((string) $erliStatusName),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{erli_status_code:string,erli_status_name:string,orderpro_status_code:string}> $mappings
|
||||||
|
*/
|
||||||
|
public function replaceAll(array $mappings): void
|
||||||
|
{
|
||||||
|
$this->pdo->exec('DELETE FROM erli_order_status_pull_mappings');
|
||||||
|
|
||||||
|
if ($mappings === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO erli_order_status_pull_mappings (
|
||||||
|
erli_status_code, erli_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:erli_status_code, :erli_status_name, :orderpro_status_code, NOW(), NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($mappings as $mapping) {
|
||||||
|
$erliCode = strtolower(trim((string) ($mapping['erli_status_code'] ?? '')));
|
||||||
|
if ($erliCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement->execute([
|
||||||
|
'erli_status_code' => $erliCode,
|
||||||
|
'erli_status_name' => StringHelper::nullableString((string) ($mapping['erli_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => StringHelper::nullableString(strtolower(trim((string) ($mapping['orderpro_status_code'] ?? '')))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/Modules/Settings/ErliStatusMappingRepository.php
Normal file
117
src/Modules/Settings/ErliStatusMappingRepository.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Support\StringHelper;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final class ErliStatusMappingRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{erli_status_code:string,erli_status_name:string,orderpro_status_code:string}>
|
||||||
|
*/
|
||||||
|
public function listAll(): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->query(
|
||||||
|
'SELECT erli_status_code, erli_status_name, orderpro_status_code
|
||||||
|
FROM erli_order_status_mappings
|
||||||
|
ORDER BY erli_status_code ASC'
|
||||||
|
);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$code = trim((string) ($row['erli_status_code'] ?? ''));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'erli_status_code' => $code,
|
||||||
|
'erli_status_name' => trim((string) ($row['erli_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> orderpro_status_code => erli_status_code
|
||||||
|
*/
|
||||||
|
public function buildOrderproToErliMap(): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->query(
|
||||||
|
'SELECT orderpro_status_code, erli_status_code
|
||||||
|
FROM erli_order_status_mappings
|
||||||
|
WHERE orderpro_status_code IS NOT NULL
|
||||||
|
AND orderpro_status_code <> ""
|
||||||
|
AND erli_status_code IS NOT NULL
|
||||||
|
AND erli_status_code <> ""
|
||||||
|
ORDER BY id ASC'
|
||||||
|
);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$orderproCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? '')));
|
||||||
|
$erliCode = trim((string) ($row['erli_status_code'] ?? ''));
|
||||||
|
if ($orderproCode === '' || $erliCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($map[$orderproCode])) {
|
||||||
|
$map[$orderproCode] = $erliCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{erli_status_code:string,erli_status_name:string,orderpro_status_code:string}> $mappings
|
||||||
|
*/
|
||||||
|
public function replaceAll(array $mappings): void
|
||||||
|
{
|
||||||
|
$this->pdo->exec('UPDATE erli_order_status_mappings SET orderpro_status_code = NULL, updated_at = NOW()');
|
||||||
|
|
||||||
|
if ($mappings === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO erli_order_status_mappings (
|
||||||
|
erli_status_code, erli_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:erli_status_code, :erli_status_name, :orderpro_status_code, NOW(), NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
erli_status_name = VALUES(erli_status_name),
|
||||||
|
orderpro_status_code = VALUES(orderpro_status_code),
|
||||||
|
updated_at = VALUES(updated_at)'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($mappings as $mapping) {
|
||||||
|
$erliCode = trim((string) ($mapping['erli_status_code'] ?? ''));
|
||||||
|
if ($erliCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement->execute([
|
||||||
|
'erli_status_code' => $erliCode,
|
||||||
|
'erli_status_name' => StringHelper::nullableString((string) ($mapping['erli_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => StringHelper::nullableString(strtolower(trim((string) ($mapping['orderpro_status_code'] ?? '')))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/Modules/Settings/ErliStatusSyncService.php
Normal file
223
src/Modules/Settings/ErliStatusSyncService.php
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Constants\IntegrationSources;
|
||||||
|
use App\Modules\Cron\CronRepository;
|
||||||
|
use PDO;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ErliStatusSyncService
|
||||||
|
{
|
||||||
|
private const DIRECTION_ERLI_TO_ORDERPRO = 'erli_to_orderpro';
|
||||||
|
private const DIRECTION_ORDERPRO_TO_ERLI = 'orderpro_to_erli';
|
||||||
|
private const MAX_ORDERS_PER_RUN = 50;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly CronRepository $cronRepository,
|
||||||
|
private readonly ErliIntegrationRepository $integrationRepository,
|
||||||
|
private readonly ErliOrdersSyncService $ordersSyncService,
|
||||||
|
private readonly ErliApiClient $apiClient,
|
||||||
|
private readonly ErliOrderSyncStateRepository $syncStateRepository,
|
||||||
|
private readonly ErliStatusMappingRepository $statusMappings,
|
||||||
|
private readonly PDO $pdo
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function sync(): array
|
||||||
|
{
|
||||||
|
$direction = $this->resolveDirection();
|
||||||
|
if ($direction === self::DIRECTION_ORDERPRO_TO_ERLI) {
|
||||||
|
return $this->syncPushDirection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->syncPullDirection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function syncPullDirection(): array
|
||||||
|
{
|
||||||
|
$result = $this->ordersSyncService->sync([
|
||||||
|
'ignore_orders_fetch_enabled' => true,
|
||||||
|
'max_messages' => 200,
|
||||||
|
]);
|
||||||
|
$result['ok'] = ((int) ($result['failed'] ?? 0)) === 0;
|
||||||
|
$result['direction'] = self::DIRECTION_ERLI_TO_ORDERPRO;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function syncPushDirection(): array
|
||||||
|
{
|
||||||
|
$credentials = $this->integrationRepository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
return $this->emptyPushResult(false, 'Brak aktywnej konfiguracji Erli.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$integrationId = (int) ($credentials['integration_id'] ?? 0);
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return $this->emptyPushResult(false, 'Brak aktywnego ID integracji Erli.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reverseMap = $this->statusMappings->buildOrderproToErliMap();
|
||||||
|
if ($reverseMap === []) {
|
||||||
|
return $this->emptyPushResult(true, 'Brak mapowan statusow orderPRO -> Erli.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastStatusPushedAt = $this->syncStateRepository->getLastStatusPushedAt($integrationId);
|
||||||
|
$orders = $this->findOrdersForPush($integrationId, $lastStatusPushedAt);
|
||||||
|
if ($orders === []) {
|
||||||
|
return $this->emptyPushResult(true, 'Brak zamowien do synchronizacji statusow.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'ok' => true,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_ERLI,
|
||||||
|
'pushed' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'errors' => [],
|
||||||
|
];
|
||||||
|
$latestPushedChangeAt = null;
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$sourceOrderId = trim((string) ($order['source_order_id'] ?? ''));
|
||||||
|
$orderproStatusCode = strtolower(trim((string) ($order['orderpro_status_code'] ?? '')));
|
||||||
|
if ($sourceOrderId === '' || $orderproStatusCode === '') {
|
||||||
|
$result['skipped']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$erliStatusCode = $reverseMap[$orderproStatusCode] ?? null;
|
||||||
|
if ($erliStatusCode === null || trim($erliStatusCode) === '') {
|
||||||
|
$result['skipped']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$apiResult = $this->apiClient->updateOrderStatus($credentials, $sourceOrderId, $erliStatusCode);
|
||||||
|
if (($apiResult['ok'] ?? false) !== true) {
|
||||||
|
$result['failed']++;
|
||||||
|
$this->appendError($result, [
|
||||||
|
'source_order_id' => $sourceOrderId,
|
||||||
|
'orderpro_status_code' => $orderproStatusCode,
|
||||||
|
'erli_status_code' => $erliStatusCode,
|
||||||
|
'error' => (string) ($apiResult['message'] ?? 'Blad API Erli.'),
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['pushed']++;
|
||||||
|
$changeAt = trim((string) ($order['latest_change'] ?? ''));
|
||||||
|
if ($changeAt !== '' && ($latestPushedChangeAt === null || $changeAt > $latestPushedChangeAt)) {
|
||||||
|
$latestPushedChangeAt = $changeAt;
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$result['failed']++;
|
||||||
|
$this->appendError($result, [
|
||||||
|
'source_order_id' => $sourceOrderId,
|
||||||
|
'orderpro_status_code' => $orderproStatusCode,
|
||||||
|
'erli_status_code' => $erliStatusCode,
|
||||||
|
'error' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($latestPushedChangeAt !== null) {
|
||||||
|
$this->syncStateRepository->updateLastStatusPushedAt($integrationId, $latestPushedChangeAt);
|
||||||
|
}
|
||||||
|
$result['ok'] = (int) $result['failed'] === 0;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function emptyPushResult(bool $ok, string $message): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ok' => $ok,
|
||||||
|
'direction' => self::DIRECTION_ORDERPRO_TO_ERLI,
|
||||||
|
'pushed' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'message' => $message,
|
||||||
|
'errors' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{order_id:int,source_order_id:string,orderpro_status_code:string,latest_change:string}>
|
||||||
|
*/
|
||||||
|
private function findOrdersForPush(int $integrationId, ?string $lastStatusPushedAt): array
|
||||||
|
{
|
||||||
|
$sinceDate = $lastStatusPushedAt !== null && trim($lastStatusPushedAt) !== ''
|
||||||
|
? $lastStatusPushedAt
|
||||||
|
: date('Y-m-d H:i:s', strtotime('-24 hours'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT
|
||||||
|
o.id AS order_id,
|
||||||
|
o.source_order_id,
|
||||||
|
o.status_code AS orderpro_status_code,
|
||||||
|
MAX(h.changed_at) AS latest_change
|
||||||
|
FROM order_status_history h
|
||||||
|
INNER JOIN orders o ON o.id = h.order_id
|
||||||
|
WHERE o.source = :source
|
||||||
|
AND o.integration_id = :integration_id
|
||||||
|
AND h.change_source = :change_source
|
||||||
|
AND h.changed_at > :since_date
|
||||||
|
GROUP BY o.id, o.source_order_id, o.status_code
|
||||||
|
ORDER BY latest_change ASC
|
||||||
|
LIMIT ' . self::MAX_ORDERS_PER_RUN
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'source' => IntegrationSources::ERLI,
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'change_source' => 'manual',
|
||||||
|
'since_date' => $sinceDate,
|
||||||
|
]);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveDirection(): string
|
||||||
|
{
|
||||||
|
$direction = trim($this->cronRepository->getStringSetting(
|
||||||
|
'erli_status_sync_direction',
|
||||||
|
self::DIRECTION_ERLI_TO_ORDERPRO
|
||||||
|
));
|
||||||
|
|
||||||
|
return in_array($direction, [self::DIRECTION_ERLI_TO_ORDERPRO, self::DIRECTION_ORDERPRO_TO_ERLI], true)
|
||||||
|
? $direction
|
||||||
|
: self::DIRECTION_ERLI_TO_ORDERPRO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $result
|
||||||
|
* @param array<string, mixed> $error
|
||||||
|
*/
|
||||||
|
private function appendError(array &$result, array $error): void
|
||||||
|
{
|
||||||
|
$errors = is_array($result['errors'] ?? null) ? $result['errors'] : [];
|
||||||
|
if (count($errors) < 20) {
|
||||||
|
$errors[] = $error;
|
||||||
|
}
|
||||||
|
$result['errors'] = $errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Tests\Unit;
|
|||||||
|
|
||||||
use App\Core\Constants\IntegrationSources;
|
use App\Core\Constants\IntegrationSources;
|
||||||
use App\Modules\Settings\ErliOrderMapper;
|
use App\Modules\Settings\ErliOrderMapper;
|
||||||
|
use App\Modules\Settings\ErliPullStatusMappingRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
@@ -51,6 +52,30 @@ final class ErliOrderMapperTest extends TestCase
|
|||||||
self::assertTrue($aggregate['order']['is_canceled_by_buyer']);
|
self::assertTrue($aggregate['order']['is_canceled_by_buyer']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testConfiguredPullMappingOverridesDefaultStatus(): void
|
||||||
|
{
|
||||||
|
$pullMappings = $this->createMock(ErliPullStatusMappingRepository::class);
|
||||||
|
$pullMappings
|
||||||
|
->method('findMappedStatusCode')
|
||||||
|
->with('purchased')
|
||||||
|
->willReturn('w_realizacji');
|
||||||
|
|
||||||
|
$mapper = new ErliOrderMapper($pullMappings);
|
||||||
|
$aggregate = $mapper->mapInboxMessage(7, $this->message('purchased'));
|
||||||
|
|
||||||
|
self::assertIsArray($aggregate);
|
||||||
|
self::assertSame('w_realizacji', $aggregate['order']['status_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownStatusFallsBackToRawCodeForDiscoveryMappingLater(): void
|
||||||
|
{
|
||||||
|
$aggregate = $this->mapper->mapInboxMessage(7, $this->message('readyToProcess'));
|
||||||
|
|
||||||
|
self::assertIsArray($aggregate);
|
||||||
|
self::assertSame('readytoprocess', $aggregate['order']['status_code']);
|
||||||
|
self::assertSame('readytoprocess', $aggregate['order']['preferences_json']['erli_status_raw']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testCompanyInvoiceDataDetectsInvoiceRequest(): void
|
public function testCompanyInvoiceDataDetectsInvoiceRequest(): void
|
||||||
{
|
{
|
||||||
$message = $this->message('purchased');
|
$message = $this->message('purchased');
|
||||||
|
|||||||
200
tests/Unit/ErliStatusSyncServiceTest.php
Normal file
200
tests/Unit/ErliStatusSyncServiceTest.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Modules\Cron\CronRepository;
|
||||||
|
use App\Modules\Settings\ErliApiClient;
|
||||||
|
use App\Modules\Settings\ErliIntegrationRepository;
|
||||||
|
use App\Modules\Settings\ErliOrdersSyncService;
|
||||||
|
use App\Modules\Settings\ErliOrderSyncStateRepository;
|
||||||
|
use App\Modules\Settings\ErliStatusMappingRepository;
|
||||||
|
use App\Modules\Settings\ErliStatusSyncService;
|
||||||
|
use PDO;
|
||||||
|
use PDOStatement;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ErliStatusSyncServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private CronRepository&MockObject $cronRepository;
|
||||||
|
private ErliIntegrationRepository&MockObject $integrationRepository;
|
||||||
|
private ErliOrdersSyncService&MockObject $ordersSyncService;
|
||||||
|
private ErliApiClient&MockObject $apiClient;
|
||||||
|
private ErliOrderSyncStateRepository&MockObject $syncStateRepository;
|
||||||
|
private ErliStatusMappingRepository&MockObject $statusMappings;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->cronRepository = $this->createMock(CronRepository::class);
|
||||||
|
$this->integrationRepository = $this->createMock(ErliIntegrationRepository::class);
|
||||||
|
$this->ordersSyncService = $this->createMock(ErliOrdersSyncService::class);
|
||||||
|
$this->apiClient = $this->createMock(ErliApiClient::class);
|
||||||
|
$this->syncStateRepository = $this->createMock(ErliOrderSyncStateRepository::class);
|
||||||
|
$this->statusMappings = $this->createMock(ErliStatusMappingRepository::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPullDirectionDelegatesToOrdersInboxImport(): void
|
||||||
|
{
|
||||||
|
$this->cronRepository
|
||||||
|
->method('getStringSetting')
|
||||||
|
->willReturn('erli_to_orderpro');
|
||||||
|
|
||||||
|
$this->ordersSyncService
|
||||||
|
->expects($this->once())
|
||||||
|
->method('sync')
|
||||||
|
->with([
|
||||||
|
'ignore_orders_fetch_enabled' => true,
|
||||||
|
'max_messages' => 200,
|
||||||
|
])
|
||||||
|
->willReturn([
|
||||||
|
'failed' => 0,
|
||||||
|
'processed' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->createServiceWithRows([])->sync();
|
||||||
|
|
||||||
|
self::assertTrue($result['ok']);
|
||||||
|
self::assertSame('erli_to_orderpro', $result['direction']);
|
||||||
|
self::assertSame(2, $result['processed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPushDirectionProcessesMappedManualOrdersAndSkipsUnmapped(): void
|
||||||
|
{
|
||||||
|
$this->preparePushDefaults([
|
||||||
|
'w_realizacji' => 'inProgress',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->apiClient
|
||||||
|
->expects($this->exactly(2))
|
||||||
|
->method('updateOrderStatus')
|
||||||
|
->willReturn(['ok' => true, 'http_code' => 204, 'message' => 'OK']);
|
||||||
|
|
||||||
|
$this->syncStateRepository
|
||||||
|
->expects($this->once())
|
||||||
|
->method('updateLastStatusPushedAt')
|
||||||
|
->with(12, '2026-05-16 10:10:00');
|
||||||
|
|
||||||
|
$service = $this->createServiceWithRows([
|
||||||
|
['source_order_id' => 'E1', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:00:00'],
|
||||||
|
['source_order_id' => 'E2', 'orderpro_status_code' => 'nowe', 'latest_change' => '2026-05-16 10:05:00'],
|
||||||
|
['source_order_id' => 'E3', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:10:00'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $service->sync();
|
||||||
|
|
||||||
|
self::assertTrue($result['ok']);
|
||||||
|
self::assertSame('orderpro_to_erli', $result['direction']);
|
||||||
|
self::assertSame(2, $result['pushed']);
|
||||||
|
self::assertSame(1, $result['skipped']);
|
||||||
|
self::assertSame(0, $result['failed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPushFailureDoesNotAdvanceCursorPastFailedOrder(): void
|
||||||
|
{
|
||||||
|
$this->preparePushDefaults([
|
||||||
|
'w_realizacji' => 'inProgress',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->apiClient
|
||||||
|
->expects($this->exactly(2))
|
||||||
|
->method('updateOrderStatus')
|
||||||
|
->willReturnOnConsecutiveCalls(
|
||||||
|
['ok' => true, 'http_code' => 204, 'message' => 'OK'],
|
||||||
|
['ok' => false, 'http_code' => 500, 'message' => 'Erli error']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->syncStateRepository
|
||||||
|
->expects($this->once())
|
||||||
|
->method('updateLastStatusPushedAt')
|
||||||
|
->with(12, '2026-05-16 10:00:00');
|
||||||
|
|
||||||
|
$service = $this->createServiceWithRows([
|
||||||
|
['source_order_id' => 'E1', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:00:00'],
|
||||||
|
['source_order_id' => 'E2', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:10:00'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $service->sync();
|
||||||
|
|
||||||
|
self::assertFalse($result['ok']);
|
||||||
|
self::assertSame(1, $result['pushed']);
|
||||||
|
self::assertSame(1, $result['failed']);
|
||||||
|
self::assertCount(1, $result['errors']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPushDirectionReturnsEarlyWithoutCredentials(): void
|
||||||
|
{
|
||||||
|
$this->cronRepository
|
||||||
|
->method('getStringSetting')
|
||||||
|
->willReturn('orderpro_to_erli');
|
||||||
|
|
||||||
|
$this->integrationRepository
|
||||||
|
->method('getCredentials')
|
||||||
|
->willReturn(null);
|
||||||
|
|
||||||
|
$this->apiClient
|
||||||
|
->expects($this->never())
|
||||||
|
->method('updateOrderStatus');
|
||||||
|
|
||||||
|
$result = $this->createServiceWithRows([])->sync();
|
||||||
|
|
||||||
|
self::assertFalse($result['ok']);
|
||||||
|
self::assertSame(0, $result['pushed']);
|
||||||
|
self::assertSame('orderpro_to_erli', $result['direction']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $map
|
||||||
|
*/
|
||||||
|
private function preparePushDefaults(array $map): void
|
||||||
|
{
|
||||||
|
$this->cronRepository
|
||||||
|
->method('getStringSetting')
|
||||||
|
->willReturn('orderpro_to_erli');
|
||||||
|
$this->integrationRepository
|
||||||
|
->method('getCredentials')
|
||||||
|
->willReturn([
|
||||||
|
'integration_id' => 12,
|
||||||
|
'base_url' => 'https://erli.test',
|
||||||
|
'api_key' => 'token',
|
||||||
|
'timeout_seconds' => 15,
|
||||||
|
'orders_fetch_enabled' => true,
|
||||||
|
'orders_fetch_start_date' => null,
|
||||||
|
]);
|
||||||
|
$this->syncStateRepository
|
||||||
|
->method('getLastStatusPushedAt')
|
||||||
|
->willReturn(null);
|
||||||
|
$this->statusMappings
|
||||||
|
->method('buildOrderproToErliMap')
|
||||||
|
->willReturn($map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $rows
|
||||||
|
*/
|
||||||
|
private function createServiceWithRows(array $rows): ErliStatusSyncService
|
||||||
|
{
|
||||||
|
$statement = $this->createMock(PDOStatement::class);
|
||||||
|
$statement
|
||||||
|
->method('execute')
|
||||||
|
->willReturn(true);
|
||||||
|
$statement
|
||||||
|
->method('fetchAll')
|
||||||
|
->willReturn($rows);
|
||||||
|
|
||||||
|
$pdo = $this->createMock(PDO::class);
|
||||||
|
$pdo
|
||||||
|
->method('prepare')
|
||||||
|
->willReturn($statement);
|
||||||
|
|
||||||
|
return new ErliStatusSyncService(
|
||||||
|
$this->cronRepository,
|
||||||
|
$this->integrationRepository,
|
||||||
|
$this->ordersSyncService,
|
||||||
|
$this->apiClient,
|
||||||
|
$this->syncStateRepository,
|
||||||
|
$this->statusMappings,
|
||||||
|
$pdo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user