feat(133): erli cross-surface parity
Phase 133 complete: - Add shared order source registry for Allegro, shopPRO, and Erli - Include Erli in order filters, statistics, automation integration filters, and integration menu state - Document verification gaps for missing PHPUnit vendor and sonar-scanner
This commit is contained in:
@@ -13,8 +13,8 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 3.8.0-dev |
|
||||
| Status | v3.8 Erli Marketplace Integration complete in code — Phases 127-132 shipped; live Erli smoke/migrations remain operator follow-up |
|
||||
| Last Updated | 2026-05-16 (Phase 132 closed) |
|
||||
| Status | v3.8 Erli Marketplace Integration complete in code - Phases 127-133 shipped; live Erli smoke/migrations remain operator follow-up |
|
||||
| Last Updated | 2026-05-16 (Phase 133 closed) |
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -132,6 +132,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Przesylki Erli: zakladka mapowania dostaw, etykiety przez lokalne providery InPost/Apaczka i rejestracja paczek zewnetrznych w Erli przez `POST /shipping/external` — Phase 130
|
||||
- [x] Tracking i automatyzacje Erli: lokalny provider tracking jak w Allegro, retry niekrytycznej rejestracji paczki zewnetrznej Erli z `shipment_tracking_sync`, wspolny kontekst `shipment.created`/`shipment.status_changed` dla regul e-mail/SMS/statystyk — Phase 131
|
||||
- [x] Hardening Erli: spojna diagnostyka importu/ACK w `integration_order_sync_state.last_error`, brak ACK po blednym batchu, testy jednostkowe import/status sync i dokumentacja obserwowalnosci bez nowej migracji — Phase 132
|
||||
- [x] Parytet Erli w powierzchniach wspolnych: filtr zrodla zamowien, kanaly statystyk dziennych/podsumowania, warunek integracji automatyzacji, menu integracji i etykiety `zrodlo` uzywaja wspolnego rejestru zrodel — Phase 133
|
||||
- [x] Integracja polkurier.pl (fundament): pojedyncza globalna konfiguracja w `/settings/integrations/polkurier`, szyfrowany Token API + login, karta w hubie integracji obok Apaczki i realny test polaczenia przez `apimetod=test_auth_api` zweryfikowany na zywym koncie operatora; `ShipmentProviderRegistry` netkniety — `PolkurierShipmentService/TrackingService` w kolejnych fazach — Phase 127
|
||||
- [x] polkurier ShipmentService + TrackingService + UI prepare panel: pelen kontrakt API (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers), `PolkurierShipmentService` implementujacy `ShipmentProviderInterface` z normalizacja shipmenttype (lowercase) i splitem ulicy na street/housenumber/flatnumber, `PolkurierTrackingService` mapujacy statusy O/P/A/WP/D/Z/W na znormalizowane, panel "polkurier" w `prepare.php` z dynamiczna lista uslug z `available_carriers`, seed migracja `delivery_status_mappings(provider='polkurier')` z 7 wpisami z PDF v1.11; live test na #114/#115 zakonczony sukcesem po 4 iteracjach (ReferenceError → uppercase shipmenttype → orderno parsing → A4/A6); rozmiar etykiety sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — Phase 128
|
||||
- [x] Order User Notes module (Phase 129): pelen CRUD notatek autorskich operatora per zamowienie. Reuse `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) + `author_name` (snapshot) + indeks `idx_order_notes_type_order`. `OrderNotesService` z autoryzacja DB-level (`WHERE user_id = :user_id`, rowCount=0 ⇒ 403). Sekcja `#notes` w "Wiadomosci i zalaczniki" w `/orders/{id}` z inline edit form + delete przez `OrderProAlerts.confirm`. Badge `[N]` (indigo neutralny) przy nr zamowienia na `/orders/list` (subquery `user_notes_count` w paginate). Brak admin override (brak systemu rol w aplikacji) — edit/delete tylko dla autora — Phase 129
|
||||
@@ -259,6 +260,7 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
||||
| Erli etykiety uzywaja lokalnych providerow, a Erli dostaje paczke zewnetrzna przez `POST /shipping/external` | Operator nie chce nadawac na umowie Erli; API wspiera zewnetrzne paczki/tracking | 2026-05-16 | Active |
|
||||
| `carrier_delivery_method_mappings` przechowuje `source_vendor_code`/`source_service_id` dla Erli | Vendor Erli i lokalny provider to osobne kontrakty, nie nalezy ich mieszac w polach Apaczki/InPost | 2026-05-16 | Active |
|
||||
| Erli hardening uzywa istniejacych powierzchni obserwowalnosci zamiast nowej tabeli logow | Operator wybral ujednolicenie istniejacych miejsc; `integration_order_sync_state.last_error`, wynik crona i activity log wystarczaja dla Phase 132 | 2026-05-16 | Active |
|
||||
| Zrodla zamowien marketplace maja wspolny `OrderSourceRegistry` | Parytet Erli ma byc utrzymany wszedzie tam, gdzie kod potrzebuje listy lub etykiety zrodla; lokalne pary Allegro/shopPRO prowadzily do pominiec Erli | 2026-05-16 | Active |
|
||||
| polkurier startuje jako jedna globalna konfiguracja (single-instance, mirror Apaczka/HostedSMS/SMSPLANET) z realnym testowym wywolaniem `apimetod=test_auth_api` | Operator ma jedno konto polkurier; fundament musi byc zweryfikowany na zywym API zanim dolozymy `PolkurierShipmentService` | 2026-05-14 | Active |
|
||||
| polkurier wymaga `login + token` razem w body `authorization` (nie samego tokena) | Zweryfikowane w SDK polkurier-sdk (`Auth.php`/`Request.php`); kolumna `login VARCHAR(190)` w `polkurier_integration_settings` mimo ze PLAN tego nie wymagal — kontrakt API to dyktuje | 2026-05-14 | Active |
|
||||
| polkurier API: top-level `status` === `'success'` (nie `'ok'`), tresc bledu w polu `response` envelope'a | `ResponseStatus::SUCCESS = 'success'` z `src/Type/ResponseStatus.php` SDK; bledy rzucane przez `ErrorException($response->get('response'))` w `PolkurierWebService.php`. Pattern dla wszystkich przyszlych metod polkurier API (`createShipment`, `getLabel`, `getStatus`, `cancelOrder`, etc.) | 2026-05-14 | Active |
|
||||
@@ -306,6 +308,6 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-05-16 after Phase 132 (Erli Hardening, Observability + Docs) closure; v3.8 milestone complete in code*
|
||||
*Last updated: 2026-05-16 after Phase 133 (Erli Cross-Surface Parity) closure; v3.8 milestone complete in code*
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
|
||||
|
||||
## Current Milestone
|
||||
|
||||
v3.8 Erli Marketplace Integration — Complete in code
|
||||
v3.8 Erli Marketplace Integration - Complete in code
|
||||
|
||||
Pelna integracja z erli.pl wzorowana na istniejacej integracji Allegro: konfiguracja konta/API, pobieranie zamowien, mapowanie i synchronizacja statusow, generowanie etykiet, tracking oraz wlaczenie Erli w istniejace przeplywy automatyzacji, statystyk i obslugi zamowien.
|
||||
|
||||
Progress: 6 of 6 phases complete (100%).
|
||||
Progress: 7 of 7 phases complete (100%).
|
||||
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
@@ -20,6 +20,7 @@ Progress: 6 of 6 phases complete (100%).
|
||||
| 130 | Erli Shipments + Labels | 1/1 | Complete (2026-05-16; migration/manual Erli shipping smoke pending operator) |
|
||||
| 131 | Erli Tracking + Automation Hooks | 1/1 | Complete (2026-05-16; manual Erli tracking/automation smoke pending operator) |
|
||||
| 132 | Erli Hardening, Observability + Docs | 1/1 | Complete (2026-05-16; PHPUnit/Sonar env gaps documented) |
|
||||
| 133 | Erli Cross-Surface Parity | 1/1 | Complete (2026-05-16; PHPUnit/Sonar env gaps documented) |
|
||||
|
||||
### Phase 127: Erli Integration Foundation
|
||||
|
||||
@@ -51,6 +52,11 @@ Plans: 131-01 (complete)
|
||||
Focus: Testy jednostkowe mapperow/klientow, logi integracji i bledow API, retry/idempotencja, manual smoke checklist na zywej konfiguracji oraz aktualizacja `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md`.
|
||||
Plans: 132-01 (complete)
|
||||
|
||||
### Phase 133: Erli Cross-Surface Parity
|
||||
|
||||
Focus: Domknac Erli jako pelnoprawny kanal w istniejacych wspolnych powierzchniach: filtr `Zrodlo` na liscie zamowien, kanaly sprzedazy w `/statistics/orders` i `/statistics/summary`, warunki integracji w automatyzacjach oraz aktywne menu integracji. Wprowadzic maly wspolny rejestr zrodel, zeby ograniczyc kolejne lokalne pominiecia Erli.
|
||||
Plans: 133-01 (complete)
|
||||
|
||||
## Previous Milestone (transition pending)
|
||||
|
||||
v3.7 Invoices (Fakturownia integration) — Complete in code, transition/follow-ups pending
|
||||
@@ -561,4 +567,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-05-16 - Phase 132 complete; v3.8 complete in code*
|
||||
*Last updated: 2026-05-16 - Phase 133 complete; v3.8 complete in code*
|
||||
|
||||
@@ -10,37 +10,37 @@ See: .paul/PROJECT.md (updated 2026-05-16)
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.8 Erli Marketplace Integration
|
||||
Phase: 132 of 132 (Erli Hardening, Observability + Docs) - Complete
|
||||
Plan: 132-01 unified
|
||||
Phase: 133 of 133 (Erli Cross-Surface Parity) - Complete
|
||||
Plan: 133-01 unified
|
||||
Status: Loop complete; v3.8 complete in code
|
||||
Last activity: 2026-05-16 15:51 - Unified .paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md
|
||||
Last activity: 2026-05-16 16:22 - Unified .paul/phases/133-erli-cross-surface-parity/133-01-PLAN.md
|
||||
|
||||
Progress:
|
||||
- Milestone v3.8: [##########] 100% (Phases 127-132 complete in code)
|
||||
- Phase 132: [##########] 100% (complete)
|
||||
- Milestone v3.8: [##########] 100% (Phases 127-133 complete in code)
|
||||
- Phase 133: [##########] 100% (complete)
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN -> APPLY -> UNIFY
|
||||
done done done [Loop complete - ready for next milestone]
|
||||
done done done [Loop complete - ready for milestone completion or next milestone]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-05-16 15:51
|
||||
Stopped at: Phase 132 complete; v3.8 complete in code
|
||||
Last session: 2026-05-16 16:22
|
||||
Stopped at: Phase 133 complete; v3.8 complete in code
|
||||
Next action: Choose next milestone or run $paul-complete-milestone for v3.8 archival/release closure
|
||||
Resume file: .paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md
|
||||
Resume file: .paul/phases/133-erli-cross-surface-parity/133-01-SUMMARY.md
|
||||
|
||||
## Pending parallel work
|
||||
- None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1).
|
||||
|
||||
## Git State
|
||||
|
||||
Last phase commit: feat(132): erli hardening observability docs
|
||||
Previous: feat(131): erli tracking automation hooks
|
||||
Last phase commit: feat(133): erli cross-surface parity
|
||||
Previous: feat(132): erli hardening observability docs
|
||||
Branch: main
|
||||
|
||||
### Skill Audit (Phase 129)
|
||||
@@ -67,6 +67,12 @@ Branch: main
|
||||
|----------|---------|-------|
|
||||
| `sonar-scanner` | gap documented | Attempted after APPLY; CLI is not available in PATH. |
|
||||
|
||||
### Skill Audit (Phase 133)
|
||||
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| `sonar-scanner` | gap documented | Attempted after APPLY; CLI is not available in PATH. |
|
||||
|
||||
## Pending Actions
|
||||
|
||||
- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online).
|
||||
@@ -102,6 +108,9 @@ Branch: main
|
||||
- Phase 131 follow-up: manualny smoke po migracjach/konfiguracji Erli — utworz paczke dla zamowienia Erli z lokalnym providerem, potwierdz `shipment.created`, uruchom `shipment_tracking_sync`, sprawdz `shipment.status_changed` i retry `POST /shipping/external` tylko po pojawieniu sie tracking number.
|
||||
- Phase 132 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, a globalny XAMPP PHPUnit jest niekompatybilny z PHP (`each()` removed), wiec testy `ErliOrdersSyncServiceTest`, `ErliStatusSyncServiceTest` i `ErliOrderMapperTest` nie zostaly uruchomione przez PHPUnit; wykonano `php -l` i `git diff --check`.
|
||||
- Phase 132 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony.
|
||||
- Phase 133 follow-up: manualny smoke `/orders/list` -> filtr `Zrodlo` ma Allegro, shopPRO i Erli; `/statistics/orders` i `/statistics/summary` -> kanal Erli widoczny i liczony; edycja automatyzacji -> aktywna integracja Erli dostepna w warunku Integracja.
|
||||
- Phase 133 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec testy `OrderSourceRegistryTest` i `OrdersStatisticsRepositoryTest` nie zostaly uruchomione przez PHPUnit; wykonano `php -l`, ad-hoc SQLite smoke i `git diff --check`.
|
||||
- Phase 133 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony.
|
||||
- Phase 127 follow-up: zaplanowac kolejna faze polkurier — `PolkurierShipmentService` (CreateOrder + GetLabel + OrderValuationV2 + AvailableCarriers mapping + UI mapowan metod dostawy + presety przesylek) — fundament + zweryfikowany kontrakt API gotowy.
|
||||
- Phase 127 follow-up: drugi krok — `PolkurierTrackingService` + wpisy w `delivery_status_mappings` (provider='polkurier').
|
||||
- Phase 127 follow-up: po polkurier shipment service rozwazyc fazy paczkomaty (`InpostParcelMachines` / `PocztexPostOffices` / `Kurier48PostOffices` API juz dostepne w SDK polkuriera).
|
||||
@@ -123,4 +132,4 @@ Branch: main
|
||||
|
||||
## Skill Requirements
|
||||
|
||||
- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128, Phase 129, Phase 130, Phase 131 and Phase 132 gaps documented because CLI was not available in PATH.
|
||||
- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128, Phase 129, Phase 130, Phase 131, Phase 132 and Phase 133 gaps documented because CLI was not available in PATH.
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
- Blad `POST /inbox/mark-read` jest zapisywany przez istniejacy state failure path z czytelnym komunikatem.
|
||||
- Dodano `ErliOrdersSyncServiceTest` oraz rozszerzono `ErliStatusSyncServiceTest` o diagnostyke nieudanego push statusu.
|
||||
- Udokumentowano gapy srodowiskowe Phase 132: brak `vendor/bin/phpunit`, globalny XAMPP PHPUnit niekompatybilny z PHP, brak `sonar-scanner` w PATH.
|
||||
- [Phase 133, Plan 01] Domknieto parytet Erli w powierzchniach wspolnych: lista zamowien, statystyki dzienne i podsumowanie, automatyzacje oraz menu integracji.
|
||||
- Dodano `OrderSourceRegistry` jako wspolny kontrakt zrodel `allegro`, `shoppro`, `erli`.
|
||||
- Statystyki zawsze pokazuja kanal Erli i licza `orders.source='erli'` w agregacjach dziennych, miesiecznych i diagnostyce.
|
||||
- Automatyzacje pobieraja typy integracji zamowieniowych z rejestru, wiec aktywna integracja Erli jest dostepna w warunku Integracja.
|
||||
- Udokumentowano gapy srodowiskowe Phase 133: brak `vendor/` dla PHPUnit oraz brak `sonar-scanner` w PATH.
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
@@ -69,3 +74,16 @@
|
||||
- `.paul/phases/132-erli-hardening-observability-docs/132-01-PLAN.md`
|
||||
- `.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md`
|
||||
- `tests/Unit/ErliOrdersSyncServiceTest.php`
|
||||
- `.paul/phases/133-erli-cross-surface-parity/133-01-PLAN.md`
|
||||
- `.paul/phases/133-erli-cross-surface-parity/133-01-SUMMARY.md`
|
||||
- `src/Modules/Orders/OrderSourceRegistry.php`
|
||||
- `src/Modules/Orders/OrdersRepository.php`
|
||||
- `src/Modules/Orders/OrdersController.php`
|
||||
- `src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||
- `src/Modules/Automation/AutomationRepository.php`
|
||||
- `src/Modules/Accounting/InvoiceService.php`
|
||||
- `src/Modules/Settings/EmailTemplateController.php`
|
||||
- `src/Modules/Settings/SmsTemplateController.php`
|
||||
- `resources/views/layouts/app.php`
|
||||
- `tests/Unit/OrderSourceRegistryTest.php`
|
||||
- `tests/Unit/OrdersStatisticsRepositoryTest.php`
|
||||
|
||||
210
.paul/phases/133-erli-cross-surface-parity/133-01-PLAN.md
Normal file
210
.paul/phases/133-erli-cross-surface-parity/133-01-PLAN.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
phase: 133-erli-cross-surface-parity
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Orders/OrderSourceRegistry.php
|
||||
- src/Modules/Orders/OrdersRepository.php
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||
- src/Modules/Automation/AutomationRepository.php
|
||||
- src/Modules/Accounting/InvoiceService.php
|
||||
- src/Modules/Settings/EmailTemplateController.php
|
||||
- src/Modules/Settings/SmsTemplateController.php
|
||||
- resources/views/layouts/app.php
|
||||
- tests/Unit/OrderSourceRegistryTest.php
|
||||
- tests/Unit/OrdersStatisticsRepositoryTest.php
|
||||
- DOCS/DB_SCHEMA.md
|
||||
- DOCS/ARCHITECTURE.md
|
||||
- DOCS/TECH_CHANGELOG.md
|
||||
autonomous: true
|
||||
delegation: auto
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Domknac parytet Erli w miejscach, gdzie aplikacja traktuje obecnie tylko Allegro/shopPRO jako zrodla zamowien: lista zamowien, statystyki dzienne i podsumowanie, automatyzacje oraz aktywny stan menu integracji.
|
||||
|
||||
## Purpose
|
||||
Phase 127-132 dodaly techniczna integracje Erli, ale czesc powierzchni UI i raportowania nadal ma twardo wpisane tylko Allegro/shopPRO. Sprzedawca ma widziec Erli jako pelnoprawny kanal sprzedazy we wszystkich wspolnych przeplywach obslugi zamowien.
|
||||
|
||||
## Output
|
||||
Powstanie maly wspolny rejestr zrodel zamowien, a istniejace ekrany i zapytania beda korzystac z tego kontraktu zamiast lokalnych list Allegro/shopPRO. Erli bedzie zawsze dostepne w statystykach jako kanal `erli`, a lista zamowien i automatyzacje beda pokazywac Erli z czytelna etykieta.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **Zakres** - Czy plan ma objac wszystkie znalezione miejsca z parytetem Allegro/shopPRO, czy tylko wskazane ekrany listy zamowien i statystyk?
|
||||
-> Odpowiedz: Wszedzie.
|
||||
- **Kanaly** - Jak ma zachowywac sie filtr kanalow sprzedazy w statystykach dla Erli?
|
||||
-> Odpowiedz: Zawsze.
|
||||
- **Wdrozenie** - Czy podczas wdrozenia wolno wydzielic wspolny helper/rejestr kanalow, zeby ograniczyc kolejne pominiecia Erli?
|
||||
-> Odpowiedz: Tak, moze.
|
||||
</clarifications>
|
||||
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
@.paul/codebase/architecture.md
|
||||
@.paul/codebase/db_schema.md
|
||||
@.paul/phases/132-erli-hardening-observability-docs/132-01-SUMMARY.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Orders/OrdersRepository.php
|
||||
@src/Modules/Orders/OrdersController.php
|
||||
@src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||
@src/Modules/Statistics/OrdersStatisticsController.php
|
||||
@src/Modules/Automation/AutomationRepository.php
|
||||
@resources/views/layouts/app.php
|
||||
@resources/views/statistics/orders.php
|
||||
@resources/views/statistics/summary.php
|
||||
@resources/views/orders/list.php
|
||||
@DOCS/ARCHITECTURE.md
|
||||
@DOCS/DB_SCHEMA.md
|
||||
@DOCS/TECH_CHANGELOG.md
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | o |
|
||||
|
||||
**BLOCKING:** `sonar-scanner` jest wymagany przez `.paul/SPECIAL-FLOWS.md`. Jezeli CLI nadal nie jest dostepny w PATH, udokumentuj gap w SUMMARY/STATE tak jak w poprzednich fazach.
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Erli jest zrodlem zamowien na liscie zamowien
|
||||
```gherkin
|
||||
Given operator otwiera `/orders/list`
|
||||
When rozwija filtr `Zrodlo`
|
||||
Then widzi stale opcje Allegro, shopPRO i Erli z czytelnymi etykietami, nawet jezeli w bazie nie ma jeszcze zamowien Erli
|
||||
```
|
||||
|
||||
## AC-2: Statystyki dzienne i podsumowanie zawsze obsluguja Erli
|
||||
```gherkin
|
||||
Given operator otwiera `/statistics/orders` albo `/statistics/summary`
|
||||
When filtruje `Kanaly sprzedazy`
|
||||
Then kanal `Erli` jest dostepny domyslnie, a agregacje dzienne, miesieczne, tabele, wykresy i diagnostyka licza zamowienia `orders.source='erli'`
|
||||
```
|
||||
|
||||
## AC-3: Automatyzacje i menu integracji maja parytet Erli
|
||||
```gherkin
|
||||
Given aktywna integracja Erli istnieje w tabeli `integrations`
|
||||
When operator tworzy albo edytuje regule automatyzacji z warunkiem `Integracja`
|
||||
Then Erli pojawia sie na liscie integracji tak jak Allegro/shopPRO, a ekran konfiguracji Erli zachowuje aktywne podswietlenie menu Integracje
|
||||
```
|
||||
|
||||
## AC-4: Rejestr zrodel ogranicza kolejne pominiecia Erli
|
||||
```gherkin
|
||||
Given kod potrzebuje etykiet lub list wspieranych zrodel zamowien
|
||||
When uzywa wspolnego rejestru zrodel
|
||||
Then Allegro, shopPRO i Erli maja jedno zrodlo prawdy dla wartosci technicznych i etykiet, a testy potwierdzaja obecnosc Erli
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Wydzielic rejestr zrodel i podpiac liste zamowien oraz automatyzacje</name>
|
||||
<files>src/Modules/Orders/OrderSourceRegistry.php, src/Modules/Orders/OrdersRepository.php, src/Modules/Orders/OrdersController.php, src/Modules/Automation/AutomationRepository.php, resources/views/layouts/app.php</files>
|
||||
<action>
|
||||
Dodaj maly, finalny helper `OrderSourceRegistry` w module Orders:
|
||||
- stale techniczne dla `allegro`, `shoppro`, `erli`;
|
||||
- metode etykietowania `label(string $source): string`;
|
||||
- metode zwracajaca stale zrodla marketplace uzywane w filtrach i raportach;
|
||||
- metode zwracajaca typy integracji z zamowieniami dla automatyzacji.
|
||||
|
||||
Podlacz helper w istniejacych miejscach:
|
||||
- `OrdersRepository::sourceOptions()` ma zwracac stale Allegro/shopPRO/Erli z etykietami oraz ewentualne dodatkowe zrodla znalezione w DB, bez dublowania kluczy;
|
||||
- `OrdersController::sourceLabel()` ma korzystac z helpera;
|
||||
- `AutomationRepository::listOrderIntegrations()` ma obejmowac `erli` przez helper albo przygotowana liste typow, nie przez twarde `('allegro','shoppro')`;
|
||||
- `resources/views/layouts/app.php` ma traktowac `erli` jako aktywna podstrone integracji, jezeli kontroler lub przyszly kod przekaze `activeSettings='erli'`.
|
||||
|
||||
Nie zmieniaj kontraktu formularzy, nazw parametrow GET ani zapisu warunkow automatyzacji.
|
||||
</action>
|
||||
<verify>php -l src/Modules/Orders/OrderSourceRegistry.php; php -l src/Modules/Orders/OrdersRepository.php; php -l src/Modules/Orders/OrdersController.php; php -l src/Modules/Automation/AutomationRepository.php</verify>
|
||||
<done>AC-1, AC-3 i AC-4 satisfied: Erli jest widoczne na liscie zrodel, w automatyzacjach i w menu integracji, a etykiety pochodza ze wspolnego helpera.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rozszerzyc statystyki dzienne, miesieczne i diagnostyke o Erli</name>
|
||||
<files>src/Modules/Statistics/OrdersStatisticsRepository.php</files>
|
||||
<action>
|
||||
Zastap twarde zalozenie `LOWER(o.source) IN ("allegro", "shoppro")` wspolna lista z rejestru zrodel:
|
||||
- `listChannelOptions()` ma zawsze zwracac `allegro` i `erli`, a shopPRO nadal rozbijac po `integration_id` tak jak obecnie;
|
||||
- `channelSql()` ma mapowac Erli do stalego klucza `erli`;
|
||||
- `aggregateByDay()` i `aggregateByMonth()` maja filtrowac po wspieranych zrodlach marketplace, w tym Erli;
|
||||
- `diagnostics()` ma liczyc `in_date_and_source`, `after_channel_filter` i `after_status_filter` z Erli;
|
||||
- zachowaj prepared statements i obecny model `channels[]`.
|
||||
|
||||
Nie zmieniaj widokow statystyk, jezeli same dzialaja z przekazana lista `channelOptions`; obecne widoki juz renderuja opcje dynamicznie.
|
||||
</action>
|
||||
<verify>php -l src/Modules/Statistics/OrdersStatisticsRepository.php</verify>
|
||||
<done>AC-2 satisfied: `Erli` jest zawsze opcja kanalow i agregacje licza `orders.source='erli'` w dziennym raporcie, podsumowaniu oraz diagnostyce.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Dodac testy regresji i dokumentacje parytetu Erli</name>
|
||||
<files>tests/Unit/OrderSourceRegistryTest.php, tests/Unit/OrdersStatisticsRepositoryTest.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
|
||||
<action>
|
||||
Dodaj testy bez zewnetrznego API:
|
||||
- `OrderSourceRegistryTest` ma potwierdzac etykiety Allegro/shopPRO/Erli, stale kanaly raportowe i typy integracji zamowien;
|
||||
- `OrdersStatisticsRepositoryTest` ma potwierdzic, ze `listChannelOptions()` zwraca `Erli` nawet bez zamowien Erli oraz zachowuje warianty shopPRO po `integration_id` tam, gdzie testowa baza to wspiera.
|
||||
|
||||
Zaktualizuj dokumentacje:
|
||||
- `DOCS/ARCHITECTURE.md`: dodaj opis wspolnego rejestru zrodel i tego, ze statystyki korzystaja z Allegro/shopPRO/Erli;
|
||||
- `DOCS/TECH_CHANGELOG.md`: dodaj wpis z data 2026-05-16, opisujacy parytet Erli w UI/statystykach/automatyzacjach i powod zmiany.
|
||||
|
||||
`DOCS/DB_SCHEMA.md` nie wymaga zmian, jezeli implementacja nie doda migracji ani kolumn; nie dopisuj sztucznego wpisu bez zmiany schematu.
|
||||
</action>
|
||||
<verify>php -l tests/Unit/OrderSourceRegistryTest.php; php -l tests/Unit/OrdersStatisticsRepositoryTest.php; vendor/bin/phpunit tests/Unit/OrderSourceRegistryTest.php tests/Unit/OrdersStatisticsRepositoryTest.php; git diff --check -- DOCS/ARCHITECTURE.md DOCS/TECH_CHANGELOG.md</verify>
|
||||
<done>AC-2 i AC-4 satisfied: testy chronia obecnosc Erli, a dokumentacja opisuje nowy kontrakt bez zmian schematu.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- Nie dodawaj migracji ani nowych tabel; plan dotyczy parytetu UI/raportow na istniejacym `orders.source='erli'`.
|
||||
- Nie zmieniaj importu, ACK, status sync, przesylek ani API klienta Erli z Phase 127-132.
|
||||
- Nie podpinaj `DB_HOST_REMOTE` do runtime aplikacji.
|
||||
- Nie dodawaj native `alert()` / `confirm()` ani inline CSS w widokach.
|
||||
- Nie zmieniaj nazw parametrow formularzy: `source`, `channels[]`, `status_groups[]`.
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak nowych ekranow i redesignu UI.
|
||||
- Brak zmian w modelu danych automatyzacji; warunek integracji nadal uzywa `integration_ids`.
|
||||
- Brak live smoke API Erli w tym planie.
|
||||
- Brak refaktoru calego modulu statystyk poza wspolna lista zrodel i usunieciem twardych par Allegro/shopPRO.
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php -l` dla wszystkich zmienionych plikow PHP.
|
||||
- [ ] `vendor/bin/phpunit tests/Unit/OrderSourceRegistryTest.php tests/Unit/OrdersStatisticsRepositoryTest.php` albo jawny gap, jezeli `vendor/bin/phpunit` nadal nie istnieje.
|
||||
- [ ] `rg -n 'IN \("allegro", "shoppro"\)|type IN \(''allegro'', ''shoppro''\)|allegro.*shoppro|shoppro.*allegro' src resources -S --glob '!resources/scss/app.css.map' --glob '!public/assets/css/**'` nie pokazuje aktywnych par, ktore powinny obejmowac Erli.
|
||||
- [ ] `git diff --check -- . ':!.vscode/ftp-kr.sync.cache.json'`.
|
||||
- [ ] `sonar-scanner --version` i `sonar-scanner` uruchomiony albo gap udokumentowany zgodnie z `.paul/SPECIAL-FLOWS.md`.
|
||||
- [ ] All acceptance criteria met.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Erli jest zawsze widoczne w filtrze zrodla zamowien.
|
||||
- Erli jest zawsze widoczne i liczone w obu widokach statystyk.
|
||||
- Erli jest dostepne w warunku integracji automatyzacji.
|
||||
- Wspolny rejestr zrodel zastapil lokalne listy tam, gdzie dotyczy to zrodel zamowien.
|
||||
- Testy i dokumentacja potwierdzaja nowy kontrakt.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/133-erli-cross-surface-parity/133-01-SUMMARY.md`.
|
||||
</output>
|
||||
162
.paul/phases/133-erli-cross-surface-parity/133-01-SUMMARY.md
Normal file
162
.paul/phases/133-erli-cross-surface-parity/133-01-SUMMARY.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
phase: 133-erli-cross-surface-parity
|
||||
plan: 01
|
||||
subsystem: orders-statistics-automation
|
||||
tags: [erli, orders, statistics, automation, source-registry]
|
||||
requires:
|
||||
- phase: 127-132-erli-marketplace-integration
|
||||
provides: Erli settings, import, status sync, shipments, tracking, and hardening foundations
|
||||
provides:
|
||||
- Shared order source registry for Allegro, shopPRO, and Erli
|
||||
- Erli parity in order source filters, statistics channels, automation integration filters, and integration menu state
|
||||
- Regression tests for registry and statistics Erli aggregation
|
||||
affects: [orders, statistics, automation, templates, documentation]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [shared-order-source-registry]
|
||||
key-files:
|
||||
created:
|
||||
- src/Modules/Orders/OrderSourceRegistry.php
|
||||
- tests/Unit/OrderSourceRegistryTest.php
|
||||
- tests/Unit/OrdersStatisticsRepositoryTest.php
|
||||
modified:
|
||||
- src/Modules/Orders/OrdersRepository.php
|
||||
- src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||
- src/Modules/Automation/AutomationRepository.php
|
||||
- resources/views/layouts/app.php
|
||||
key-decisions:
|
||||
- "Order marketplace sources use OrderSourceRegistry as the shared contract."
|
||||
- "Statistics always expose Erli as a channel, while shopPRO remains split by integration id."
|
||||
patterns-established:
|
||||
- "Use OrderSourceRegistry for shared order source labels and marketplace source lists."
|
||||
duration: 20min
|
||||
started: 2026-05-16T16:01:00+02:00
|
||||
completed: 2026-05-16T16:22:41+02:00
|
||||
---
|
||||
|
||||
# Phase 133 Plan 01: Erli Cross-Surface Parity Summary
|
||||
|
||||
Erli is now treated as a first-class shared order source across order filters, statistics, automation integration filters, menu state, and source-template descriptions.
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~20min |
|
||||
| Started | 2026-05-16T16:01:00+02:00 |
|
||||
| Completed | 2026-05-16T16:22:41+02:00 |
|
||||
| Tasks | 3 completed |
|
||||
| Files modified | 17 plus PAUL metadata |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Erli is an order source on `/orders/list` | Pass | `OrdersRepository::sourceOptions()` starts from `OrderSourceRegistry::sourceOptions()`, so Allegro, shopPRO, and Erli are stable options even before matching rows exist. |
|
||||
| AC-2: Daily and summary statistics support Erli | Pass | `listChannelOptions()` always exposes `erli`; `aggregateByDay()`, `aggregateByMonth()`, and `diagnostics()` use registry-backed marketplace source filtering. |
|
||||
| AC-3: Automation and integration menu have Erli parity | Pass | `AutomationRepository::listOrderIntegrations()` uses registry integration types; the settings menu active list includes `erli`. |
|
||||
| AC-4: Shared registry prevents future omissions | Pass | `OrderSourceRegistry` centralizes source keys/labels/types; unit tests cover labels and Erli presence. |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Added `OrderSourceRegistry` as the single shared contract for `allegro`, `shoppro`, and `erli`.
|
||||
- Removed hardcoded Allegro/shopPRO-only source filters from statistics and automation integration lookup.
|
||||
- Kept Erli always visible in statistics as channel key `erli`, while preserving shopPRO per-integration keys.
|
||||
- Updated labels/docs where the old descriptions still implied only Allegro/shopPRO.
|
||||
|
||||
## Task Commits
|
||||
|
||||
The phase was committed as one scoped transition commit:
|
||||
|
||||
| Task | Commit | Type | Description |
|
||||
|------|--------|------|-------------|
|
||||
| Phase 133 | Phase transition commit | feat | Erli cross-surface parity |
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Orders/OrderSourceRegistry.php` | Created | Shared source keys, labels, marketplace source list, and order integration types. |
|
||||
| `src/Modules/Orders/OrdersRepository.php` | Modified | Source filter options now include stable Allegro/shopPRO/Erli labels plus DB-discovered extras. |
|
||||
| `src/Modules/Orders/OrdersController.php` | Modified | Source labels now use the shared registry. |
|
||||
| `src/Modules/Statistics/OrdersStatisticsRepository.php` | Modified | Statistics channels and source filters include Erli; SQLite-compatible helpers support new tests. |
|
||||
| `src/Modules/Automation/AutomationRepository.php` | Modified | Order integration filter uses registry-backed prepared placeholders and includes Erli. |
|
||||
| `resources/views/layouts/app.php` | Modified | Erli settings keep the Integracje menu active. |
|
||||
| `src/Modules/Settings/EmailTemplateController.php` | Modified | Source variable label mentions Erli. |
|
||||
| `src/Modules/Settings/SmsTemplateController.php` | Modified | Source variable label mentions Erli. |
|
||||
| `src/Modules/Accounting/InvoiceService.php` | Modified | NIP extraction comment documents Erli payload support. |
|
||||
| `tests/Unit/OrderSourceRegistryTest.php` | Created | Covers labels and order integration source list. |
|
||||
| `tests/Unit/OrdersStatisticsRepositoryTest.php` | Created | Covers Erli channel presence and day/month Erli aggregation. |
|
||||
| `DOCS/ARCHITECTURE.md` | Modified | Documents `OrderSourceRegistry` and statistics Erli contract. |
|
||||
| `DOCS/DB_SCHEMA.md` | Modified | Updates integration type/reporting notes to mention Erli. |
|
||||
| `DOCS/TECH_CHANGELOG.md` | Modified | Adds Phase 133 technical changelog entry. |
|
||||
| `.paul/ROADMAP.md` | Modified | Marks Phase 133/milestone transition state. |
|
||||
| `.paul/STATE.md` | Modified | Tracks APPLY/UNIFY state and skill gaps. |
|
||||
| `.paul/changelog/2026-05-16.md` | Modified | Adds human-readable Phase 133 changelog. |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Use `OrderSourceRegistry` for shared order source contracts | The user asked for Erli everywhere and approved a helper/registry. | Future source lists should use the registry instead of local Allegro/shopPRO pairs. |
|
||||
| Keep Erli as one statistics channel | Current Erli settings are single-account/global, unlike shopPRO per-integration reporting. | Statistics use stable `erli` key and can split later only if Erli becomes multi-account. |
|
||||
| Extend small documentation/template labels found by parity scan | The scope was "wszędzie"; old labels still implied Allegro/shopPRO-only source support. | No runtime behavior risk; reduces operator confusion. |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 3 | Documentation/template parity only; no schema or API change. |
|
||||
| Deferred | 3 | Environment/manual verification gaps documented. |
|
||||
|
||||
### Scope Additions
|
||||
|
||||
- Updated `DOCS/DB_SCHEMA.md` because the integration type/reporting notes still omitted Erli.
|
||||
- Updated e-mail/SMS source variable labels from `Allegro/shopPRO/...` to `Allegro/shopPRO/Erli`.
|
||||
- Updated an invoice NIP extraction comment to document existing Erli payload support.
|
||||
|
||||
### Deferred Items
|
||||
|
||||
- Full PHPUnit run waits for `composer install` because `vendor/` is missing.
|
||||
- SonarQube scan waits for `sonar-scanner` in PATH.
|
||||
- Manual browser smoke remains for `/orders/list`, `/statistics/orders`, `/statistics/summary`, and automation rule edit UI.
|
||||
|
||||
## Verification Results
|
||||
|
||||
| Command | Result |
|
||||
|---------|--------|
|
||||
| `php -l` on changed PHP source/test files | Pass |
|
||||
| Ad-hoc PHP/SQLite source/statistics check | Pass |
|
||||
| `rg -F 'IN ("allegro", "shoppro")' ...` | Pass, no remaining hardcoded source filter |
|
||||
| `rg -F "type IN ('allegro', 'shoppro')" ...` | Pass, no remaining hardcoded automation integration filter |
|
||||
| `rg -F "Zrodlo (Allegro/shopPRO/...)" ...` | Pass, no remaining old source variable label |
|
||||
| `git diff --check -- . ':!.vscode/ftp-kr.sync.cache.json'` | Pass; Git reported only LF/CRLF warnings |
|
||||
| `vendor/bin/phpunit tests/Unit/OrderSourceRegistryTest.php tests/Unit/OrdersStatisticsRepositoryTest.php` | Not run; `vendor/` is missing |
|
||||
| `sonar-scanner --version` | Not run successfully; command unavailable in PATH |
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Existing `SUMMARY.md` was created during APPLY because the plan output requested it. | Treated it as an APPLY draft and rewrote it during UNIFY with full frontmatter/reconciliation. |
|
||||
| PHPUnit executable missing. | Added tests and verified syntax plus ad-hoc SQLite behavior; gap documented. |
|
||||
| Sonar scanner missing. | Gap documented in STATE and SUMMARY. |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- v3.8 Erli marketplace integration is complete in code through cross-surface parity.
|
||||
- New shared source registry is available for future order-source code.
|
||||
|
||||
**Concerns:**
|
||||
- Live Erli smoke and migrations remain operator follow-up from prior phases.
|
||||
- Full PHPUnit/Sonar verification still depends on local tooling setup.
|
||||
|
||||
**Blockers:**
|
||||
- None for PAUL loop closure.
|
||||
|
||||
---
|
||||
*Phase: 133-erli-cross-surface-parity, Plan: 01*
|
||||
*Completed: 2026-05-16*
|
||||
@@ -34,7 +34,7 @@ HTTP Request
|
||||
|--------|-------|-------------|---------|
|
||||
| **Auth** | 3 | `AuthController`, `AuthMiddleware`, `AuthService` | Login/logout, session |
|
||||
| **Users** | 2 | `UserController`, `UserRepository` | User CRUD |
|
||||
| **Orders** | 3 | `OrdersController` (1187 LOC), `OrdersRepository` (1221 LOC) | Order list, detail, status, payment, correlated subquery for return-risk |
|
||||
| **Orders** | 4 | `OrdersController` (1187 LOC), `OrdersRepository` (1221 LOC), `OrderSourceRegistry` | Order list, detail, status, payment, shared order source labels/options, correlated subquery for return-risk |
|
||||
| **Shipments** | 17 | `ShipmentController`, provider services + tracking services | Shipment creation, label download, tracking polling |
|
||||
| **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export |
|
||||
| **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments |
|
||||
@@ -47,6 +47,15 @@ HTTP Request
|
||||
| **Statistics** | 3 | `OrdersStatisticsController`, `OrdersStatisticsRepository`, `statistics-summary-charts.js` | Daily order statistics and monthly summary charts |
|
||||
| **Info** | 1 | `InfoController` | Health check |
|
||||
|
||||
## Order Source Registry
|
||||
|
||||
- `OrderSourceRegistry` (`src/Modules/Orders/OrderSourceRegistry.php`) is the shared contract for order marketplace sources.
|
||||
- It exposes stable source keys and labels for `allegro`, `shoppro`, and `erli`.
|
||||
- `OrdersRepository::sourceOptions()` starts from the registry so `/orders/list` shows Allegro, shopPRO, and Erli even before orders from every source exist locally.
|
||||
- `OrdersController::sourceLabel()` uses the registry for badge/filter labels.
|
||||
- `AutomationRepository::listOrderIntegrations()` uses the registry integration types so order automation conditions include active Erli integrations together with Allegro/shopPRO.
|
||||
- `OrdersStatisticsRepository` uses the registry for marketplace source filtering. Statistics channels always include Allegro and Erli; shopPRO remains split per integration as `shoppro:{integration_id}`.
|
||||
|
||||
## Frontend Enhancement Modules
|
||||
|
||||
### Checkbox Multiselect (`public/assets/js/modules/checkbox-multiselect.js`)
|
||||
@@ -77,6 +86,9 @@ HTTP Request
|
||||
3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` / `ErliStatusSyncService` → marketplace API
|
||||
|
||||
### Statistics Summary
|
||||
|
||||
Phase 133 keeps Erli in the shared statistics channel contract: `listChannelOptions()` always exposes `erli`, and daily/monthly aggregation filters marketplace orders through `OrderSourceRegistry::marketplaceSources()`.
|
||||
|
||||
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
||||
2. **Filters** — controller reuses statistics filter semantics: date range, `channels[]`, `status_groups[]`, default status groups excluding cancelled; default history starts at `2026-04-01`.
|
||||
3. **Aggregation** — `OrdersStatisticsRepository::aggregateByMonth()` groups existing `orders` rows by `YYYY-MM` and channel key, using the same effective date/channel/status/gross amount SQL helpers as the daily report.
|
||||
|
||||
@@ -497,7 +497,7 @@ UNIQUE: `(provider, raw_status)`
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `type` | VARCHAR(32) | NO | e.g., 'allegro', 'shoppro', 'apaczka', 'inpost' |
|
||||
| `type` | VARCHAR(32) | NO | e.g., 'allegro', 'shoppro', 'erli', 'apaczka', 'inpost' |
|
||||
| `name` | VARCHAR(128) | NO | |
|
||||
| `base_url` | VARCHAR(255) | NO | |
|
||||
| `api_key_encrypted` | TEXT | YES | AES-encrypted |
|
||||
@@ -1073,7 +1073,7 @@ Default keys: `cron_run_on_web`, `cron_web_limit`, `gs1_api_login`, `gs1_prefix`
|
||||
**Statistics Summary (`/statistics/summary`)** — no dedicated reporting tables.
|
||||
- Reads existing `orders` rows and groups by month using the same effective order date used by `/statistics/orders`.
|
||||
- Default summary history starts at April 2026 (`2026-04-01`), even if older rows exist.
|
||||
- Splits series by channel key: Allegro as one series and each shopPRO integration by `orders.integration_id`.
|
||||
- Splits series by channel key: Allegro as one series, Erli as one series, and each shopPRO integration by `orders.integration_id`.
|
||||
- Uses `integrations.name` only for display labels when available.
|
||||
- Filters by selected status groups through `order_status_groups` and `order_statuses`.
|
||||
- Uses existing gross amount columns via `OrdersStatisticsRepository::grossAmountSql()`.
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# Technical Changelog
|
||||
|
||||
## 2026-05-16 - Phase 133 Plan 01: Erli Cross-Surface Parity
|
||||
|
||||
**Co zrobiono:**
|
||||
- Dodano `OrderSourceRegistry` jako wspolne zrodlo kluczy i etykiet `allegro`, `shoppro`, `erli`.
|
||||
- Lista zamowien startuje od stalych opcji Allegro/shopPRO/Erli i dopiero potem dopina dodatkowe zrodla znalezione w bazie.
|
||||
- Statystyki dzienne i podsumowanie zawsze pokazuja kanal Erli, licza `orders.source='erli'` i uzywaja wspolnego filtra zrodel marketplace.
|
||||
- Automatyzacje pobieraja aktywne integracje zamowieniowe przez wspolny rejestr, wiec Erli jest dostepne w warunku integracji.
|
||||
- Menu Integracje pozostaje aktywne na ekranach Erli.
|
||||
- Opisy zmiennej `zrodlo` w szablonach e-mail/SMS oraz dokumentacja raportowania wskazuja Erli obok Allegro/shopPRO.
|
||||
- Dodano testy jednostkowe rejestru zrodel i agregacji statystyk dla Erli.
|
||||
|
||||
**Dlaczego:**
|
||||
- Phase 127-132 dodaly integracje Erli, ale czesc wspolnych powierzchni nadal miala lokalne listy Allegro/shopPRO. Jeden rejestr zmniejsza ryzyko kolejnych pominiec Erli.
|
||||
|
||||
**BREAKING / migracja:**
|
||||
- Brak migracji i brak breaking changes. Zmiana korzysta z istniejacych wartosci `orders.source`, `orders.integration_id` oraz `integrations.type`.
|
||||
|
||||
## 2026-05-16 - Phase 132 Plan 01: Erli Hardening, Observability + Docs
|
||||
|
||||
**Co zrobiono:**
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'users' ? ' is-active' : '' ?>" href="/settings/users">
|
||||
<?= $e($t('navigation.users')) ?>
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && in_array($currentSettings, ['integrations', 'allegro', 'apaczka', 'inpost', 'shoppro'], true) ? ' is-active' : '' ?>" href="/settings/integrations">
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && in_array($currentSettings, ['integrations', 'allegro', 'apaczka', 'inpost', 'shoppro', 'erli'], true) ? ' is-active' : '' ?>" href="/settings/integrations">
|
||||
<?= $e($t('navigation.integrations')) ?>
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'statuses' ? ' is-active' : '' ?>" href="/settings/statuses">
|
||||
|
||||
@@ -412,7 +412,7 @@ final class InvoiceService
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract NIP from various payload locations (Allegro, shopPRO).
|
||||
* Extract NIP from various payload locations (Allegro, shopPRO, Erli).
|
||||
*
|
||||
* @param array<string, mixed> $order
|
||||
* @param array<string, mixed>|null $buyerAddress
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Automation;
|
||||
|
||||
use App\Core\Http\ToggleableRepositoryTrait;
|
||||
use App\Modules\Orders\OrderSourceRegistry;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
@@ -187,15 +188,37 @@ final class AutomationRepository
|
||||
*/
|
||||
public function listOrderIntegrations(): array
|
||||
{
|
||||
[$typeInSql, $params] = $this->buildStringInClause('type', OrderSourceRegistry::orderIntegrationTypes());
|
||||
$statement = $this->pdo->prepare(
|
||||
"SELECT id, type, name FROM integrations WHERE type IN ('allegro', 'shoppro') AND is_active = 1 ORDER BY type, name"
|
||||
'SELECT id, type, name
|
||||
FROM integrations
|
||||
WHERE type IN (' . $typeInSql . ')
|
||||
AND is_active = 1
|
||||
ORDER BY type, name'
|
||||
);
|
||||
$statement->execute();
|
||||
$statement->execute($params);
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $values
|
||||
* @return array{0:string,1:array<string,string>}
|
||||
*/
|
||||
private function buildStringInClause(string $prefix, array $values): array
|
||||
{
|
||||
$placeholders = [];
|
||||
$params = [];
|
||||
foreach (array_values($values) as $index => $value) {
|
||||
$key = $prefix . '_' . $index;
|
||||
$placeholders[] = ':' . $key;
|
||||
$params[$key] = $value;
|
||||
}
|
||||
|
||||
return [implode(', ', $placeholders), $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: int, name: string}>
|
||||
*/
|
||||
|
||||
57
src/Modules/Orders/OrderSourceRegistry.php
Normal file
57
src/Modules/Orders/OrderSourceRegistry.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Orders;
|
||||
|
||||
final class OrderSourceRegistry
|
||||
{
|
||||
public const SOURCE_ALLEGRO = 'allegro';
|
||||
public const SOURCE_SHOPPRO = 'shoppro';
|
||||
public const SOURCE_ERLI = 'erli';
|
||||
|
||||
/** @var array<string, string> */
|
||||
private const LABELS = [
|
||||
self::SOURCE_ALLEGRO => 'Allegro',
|
||||
self::SOURCE_SHOPPRO => 'shopPRO',
|
||||
self::SOURCE_ERLI => 'Erli',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function sourceOptions(): array
|
||||
{
|
||||
return self::LABELS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function marketplaceSources(): array
|
||||
{
|
||||
return array_keys(self::LABELS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function orderIntegrationTypes(): array
|
||||
{
|
||||
return self::marketplaceSources();
|
||||
}
|
||||
|
||||
public static function label(string $source): string
|
||||
{
|
||||
$normalized = self::normalize($source);
|
||||
if ($normalized === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::LABELS[$normalized] ?? ucfirst($normalized);
|
||||
}
|
||||
|
||||
public static function normalize(string $source): string
|
||||
{
|
||||
return strtolower(trim($source));
|
||||
}
|
||||
}
|
||||
@@ -886,12 +886,7 @@ final class OrdersController
|
||||
|
||||
private function sourceLabel(string $source): string
|
||||
{
|
||||
return match (strtolower(trim($source))) {
|
||||
'allegro' => 'Allegro',
|
||||
'shoppro' => 'shopPRO',
|
||||
'erli' => 'Erli',
|
||||
default => ucfirst(strtolower(trim($source))),
|
||||
};
|
||||
return OrderSourceRegistry::label($source);
|
||||
}
|
||||
|
||||
private function statusLabel(string $statusCode, array $statusLabelMap = []): string
|
||||
|
||||
@@ -294,20 +294,22 @@ final class OrdersRepository
|
||||
try {
|
||||
$rows = $this->pdo->query('SELECT DISTINCT source FROM orders WHERE source IS NOT NULL AND source <> "" ORDER BY source ASC')->fetchAll(PDO::FETCH_COLUMN);
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
return OrderSourceRegistry::sourceOptions();
|
||||
}
|
||||
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
return OrderSourceRegistry::sourceOptions();
|
||||
}
|
||||
|
||||
$options = [];
|
||||
$options = OrderSourceRegistry::sourceOptions();
|
||||
foreach ($rows as $row) {
|
||||
$value = trim((string) $row);
|
||||
$value = OrderSourceRegistry::normalize((string) $row);
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$options[$value] = $value;
|
||||
if (!isset($options[$value])) {
|
||||
$options[$value] = OrderSourceRegistry::label($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
|
||||
@@ -21,7 +21,7 @@ final class EmailTemplateController
|
||||
'vars' => [
|
||||
'numer' => 'Numer wewnetrzny (OP...)',
|
||||
'numer_zewnetrzny' => 'Numer z platformy',
|
||||
'zrodlo' => 'Zrodlo (Allegro/shopPRO/...)',
|
||||
'zrodlo' => 'Zrodlo (Allegro/shopPRO/Erli)',
|
||||
'kwota' => 'Kwota brutto',
|
||||
'waluta' => 'Waluta (PLN/EUR/...)',
|
||||
'data' => 'Data zamowienia',
|
||||
|
||||
@@ -22,7 +22,7 @@ final class SmsTemplateController
|
||||
'vars' => [
|
||||
'numer' => 'Numer wewnetrzny (OP...)',
|
||||
'numer_zewnetrzny' => 'Numer z platformy',
|
||||
'zrodlo' => 'Zrodlo (Allegro/shopPRO/...)',
|
||||
'zrodlo' => 'Zrodlo (Allegro/shopPRO/Erli)',
|
||||
'kwota' => 'Kwota brutto',
|
||||
'waluta' => 'Waluta (PLN/EUR/...)',
|
||||
'data' => 'Data zamowienia',
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Statistics;
|
||||
|
||||
use App\Modules\Orders\OrderSourceRegistry;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
@@ -87,10 +88,17 @@ final class OrdersStatisticsRepository
|
||||
}
|
||||
|
||||
$channels = [
|
||||
['key' => 'allegro', 'label' => 'Allegro'],
|
||||
[
|
||||
'key' => OrderSourceRegistry::SOURCE_ALLEGRO,
|
||||
'label' => OrderSourceRegistry::label(OrderSourceRegistry::SOURCE_ALLEGRO),
|
||||
],
|
||||
];
|
||||
|
||||
if (!is_array($rows)) {
|
||||
$channels[] = [
|
||||
'key' => OrderSourceRegistry::SOURCE_ERLI,
|
||||
'label' => OrderSourceRegistry::label(OrderSourceRegistry::SOURCE_ERLI),
|
||||
];
|
||||
return $channels;
|
||||
}
|
||||
|
||||
@@ -102,6 +110,11 @@ final class OrdersStatisticsRepository
|
||||
];
|
||||
}
|
||||
|
||||
$channels[] = [
|
||||
'key' => OrderSourceRegistry::SOURCE_ERLI,
|
||||
'label' => OrderSourceRegistry::label(OrderSourceRegistry::SOURCE_ERLI),
|
||||
];
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
@@ -174,11 +187,16 @@ final class OrdersStatisticsRepository
|
||||
$grossAmountSql = $this->grossAmountSql('o');
|
||||
$rawStatusSql = $this->rawStatusSql('o');
|
||||
|
||||
['sql' => $sourceFilterSql, 'params' => $sourceParams] = $this->sourceFilterSql('o', 'src');
|
||||
[$channelInSql, $channelParams] = $this->buildStringInClause('ch', $channels);
|
||||
$params = array_merge($channelParams, [
|
||||
$params = array_merge(
|
||||
$sourceParams,
|
||||
$channelParams,
|
||||
[
|
||||
'date_from' => $dateFrom . ' 00:00:00',
|
||||
'date_to' => $dateTo . ' 23:59:59',
|
||||
]);
|
||||
]
|
||||
);
|
||||
|
||||
$statusFilterSql = '';
|
||||
if ($statusCodes !== []) {
|
||||
@@ -197,7 +215,7 @@ final class OrdersStatisticsRepository
|
||||
LEFT JOIN allegro_order_status_mappings asm
|
||||
ON o.source = "allegro"
|
||||
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
|
||||
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||
WHERE ' . $sourceFilterSql . '
|
||||
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||
AND ' . $effectiveDateSql . ' >= :date_from
|
||||
AND ' . $effectiveDateSql . ' <= :date_to
|
||||
@@ -264,11 +282,16 @@ final class OrdersStatisticsRepository
|
||||
$grossAmountSql = $this->grossAmountSql('o');
|
||||
$rawStatusSql = $this->rawStatusSql('o');
|
||||
|
||||
['sql' => $sourceFilterSql, 'params' => $sourceParams] = $this->sourceFilterSql('o', 'month_src');
|
||||
[$channelInSql, $channelParams] = $this->buildStringInClause('ch', $channels);
|
||||
$params = array_merge($channelParams, [
|
||||
$params = array_merge(
|
||||
$sourceParams,
|
||||
$channelParams,
|
||||
[
|
||||
'date_from' => $dateFrom . ' 00:00:00',
|
||||
'date_to' => $dateTo . ' 23:59:59',
|
||||
]);
|
||||
]
|
||||
);
|
||||
|
||||
$statusFilterSql = '';
|
||||
if ($statusCodes !== []) {
|
||||
@@ -277,7 +300,7 @@ final class OrdersStatisticsRepository
|
||||
$params = array_merge($params, $statusParams);
|
||||
}
|
||||
|
||||
$monthSql = 'DATE_FORMAT(' . $effectiveDateSql . ', "%Y-%m")';
|
||||
$monthSql = $this->monthSql($effectiveDateSql);
|
||||
$sql = 'SELECT
|
||||
' . $monthSql . ' AS month,
|
||||
' . $channelSql . ' AS channel_key,
|
||||
@@ -287,7 +310,7 @@ final class OrdersStatisticsRepository
|
||||
LEFT JOIN allegro_order_status_mappings asm
|
||||
ON o.source = "allegro"
|
||||
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
|
||||
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||
WHERE ' . $sourceFilterSql . '
|
||||
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||
AND ' . $effectiveDateSql . ' >= :date_from
|
||||
AND ' . $effectiveDateSql . ' <= :date_to
|
||||
@@ -338,6 +361,7 @@ final class OrdersStatisticsRepository
|
||||
$channelSql = $this->channelSql('o');
|
||||
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||
$rawStatusSql = $this->rawStatusSql('o');
|
||||
['sql' => $sourceFilterSql, 'params' => $sourceParams] = $this->sourceFilterSql('o', 'dbg_src');
|
||||
|
||||
$data = [
|
||||
'columns' => [
|
||||
@@ -356,14 +380,14 @@ final class OrdersStatisticsRepository
|
||||
$data['counts']['in_date_and_source'] = $this->safeCount(
|
||||
'SELECT COUNT(*)
|
||||
FROM orders o
|
||||
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||
WHERE ' . $sourceFilterSql . '
|
||||
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||
AND ' . $effectiveDateSql . ' >= :date_from
|
||||
AND ' . $effectiveDateSql . ' <= :date_to',
|
||||
[
|
||||
array_merge($sourceParams, [
|
||||
'date_from' => $dateFrom . ' 00:00:00',
|
||||
'date_to' => $dateTo . ' 23:59:59',
|
||||
],
|
||||
]),
|
||||
$data['errors'],
|
||||
'in_date_and_source'
|
||||
);
|
||||
@@ -373,12 +397,13 @@ final class OrdersStatisticsRepository
|
||||
$data['counts']['after_channel_filter'] = $this->safeCount(
|
||||
'SELECT COUNT(*)
|
||||
FROM orders o
|
||||
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||
WHERE ' . $sourceFilterSql . '
|
||||
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||
AND ' . $effectiveDateSql . ' >= :date_from
|
||||
AND ' . $effectiveDateSql . ' <= :date_to
|
||||
AND ' . $channelSql . ' IN (' . $channelInSql . ')',
|
||||
array_merge(
|
||||
$sourceParams,
|
||||
[
|
||||
'date_from' => $dateFrom . ' 00:00:00',
|
||||
'date_to' => $dateTo . ' 23:59:59',
|
||||
@@ -400,12 +425,13 @@ final class OrdersStatisticsRepository
|
||||
LEFT JOIN allegro_order_status_mappings asm
|
||||
ON o.source = "allegro"
|
||||
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
|
||||
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||
WHERE ' . $sourceFilterSql . '
|
||||
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||
AND ' . $effectiveDateSql . ' >= :date_from
|
||||
AND ' . $effectiveDateSql . ' <= :date_to
|
||||
AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')',
|
||||
array_merge(
|
||||
$sourceParams,
|
||||
[
|
||||
'date_from' => $dateFrom . ' 00:00:00',
|
||||
'date_to' => $dateTo . ' 23:59:59',
|
||||
@@ -439,6 +465,22 @@ final class OrdersStatisticsRepository
|
||||
return [implode(', ', $placeholders), $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{sql:string,params:array<string,string>}
|
||||
*/
|
||||
private function sourceFilterSql(string $orderAlias, string $prefix): array
|
||||
{
|
||||
[$sourceInSql, $sourceParams] = $this->buildStringInClause(
|
||||
$prefix,
|
||||
OrderSourceRegistry::marketplaceSources()
|
||||
);
|
||||
|
||||
return [
|
||||
'sql' => 'LOWER(COALESCE(' . $orderAlias . '.source, "")) IN (' . $sourceInSql . ')',
|
||||
'params' => $sourceParams,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $values
|
||||
* @return array{0:string,1:array<string,string>}
|
||||
@@ -497,21 +539,47 @@ final class OrdersStatisticsRepository
|
||||
)';
|
||||
}
|
||||
|
||||
private function monthSql(string $dateSql): string
|
||||
{
|
||||
if ($this->isSqlite()) {
|
||||
return 'strftime(\'%Y-%m\', ' . $dateSql . ')';
|
||||
}
|
||||
|
||||
return 'DATE_FORMAT(' . $dateSql . ', "%Y-%m")';
|
||||
}
|
||||
|
||||
private function channelSql(string $orderAlias): string
|
||||
{
|
||||
$collationSql = $this->channelCollationSql();
|
||||
if ($this->hasOrdersColumn('integration_id')) {
|
||||
return '(CASE
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN CONCAT("shoppro:", COALESCE(CAST(' . $orderAlias . '.integration_id AS CHAR) COLLATE utf8mb4_unicode_ci, "0"))
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "erli" THEN "erli"
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN ' . $this->shopproChannelSql($orderAlias) . '
|
||||
ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
|
||||
END) COLLATE utf8mb4_unicode_ci';
|
||||
END)' . $collationSql;
|
||||
}
|
||||
|
||||
return '(CASE
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "erli" THEN "erli"
|
||||
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN "shoppro:0"
|
||||
ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
|
||||
END) COLLATE utf8mb4_unicode_ci';
|
||||
END)' . $collationSql;
|
||||
}
|
||||
|
||||
private function shopproChannelSql(string $orderAlias): string
|
||||
{
|
||||
if ($this->isSqlite()) {
|
||||
return '(\'shoppro:\' || COALESCE(CAST(' . $orderAlias . '.integration_id AS TEXT), \'0\'))';
|
||||
}
|
||||
|
||||
return 'CONCAT("shoppro:", COALESCE(CAST(' . $orderAlias . '.integration_id AS CHAR) COLLATE utf8mb4_unicode_ci, "0"))';
|
||||
}
|
||||
|
||||
private function channelCollationSql(): string
|
||||
{
|
||||
return $this->isSqlite() ? '' : ' COLLATE utf8mb4_unicode_ci';
|
||||
}
|
||||
|
||||
private function shopproChannelLabel(int $integrationId): string
|
||||
@@ -636,18 +704,7 @@ final class OrdersStatisticsRepository
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = :table_name
|
||||
AND COLUMN_NAME = :column_name'
|
||||
);
|
||||
$stmt->execute([
|
||||
'table_name' => 'orders',
|
||||
'column_name' => $column,
|
||||
]);
|
||||
$exists = ((int) $stmt->fetchColumn()) > 0;
|
||||
$exists = $this->detectOrdersColumn($column);
|
||||
} catch (Throwable) {
|
||||
$exists = false;
|
||||
}
|
||||
@@ -676,4 +733,42 @@ final class OrdersStatisticsRepository
|
||||
|
||||
return $exists;
|
||||
}
|
||||
|
||||
private function detectOrdersColumn(string $column): bool
|
||||
{
|
||||
if ($this->isSqlite()) {
|
||||
$stmt = $this->pdo->query('PRAGMA table_info(orders)');
|
||||
$rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
if (!is_array($rows)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ((string) ($row['name'] ?? '') === $column) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = :table_name
|
||||
AND COLUMN_NAME = :column_name'
|
||||
);
|
||||
$stmt->execute([
|
||||
'table_name' => 'orders',
|
||||
'column_name' => $column,
|
||||
]);
|
||||
|
||||
return ((int) $stmt->fetchColumn()) > 0;
|
||||
}
|
||||
|
||||
private function isSqlite(): bool
|
||||
{
|
||||
return (string) $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite';
|
||||
}
|
||||
}
|
||||
|
||||
35
tests/Unit/OrderSourceRegistryTest.php
Normal file
35
tests/Unit/OrderSourceRegistryTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Modules\Orders\OrderSourceRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class OrderSourceRegistryTest extends TestCase
|
||||
{
|
||||
public function testLabelsUseStableMarketplaceNames(): void
|
||||
{
|
||||
self::assertSame('Allegro', OrderSourceRegistry::label('allegro'));
|
||||
self::assertSame('shopPRO', OrderSourceRegistry::label('shoppro'));
|
||||
self::assertSame('Erli', OrderSourceRegistry::label('erli'));
|
||||
self::assertSame('Custom', OrderSourceRegistry::label(' custom '));
|
||||
}
|
||||
|
||||
public function testSourceOptionsAlwaysContainErli(): void
|
||||
{
|
||||
self::assertSame([
|
||||
'allegro' => 'Allegro',
|
||||
'shoppro' => 'shopPRO',
|
||||
'erli' => 'Erli',
|
||||
], OrderSourceRegistry::sourceOptions());
|
||||
}
|
||||
|
||||
public function testOrderIntegrationTypesIncludeAllOrderSources(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['allegro', 'shoppro', 'erli'],
|
||||
OrderSourceRegistry::orderIntegrationTypes()
|
||||
);
|
||||
}
|
||||
}
|
||||
147
tests/Unit/OrdersStatisticsRepositoryTest.php
Normal file
147
tests/Unit/OrdersStatisticsRepositoryTest.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Modules\Statistics\OrdersStatisticsRepository;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionProperty;
|
||||
|
||||
final class OrdersStatisticsRepositoryTest extends TestCase
|
||||
{
|
||||
private PDO $pdo;
|
||||
private OrdersStatisticsRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->resetColumnCache();
|
||||
|
||||
$this->pdo = new PDO('sqlite::memory:');
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->createSchema();
|
||||
|
||||
$this->repository = new OrdersStatisticsRepository($this->pdo);
|
||||
}
|
||||
|
||||
public function testListChannelOptionsAlwaysContainsErliWithoutErliOrders(): void
|
||||
{
|
||||
$this->seedIntegration(10, 'shoppro', 'Sklep PL');
|
||||
$this->seedOrder('shoppro', 10, '2026-05-01 10:00:00', 100.0);
|
||||
|
||||
$labelsByKey = [];
|
||||
foreach ($this->repository->listChannelOptions() as $option) {
|
||||
$labelsByKey[(string) $option['key']] = (string) $option['label'];
|
||||
}
|
||||
|
||||
self::assertSame('Allegro', $labelsByKey['allegro'] ?? null);
|
||||
self::assertSame('Sklep PL', $labelsByKey['shoppro:10'] ?? null);
|
||||
self::assertSame('Erli', $labelsByKey['erli'] ?? null);
|
||||
}
|
||||
|
||||
public function testAggregateByDayCountsErliOrders(): void
|
||||
{
|
||||
$this->seedIntegration(20, 'erli', 'Erli');
|
||||
$this->seedOrder('erli', 20, '2026-05-02 12:30:00', 123.45);
|
||||
|
||||
$rows = $this->repository->aggregateByDay('2026-05-01', '2026-05-03', ['erli'], []);
|
||||
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('2026-05-02', $rows[0]['day']);
|
||||
self::assertSame('erli', $rows[0]['channel_key']);
|
||||
self::assertSame(1, $rows[0]['orders_count']);
|
||||
self::assertSame(123.45, $rows[0]['total_gross']);
|
||||
}
|
||||
|
||||
public function testAggregateByMonthCountsErliOrders(): void
|
||||
{
|
||||
$this->seedIntegration(20, 'erli', 'Erli');
|
||||
$this->seedOrder('erli', 20, '2026-05-02 12:30:00', 123.45);
|
||||
$this->seedOrder('erli', 20, '2026-05-10 09:00:00', 76.55);
|
||||
|
||||
$rows = $this->repository->aggregateByMonth('2026-05-01', '2026-05-31', ['erli'], []);
|
||||
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('2026-05', $rows[0]['month']);
|
||||
self::assertSame('erli', $rows[0]['channel_key']);
|
||||
self::assertSame(2, $rows[0]['orders_count']);
|
||||
self::assertSame(200.0, $rows[0]['total_gross']);
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
$this->pdo->exec(
|
||||
'CREATE TABLE orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT,
|
||||
integration_id INTEGER,
|
||||
ordered_at TEXT,
|
||||
source_created_at TEXT,
|
||||
source_updated_at TEXT,
|
||||
fetched_at TEXT,
|
||||
status_code TEXT,
|
||||
total_with_tax REAL
|
||||
)'
|
||||
);
|
||||
|
||||
$this->pdo->exec(
|
||||
'CREATE TABLE integrations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
type TEXT,
|
||||
name TEXT
|
||||
)'
|
||||
);
|
||||
|
||||
$this->pdo->exec(
|
||||
'CREATE TABLE allegro_order_status_mappings (
|
||||
allegro_status_code TEXT,
|
||||
orderpro_status_code TEXT
|
||||
)'
|
||||
);
|
||||
}
|
||||
|
||||
private function seedIntegration(int $id, string $type, string $name): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO integrations (id, type, name) VALUES (:id, :type, :name)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'id' => $id,
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedOrder(string $source, int $integrationId, string $orderedAt, float $gross): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO orders (source, integration_id, ordered_at, fetched_at, status_code, total_with_tax)
|
||||
VALUES (:source, :integration_id, :ordered_at, :fetched_at, :status_code, :total_with_tax)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'source' => $source,
|
||||
'integration_id' => $integrationId,
|
||||
'ordered_at' => $orderedAt,
|
||||
'fetched_at' => $orderedAt,
|
||||
'status_code' => 'new',
|
||||
'total_with_tax' => $gross,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resetColumnCache(): void
|
||||
{
|
||||
foreach ([
|
||||
'hasOrdersTotalWithoutTax',
|
||||
'hasOrdersTotalNet',
|
||||
'hasOrdersTotalWithTax',
|
||||
'hasOrdersTotalGross',
|
||||
'hasOrdersIntegrationId',
|
||||
'hasOrdersStatusCode',
|
||||
'hasOrdersExternalStatusId',
|
||||
] as $propertyName) {
|
||||
$property = new ReflectionProperty(OrdersStatisticsRepository::class, $propertyName);
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user