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:
2026-05-16 00:27:08 +02:00
parent c127ebf04d
commit 7972bb9fa4
28 changed files with 2021 additions and 57 deletions

View File

@@ -39,7 +39,7 @@ HTTP Request
| **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export |
| **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments |
| **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 |
| **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 |
@@ -74,7 +74,7 @@ HTTP Request
### Order Lifecycle
1. **Import** — Cron handler → API client → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()`
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
1. **Request**`/statistics/summary``OrdersStatisticsController::summary()`
@@ -112,6 +112,7 @@ HTTP Request
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
| `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 |
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses |
| `ShipmentTrackingHandler` | Poll carrier tracking APIs |
@@ -120,11 +121,12 @@ HTTP Request
### 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_*`.
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.
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
@@ -184,25 +186,33 @@ tests/
### ErliApiClient (`src/Modules/Settings/ErliApiClient.php`)
- `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 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)`.
- 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).
### 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`.
- `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()`.
- 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, 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.
### 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`.
- 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`.
- Automatyzacje: `order.imported` dla nowych zamowien i `payment.status_changed` przy tranzycji platnosci na re-imporcie.
### ErliOrdersImportHandler (`src/Modules/Cron/ErliOrdersImportHandler.php`)
- 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
- Dodaje wiersz Erli do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.

View File

@@ -374,6 +374,7 @@ UNIQUE: `(integration_id, shoppro_status_code)`
| `last_synced_external_order_id` | VARCHAR(128) | YES | Legacy/source-specific cursor |
| `last_run_at` | DATETIME | YES | |
| `last_success_at` | DATETIME | YES | |
| `last_status_pushed_at` | DATETIME | YES | Erli status push cursor for manual local status changes |
| `last_error` | VARCHAR(500) | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
@@ -524,6 +525,26 @@ UNIQUE: `(type, name)`
| `created_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
| Column | Type | Notes |
|--------|------|-------|
@@ -952,7 +973,7 @@ Index: `(status, priority, scheduled_at)`
| `created_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).
---

View File

@@ -1,5 +1,32 @@
# 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
**Co zrobiono:**