feat(130): erli shipments and labels
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 129 shipped (Erli status mappings/sync); Phase 130 next |
|
| Status | v3.8 Erli Marketplace Integration in progress — Phase 130 shipped (Erli shipments + labels/external parcel sync); Phase 131 next |
|
||||||
| Last Updated | 2026-05-16 (Phase 129 closed) |
|
| Last Updated | 2026-05-16 (Phase 130 closed) |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -129,6 +129,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [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
|
- [x] Mapowanie i synchronizacja statusow Erli: osobne pull/push mappings, discovery statusow z inboxa, reczny-only push `PATCH /orders/{id}/status`, cron `erli_status_sync` i zakladki w ustawieniach Erli — Phase 129
|
||||||
|
- [x] Przesylki Erli: zakladka mapowania dostaw, etykiety przez lokalne providery InPost/Apaczka i rejestracja paczek zewnetrznych w Erli przez `POST /shipping/external` — Phase 130
|
||||||
|
|
||||||
### Deferred
|
### Deferred
|
||||||
|
|
||||||
@@ -137,11 +138,10 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|
||||||
- [ ] v3.8 Erli Marketplace Integration — Phase 130 next: generowanie etykiet i obsluga przesylek Erli.
|
- [ ] v3.8 Erli Marketplace Integration — Phase 131 next: tracking przesylek Erli i hooki automatyzacji.
|
||||||
|
|
||||||
### Planned (Next)
|
### Planned (Next)
|
||||||
|
|
||||||
- [ ] Erli shipments + labels — Phase 130
|
|
||||||
- [ ] Erli tracking + automation hooks — Phase 131
|
- [ ] Erli tracking + automation hooks — Phase 131
|
||||||
- [ ] Erli hardening, observability + docs — Phase 132
|
- [ ] Erli hardening, observability + docs — Phase 132
|
||||||
- [ ] ZarzÄ…dzanie produktami
|
- [ ] ZarzÄ…dzanie produktami
|
||||||
@@ -251,13 +251,15 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
|||||||
| 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 |
|
| 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 -> 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 |
|
| Erli settings korzysta z zakladek Integracja/Statusy/Ustawienia | Po dodaniu mapowan strona wymagala parytetu UX z Allegro/shopPRO | 2026-05-16 | Active |
|
||||||
|
| Erli etykiety uzywaja lokalnych providerow, a Erli dostaje paczke zewnetrzna przez `POST /shipping/external` | Operator nie chce nadawac na umowie Erli; API wspiera zewnetrzne paczki/tracking | 2026-05-16 | Active |
|
||||||
|
| `carrier_delivery_method_mappings` przechowuje `source_vendor_code`/`source_service_id` dla Erli | Vendor Erli i lokalny provider to osobne kontrakty, nie nalezy ich mieszac w polach Apaczki/InPost | 2026-05-16 | Active |
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
| Metric | Target | Current | Status |
|
| Metric | Target | Current | Status |
|
||||||
|--------|--------|---------|--------|
|
|--------|--------|---------|--------|
|
||||||
| Liczba zintegrowanych źródeł zamówień | ≥3 | 3 zrodla importu (Allegro, shopPRO, Erli); Erli wymaga manualnego smoke po migracji | In progress |
|
| Liczba zintegrowanych źródeł zamówień | ≥3 | 3 zrodla importu (Allegro, shopPRO, Erli); Erli wymaga manualnego smoke po migracji | In progress |
|
||||||
| Generowanie etykiet | Działa | InPost | In progress |
|
| Generowanie etykiet | Działa | InPost + Erli przez lokalne providery po mapowaniu; Erli wymaga manualnego smoke po migracji | In progress |
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
@@ -282,6 +284,6 @@ Quick Reference:
|
|||||||
|
|
||||||
---
|
---
|
||||||
*PROJECT.md — Updated when requirements or context change*
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
*Last updated: 2026-05-16 after Phase 129 (Erli Status Mapping + Sync) closure; v3.8 milestone in progress*
|
*Last updated: 2026-05-16 after Phase 130 (Erli Shipments + Labels) closure; v3.8 milestone in progress*
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ 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%).
|
Progress: 4 of 6 phases complete (67%).
|
||||||
|
|
||||||
| 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 | 1/1 | Complete (2026-05-16; migration/manual Erli status smoke pending operator) |
|
| 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 | 1/1 | Complete (2026-05-16; migration/manual Erli shipping smoke pending operator) |
|
||||||
| 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 |
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Plans: 129-01 (complete)
|
|||||||
### Phase 130: Erli Shipments + Labels
|
### Phase 130: Erli Shipments + Labels
|
||||||
|
|
||||||
Focus: Generowanie etykiet dla zamowien Erli, mapowanie metod dostawy Erli na dostepne providery, zapis paczek w `shipment_packages`, pobieranie labeli i integracja z kolejka zdalnego druku.
|
Focus: Generowanie etykiet dla zamowien Erli, mapowanie metod dostawy Erli na dostepne providery, zapis paczek w `shipment_packages`, pobieranie labeli i integracja z kolejka zdalnego druku.
|
||||||
Plans: TBD (defined during $paul-plan)
|
Plans: 130-01 (complete)
|
||||||
|
|
||||||
### Phase 131: Erli Tracking + Automation Hooks
|
### Phase 131: Erli Tracking + Automation Hooks
|
||||||
|
|
||||||
@@ -555,4 +555,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-03-12*
|
*Roadmap created: 2026-03-12*
|
||||||
*Last updated: 2026-05-16 - Phase 129 complete, ready for Phase 130 planning*
|
*Last updated: 2026-05-16 - Phase 130 complete, ready for Phase 131 planning*
|
||||||
|
|||||||
@@ -5,33 +5,33 @@
|
|||||||
See: .paul/PROJECT.md (updated 2026-05-16)
|
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 130 ready to plan.
|
**Current focus:** v3.8 Erli Marketplace Integration - Phase 131 ready to plan.
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.8 Erli Marketplace Integration
|
Milestone: v3.8 Erli Marketplace Integration
|
||||||
Phase: 130 of 132 (Erli Shipments + Labels) - Not started
|
Phase: 131 of 132 (Erli Tracking + Automation Hooks) - Not started
|
||||||
Plan: Not started
|
Plan: Not started
|
||||||
Status: Ready to plan
|
Status: Ready to plan
|
||||||
Last activity: 2026-05-16 00:23 - Phase 129 complete, transitioned to Phase 130
|
Last activity: 2026-05-16 00:51 - Phase 130 complete, transitioned to Phase 131
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone v3.8: [#####-----] 50% (Phases 127-129 complete; Phase 130 next)
|
- Milestone v3.8: [#######---] 67% (Phases 127-130 complete; Phase 131 next)
|
||||||
- Phase 130: [----------] 0% (not planned)
|
- Phase 131: [----------] 0% (not planned)
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN -> APPLY -> UNIFY
|
PLAN -> APPLY -> UNIFY
|
||||||
done done done [Loop complete - ready for next PLAN]
|
done done done [Loop complete, ready for next PLAN]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-05-16 00:23
|
Last session: 2026-05-16 00:51
|
||||||
Stopped at: Phase 129 complete, ready to plan Phase 130
|
Stopped at: Phase 130 complete, ready to plan Phase 131
|
||||||
Next action: $paul-plan for Phase 130 (Erli Shipments + Labels)
|
Next action: $paul-plan for Phase 131 (Erli Tracking + Automation Hooks)
|
||||||
Resume file: .paul/ROADMAP.md
|
Resume file: .paul/ROADMAP.md
|
||||||
|
|
||||||
## Pending parallel work
|
## Pending parallel work
|
||||||
@@ -39,8 +39,8 @@ Resume file: .paul/ROADMAP.md
|
|||||||
|
|
||||||
## Git State
|
## Git State
|
||||||
|
|
||||||
Last phase commit: 7972bb9 feat(129): erli status mapping sync
|
Last phase commit: a73bd7f feat(130): erli shipments and labels
|
||||||
Previous: 2565d9b feat(128): erli orders import
|
Previous: 7972bb9 feat(129): erli status mapping sync
|
||||||
Branch: main
|
Branch: main
|
||||||
|
|
||||||
### Skill Audit (Phase 129)
|
### Skill Audit (Phase 129)
|
||||||
@@ -49,6 +49,12 @@ Branch: main
|
|||||||
|----------|---------|-------|
|
|----------|---------|-------|
|
||||||
| `sonar-scanner` | gap documented | Attempted before UNIFY; CLI is not available in PATH. |
|
| `sonar-scanner` | gap documented | Attempted before UNIFY; CLI is not available in PATH. |
|
||||||
|
|
||||||
|
### Skill Audit (Phase 130)
|
||||||
|
|
||||||
|
| Expected | Invoked | Notes |
|
||||||
|
|----------|---------|-------|
|
||||||
|
| `sonar-scanner` | gap documented | Attempted after APPLY; 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).
|
||||||
@@ -76,6 +82,9 @@ Branch: main
|
|||||||
- 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 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`.
|
- 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`.
|
||||||
|
- Phase 130 follow-up: uruchom `php bin/migrate.php` (dodaje `carrier_delivery_method_mappings.source_service_id/source_vendor_code`), otworz `/settings/integrations/erli?tab=delivery`, zapisz mapowanie metody Erli na InPost/Apaczka oraz vendor Erli, a potem utworz etykiete dla zamowienia Erli i potwierdz `POST /shipping/external`.
|
||||||
|
- Phase 130 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec test `tests/Unit/ErliExternalShipmentServiceTest.php` nie zostal uruchomiony przez PHPUnit; wykonano `php -l` i `git diff --check`.
|
||||||
|
- Phase 130 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony.
|
||||||
|
|
||||||
## Deferred to Next Milestones
|
## Deferred to Next Milestones
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
- Dodano repozytoria mapowan, `ErliStatusSyncService`, `ErliStatusSyncHandler`, discovery nieznanych statusow Erli i testy jednostkowe dla mappera/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.
|
- 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.
|
- Udokumentowano gapy srodowiskowe: brak `vendor/bin/phpunit`, globalny XAMPP PHPUnit niekompatybilny z PHP, brak `sonar-scanner` w PATH.
|
||||||
|
- [Phase 130, Plan 01] Wdrozono obsluge przesylek Erli: zakladke mapowan dostaw, lokalne generowanie etykiet przez zmapowanych providerow i rejestracje paczki zewnetrznej w Erli.
|
||||||
|
- Rozszerzono klienta Erli o slowniki shipping/delivery, vendorow, cenniki oraz `POST /shipping/external`.
|
||||||
|
- Rozszerzono mapowania dostaw o `source_service_id` i `source_vendor_code`, zeby oddzielic Erli vendor od lokalnego providera etykiety.
|
||||||
|
- Dodano niekrytyczna synchronizacje tracking number do Erli po utworzeniu lokalnej paczki.
|
||||||
|
- Udokumentowano gapy srodowiskowe Phase 130: brak `vendor/bin/phpunit`, brak `sonar-scanner` w PATH, smoke testy Erli po migracji do wykonania przez operatora.
|
||||||
|
|
||||||
## Zmienione pliki
|
## Zmienione pliki
|
||||||
|
|
||||||
@@ -35,6 +40,15 @@
|
|||||||
- `resources/lang/pl.php`
|
- `resources/lang/pl.php`
|
||||||
- `tests/Unit/ErliOrderMapperTest.php`
|
- `tests/Unit/ErliOrderMapperTest.php`
|
||||||
- `tests/Unit/ErliStatusSyncServiceTest.php`
|
- `tests/Unit/ErliStatusSyncServiceTest.php`
|
||||||
|
- `.paul/phases/130-erli-shipments-labels/130-01-PLAN.md`
|
||||||
|
- `.paul/phases/130-erli-shipments-labels/130-01-SUMMARY.md`
|
||||||
|
- `database/migrations/20260516_000117_extend_delivery_mappings_for_erli_shipping.sql`
|
||||||
|
- `src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php`
|
||||||
|
- `src/Modules/Settings/ErliDeliveryMappingController.php`
|
||||||
|
- `src/Modules/Settings/ErliExternalShipmentService.php`
|
||||||
|
- `src/Modules/Shipments/ShipmentController.php`
|
||||||
|
- `resources/views/shipments/prepare.php`
|
||||||
|
- `tests/Unit/ErliExternalShipmentServiceTest.php`
|
||||||
- `DOCS/DB_SCHEMA.md`
|
- `DOCS/DB_SCHEMA.md`
|
||||||
- `DOCS/ARCHITECTURE.md`
|
- `DOCS/ARCHITECTURE.md`
|
||||||
- `DOCS/TECH_CHANGELOG.md`
|
- `DOCS/TECH_CHANGELOG.md`
|
||||||
|
|||||||
255
.paul/phases/130-erli-shipments-labels/130-01-PLAN.md
Normal file
255
.paul/phases/130-erli-shipments-labels/130-01-PLAN.md
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
---
|
||||||
|
phase: 130-erli-shipments-labels
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Core/Constants/IntegrationSources.php
|
||||||
|
- src/Modules/Settings/ErliApiClient.php
|
||||||
|
- src/Modules/Settings/ErliDeliveryMappingController.php
|
||||||
|
- src/Modules/Settings/ErliIntegrationController.php
|
||||||
|
- src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php
|
||||||
|
- src/Modules/Settings/ErliExternalShipmentService.php
|
||||||
|
- src/Modules/Shipments/ShipmentController.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/settings/erli.php
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- tests/Unit/ErliExternalShipmentServiceTest.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dodac obsluge przesylek dla zamowien Erli: mapowanie metod dostawy w osobnej zakladce ustawien, uzycie istniejacych providerow etykiet (InPost/Apaczka tam, gdzie sa dostepne), zapis paczek w `shipment_packages` oraz rejestracje numeru nadania w Erli przez natywny endpoint przesylek zewnetrznych.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Sprzedawca ma nadawac zamowienia Erli z orderPRO bez przelaczania sie do panelu marketplace, ale bez wymuszania nadawania "na umowie Erli". Natywne Erli w tym planie oznacza wykorzystanie oficjalnych slownikow/cennikow/shipping API tam, gdzie pasuja do wlasnych umow operatora.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
Nowa zakladka dostaw w `/settings/integrations/erli`, rozszerzony Erli API client, flow tworzenia etykiety dla zmapowanych zamowien Erli oraz synchronizacja paczki zewnetrznej do Erli po uzyskaniu numeru przesylki.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
<clarifications>
|
||||||
|
- **Natywne Erli** - Czy probujemy najpierw API shipping Erli, czy od razu tylko istniejacy provider etykiet?
|
||||||
|
-> Odpowiedz: Trzeba sprobowac natywnego z erli.
|
||||||
|
- **Konfiguracja** - Gdzie operator ma mapowac metody dostawy Erli?
|
||||||
|
-> Odpowiedz: zakladka.
|
||||||
|
- **Umowa/cenniki** - Czy nadawac przez Erli na ich umowie, czy tylko to, co da sie spiac z wlasnymi providerami/cennikami?
|
||||||
|
-> Odpowiedz: Nie chce nadawac przez Erli na ich umowie, to co sie da. Tak jak allegro ma swoje cenniki.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
@AGENTS.md
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/127-erli-integration-foundation/127-01-SUMMARY.md
|
||||||
|
@.paul/phases/128-erli-orders-import/128-01-SUMMARY.md
|
||||||
|
@.paul/phases/129-erli-status-mapping-sync/129-01-SUMMARY.md
|
||||||
|
|
||||||
|
## External Contract
|
||||||
|
@https://erli.pl/svc/shop-api/doc/swagger.json
|
||||||
|
|
||||||
|
Notes from official swagger checked during planning:
|
||||||
|
- `GET /dictionaries/shippingMethods`, `GET /dictionaries/deliveryMethods`, `GET /dictionaries/deliveryVendors`, `GET /delivery/priceLists`, `GET /delivery/priceListsDetails` expose Erli shipping/delivery dictionaries and cenniki.
|
||||||
|
- `POST /shipping/external` creates external parcels with `orderId`, `vendor`, `status`, `trackingNumber`.
|
||||||
|
- `POST /shipping/parcels/` creates Erli parcels, but this path is treated as optional/discovery only because the user does not want to ship on Erli's carrier agreement.
|
||||||
|
- No label-download endpoint was found in the swagger path list; local label generation must continue through existing provider services unless APPLY finds a documented endpoint with compatible contract.
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Settings/ErliApiClient.php
|
||||||
|
@src/Modules/Settings/ErliIntegrationController.php
|
||||||
|
@src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php
|
||||||
|
@src/Modules/Settings/AllegroDeliveryMappingController.php
|
||||||
|
@src/Modules/Shipments/ShipmentController.php
|
||||||
|
@src/Modules/Shipments/ShipmentProviderInterface.php
|
||||||
|
@src/Modules/Shipments/InpostShipmentService.php
|
||||||
|
@src/Modules/Shipments/ApaczkaShipmentService.php
|
||||||
|
@resources/views/settings/erli.php
|
||||||
|
@resources/views/shipments/prepare.php
|
||||||
|
@routes/web.php
|
||||||
|
@DOCS/DB_SCHEMA.md
|
||||||
|
@DOCS/ARCHITECTURE.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
## Required Skills (from SPECIAL-FLOWS.md)
|
||||||
|
|
||||||
|
| Skill | Priority | When to Invoke | Loaded? |
|
||||||
|
|-------|----------|----------------|---------|
|
||||||
|
| `sonar-scanner` | required | Po APPLY, przed UNIFY | ○ |
|
||||||
|
| /feature-dev | optional | Przed implementacja integracji marketplace/shipping | ○ |
|
||||||
|
| /frontend-design | optional | Przy dodaniu zakladki UI w ustawieniach Erli | ○ |
|
||||||
|
| /code-review | optional | Po implementacji, przed UNIFY | ○ |
|
||||||
|
|
||||||
|
**BLOCKING:** Required `sonar-scanner` must be attempted before UNIFY. If CLI is still unavailable in PATH, document the gap in SUMMARY and STATE like Phase 128/129.
|
||||||
|
|
||||||
|
## Skill Invocation Checklist
|
||||||
|
- [ ] `sonar-scanner` attempted after APPLY
|
||||||
|
- [ ] Optional flows considered if implementation risk warrants them
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Erli Shipping Contract Is Used Where Safe
|
||||||
|
```gherkin
|
||||||
|
Given Erli API credentials are configured
|
||||||
|
When orderPRO loads shipment configuration for Erli
|
||||||
|
Then it can fetch Erli shipping/delivery dictionaries, delivery vendors and price list data from the official Erli API
|
||||||
|
And it does not assume native Erli label download exists unless a documented endpoint is confirmed during implementation.
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Erli Delivery Mapping Tab Exists
|
||||||
|
```gherkin
|
||||||
|
Given the operator opens /settings/integrations/erli
|
||||||
|
When they select the delivery/shipping tab
|
||||||
|
Then they see distinct Erli delivery methods from imported orders plus Erli dictionary context
|
||||||
|
And they can map each method to an available local label provider/service and Erli vendor with a CSRF-protected save form.
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Erli Orders Preselect Shipment Provider
|
||||||
|
```gherkin
|
||||||
|
Given an Erli order has a saved delivery mapping
|
||||||
|
When the operator opens the shipment prepare page for that order
|
||||||
|
Then orderPRO preselects the mapped provider/service and shows a clear unmapped diagnostic if no mapping exists.
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Labels Are Generated Without Erli Carrier Agreement
|
||||||
|
```gherkin
|
||||||
|
Given an Erli order is mapped to an existing local provider such as InPost or Apaczka
|
||||||
|
When the operator creates a shipment
|
||||||
|
Then the existing provider creates the local shipment package, stores it in shipment_packages and makes the label available for download/print queue
|
||||||
|
And the flow does not create Erli-contract parcels unless explicitly supported and selected in future work.
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: External Parcel Is Registered In Erli
|
||||||
|
```gherkin
|
||||||
|
Given a local Erli shipment package has a tracking number and a mapped Erli vendor
|
||||||
|
When the package creation/status check reaches a label-ready or sent state
|
||||||
|
Then orderPRO calls POST /shipping/external with order id, vendor, status and tracking number
|
||||||
|
And API failures are logged as non-critical local shipment warnings rather than losing the label.
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Documentation And Verification Cover The Flow
|
||||||
|
```gherkin
|
||||||
|
Given the implementation is complete
|
||||||
|
When verification runs
|
||||||
|
Then PHP syntax checks, focused tests or documented PHPUnit gap, diff checks and documentation updates cover the new Erli shipment behavior.
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Extend Erli API and mapping repository for shipping</name>
|
||||||
|
<files>src/Core/Constants/IntegrationSources.php, src/Modules/Settings/ErliApiClient.php, src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php, DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md</files>
|
||||||
|
<action>
|
||||||
|
Add `erli` as an explicit integration source constant/path wherever current generic delivery mappings normalize only `allegro` and `shoppro`.
|
||||||
|
Extend `CarrierDeliveryMethodMappingRepository` so `listMappings`, `findByOrderMethod`, `saveMappings`, `hasMappingsForSource` and `getDistinctOrderDeliveryMethods` work for `source_system='erli'` and global `source_integration_id=0`.
|
||||||
|
Extend `ErliApiClient` with small focused methods for shipping dictionaries and external parcels:
|
||||||
|
- `getShippingMethods()`
|
||||||
|
- `getDeliveryMethods()`
|
||||||
|
- `getDeliveryVendors()`
|
||||||
|
- `getPriceLists()` / `getPriceListsDetails()` if the response is needed for operator context
|
||||||
|
- `createExternalParcel(array $payload)`
|
||||||
|
Keep request/response handling consistent with existing Erli client methods and keep runtime on `DB_HOST`, never `DB_HOST_REMOTE`.
|
||||||
|
Do not add a new table unless APPLY proves existing `carrier_delivery_method_mappings` cannot represent the mapping. If a schema change becomes unavoidable, add a migration and update DB docs in the same task.
|
||||||
|
</action>
|
||||||
|
<verify>`C:\xampp\php\php.exe -l src/Modules/Settings/ErliApiClient.php` and `C:\xampp\php\php.exe -l src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php`; repository can list/save/find Erli mappings without falling back to Allegro.</verify>
|
||||||
|
<done>AC-1 foundation complete and AC-2 persistence ready.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add Erli delivery mapping tab</name>
|
||||||
|
<files>src/Modules/Settings/ErliDeliveryMappingController.php, src/Modules/Settings/ErliIntegrationController.php, routes/web.php, resources/views/settings/erli.php, resources/lang/pl.php, DOCS/ARCHITECTURE.md</files>
|
||||||
|
<action>
|
||||||
|
Create or wire a focused Erli delivery mapping controller, following the clarity of `AllegroDeliveryMappingController` but without Allegro OAuth assumptions.
|
||||||
|
Load:
|
||||||
|
- distinct Erli delivery methods from imported orders,
|
||||||
|
- current `carrier_delivery_method_mappings` rows for `erli`,
|
||||||
|
- available local label provider services already supported by the shipment flow,
|
||||||
|
- Erli vendors/dictionaries for choosing the vendor sent to `/shipping/external`.
|
||||||
|
Add a new Erli settings tab (for example `delivery`) next to existing integration/status/settings tabs.
|
||||||
|
Saving must use POST + `_token`, `Flash`, bounded validation and redirect back to `/settings/integrations/erli?tab=delivery`.
|
||||||
|
Escape all view output with `$e()`, avoid inline CSS in the view, and do not add native `alert()`/`confirm()`.
|
||||||
|
</action>
|
||||||
|
<verify>`C:\xampp\php\php.exe -l src/Modules/Settings/ErliDeliveryMappingController.php`, `C:\xampp\php\php.exe -l resources/views/settings/erli.php`, and manual route smoke: opening `/settings/integrations/erli?tab=delivery` renders the tab even when Erli API metadata fetch fails.</verify>
|
||||||
|
<done>AC-2 satisfied; UI is consistent with Phase 129 tab pattern.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Use mappings during shipment creation and sync external parcel to Erli</name>
|
||||||
|
<files>src/Modules/Shipments/ShipmentController.php, src/Modules/Settings/ErliExternalShipmentService.php, resources/views/shipments/prepare.php, routes/web.php, tests/Unit/ErliExternalShipmentServiceTest.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
|
||||||
|
<action>
|
||||||
|
Update shipment prepare flow so Erli orders are eligible for `CarrierDeliveryMethodMappingRepository::findByOrderMethod('erli', 0, ...)` just like Allegro/shopPRO.
|
||||||
|
Ensure the mapped provider/service can preselect existing local provider forms. Preserve existing Allegro/shopPRO behavior.
|
||||||
|
Add an `ErliExternalShipmentService` that reads the local package/order context and calls `ErliApiClient::createExternalParcel()` with:
|
||||||
|
- Erli external order id,
|
||||||
|
- mapped Erli vendor,
|
||||||
|
- tracking number from `shipment_packages`,
|
||||||
|
- status matching Erli's accepted external parcel status contract.
|
||||||
|
Trigger this service after a local provider has produced a tracking number/label-ready package (for example from `checkStatus()` and/or label-ready creation path), and make the call idempotent enough for retries by treating duplicate/already-existing responses as non-fatal where Erli exposes that signal.
|
||||||
|
Log or flash bounded warnings for Erli sync failure but never discard the local label.
|
||||||
|
Add focused unit coverage for payload building and failure behavior. If PHPUnit remains unavailable, leave the test file and document the run gap.
|
||||||
|
</action>
|
||||||
|
<verify>`C:\xampp\php\php.exe -l src/Modules/Shipments/ShipmentController.php`; `C:\xampp\php\php.exe -l src/Modules/Settings/ErliExternalShipmentService.php`; if dependencies exist, run `vendor/bin/phpunit tests/Unit/ErliExternalShipmentServiceTest.php`; manual smoke on a mapped Erli order reaches local `shipment_packages` and attempts `/shipping/external` only after tracking exists.</verify>
|
||||||
|
<done>AC-3, AC-4, AC-5 and AC-6 satisfied.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Do not change unrelated `.vscode/ftp-kr.sync.cache.json`.
|
||||||
|
- Do not regress Allegro, shopPRO, InPost or Apaczka shipment behavior.
|
||||||
|
- Do not connect runtime application code to `DB_HOST_REMOTE`.
|
||||||
|
- Do not add inline CSS to `resources/views/...`.
|
||||||
|
- Do not add native `alert()` / `confirm()` in views.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- This plan does not implement Phase 131 carrier tracking polling, automation expansion or delivery-status cron beyond the immediate external parcel registration.
|
||||||
|
- This plan does not force creating shipments through Erli's own carrier agreement. `POST /shipping/parcels/` may be inspected during APPLY, but shipping via Erli-contract parcels is out of scope unless it is clearly just external/seller-contract compatible.
|
||||||
|
- This plan does not build product/stock sync or Erli offer management.
|
||||||
|
- This plan keeps Erli as one global integration unless a future phase explicitly introduces multi-account support.
|
||||||
|
- This plan should avoid new schema if existing generic mapping storage is enough.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `C:\xampp\php\php.exe -l` for every changed PHP/view/test file.
|
||||||
|
- [ ] `git diff --check`.
|
||||||
|
- [ ] `vendor/bin/phpunit tests/Unit/ErliExternalShipmentServiceTest.php` if PHPUnit dependencies are available; otherwise document the environment gap.
|
||||||
|
- [ ] Attempt `sonar-scanner` after APPLY; if unavailable, document the gap in SUMMARY and STATE.
|
||||||
|
- [ ] Manual smoke: `/settings/integrations/erli?tab=delivery` renders, saves mapping with `_token`, and survives Erli API metadata errors.
|
||||||
|
- [ ] Manual smoke: a mapped Erli order preselects shipment provider/service on prepare page.
|
||||||
|
- [ ] Manual smoke: local provider label creation still stores `shipment_packages` and the Erli external parcel sync is attempted only with a tracking number.
|
||||||
|
- [ ] `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md` updated for changed behavior and any schema decision.
|
||||||
|
- [ ] All acceptance criteria met.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Erli settings has a working delivery/shipping mapping tab.
|
||||||
|
- Erli delivery mappings are stored and used by shipment prepare.
|
||||||
|
- Erli orders can generate local labels through existing providers when mapped.
|
||||||
|
- Erli receives external parcel/tracking data through native shipping API when tracking exists.
|
||||||
|
- Native Erli-contract parcel creation is either explicitly not used or documented as future/manual decision.
|
||||||
|
- Tests/lints/docs and required skill audit are completed or gaps documented.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/130-erli-shipments-labels/130-01-SUMMARY.md`.
|
||||||
|
</output>
|
||||||
165
.paul/phases/130-erli-shipments-labels/130-01-SUMMARY.md
Normal file
165
.paul/phases/130-erli-shipments-labels/130-01-SUMMARY.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
phase: 130-erli-shipments-labels
|
||||||
|
plan: 01
|
||||||
|
subsystem: settings, integrations, shipments, database
|
||||||
|
tags: [erli, shipping, labels, delivery-mapping, external-parcels]
|
||||||
|
requires:
|
||||||
|
- phase: 127-erli-integration-foundation
|
||||||
|
provides: global Erli credentials and API client
|
||||||
|
- phase: 128-erli-orders-import
|
||||||
|
provides: Erli orders in common order model
|
||||||
|
- phase: 129-erli-status-mapping-sync
|
||||||
|
provides: tabbed Erli settings UI and outbound API pattern
|
||||||
|
provides:
|
||||||
|
- Erli delivery mapping tab
|
||||||
|
- Erli shipping dictionary and external parcel API methods
|
||||||
|
- Local label provider preselection for Erli orders
|
||||||
|
- External parcel registration in Erli after tracking number exists
|
||||||
|
affects: [phase-131-erli-tracking-automation, erli-settings, shipment-flow]
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [marketplace delivery mapping with source vendor, non-critical external parcel sync]
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260516_000117_extend_delivery_mappings_for_erli_shipping.sql
|
||||||
|
- src/Modules/Settings/ErliDeliveryMappingController.php
|
||||||
|
- src/Modules/Settings/ErliExternalShipmentService.php
|
||||||
|
- tests/Unit/ErliExternalShipmentServiceTest.php
|
||||||
|
modified:
|
||||||
|
- src/Modules/Settings/ErliApiClient.php
|
||||||
|
- src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php
|
||||||
|
- src/Modules/Settings/ErliIntegrationController.php
|
||||||
|
- src/Modules/Shipments/ShipmentController.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/settings/erli.php
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
key-decisions:
|
||||||
|
- "Erli labels stay on local providers; Erli receives external parcel/tracking through POST /shipping/external."
|
||||||
|
- "Erli vendor code is stored separately from local provider service in carrier_delivery_method_mappings.source_vendor_code."
|
||||||
|
patterns-established:
|
||||||
|
- "External marketplace shipment sync is non-critical and must not block local labels."
|
||||||
|
duration: ~20min
|
||||||
|
started: 2026-05-16T00:37:00+02:00
|
||||||
|
completed: 2026-05-16T00:51:00+02:00
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 130-01 Summary: Erli Shipments + Labels
|
||||||
|
|
||||||
|
Phase 130 adds Erli delivery mappings, local label provider preselection and external parcel registration through the native Erli shipping API without using Erli carrier-contract label flow.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Result |
|
||||||
|
|--------|--------|
|
||||||
|
| Duration | ~20min |
|
||||||
|
| Started | 2026-05-16T00:37:00+02:00 |
|
||||||
|
| Completed | 2026-05-16T00:51:00+02:00 |
|
||||||
|
| Tasks | 3/3 completed |
|
||||||
|
| Files changed | 18 phase files, excluding unrelated `.vscode/ftp-kr.sync.cache.json` |
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
| AC | Result | Notes |
|
||||||
|
|----|--------|-------|
|
||||||
|
| AC-1: Erli Shipping Contract Is Used Where Safe | Pass | `ErliApiClient` now supports shipping/delivery dictionaries, vendors, price lists and `POST /shipping/external`; no native label download endpoint is assumed. |
|
||||||
|
| AC-2: Erli Delivery Mapping Tab Exists | Pass | `/settings/integrations/erli?tab=delivery` has a CSRF-protected mapping tab with imported delivery methods, Erli vendor context and local provider service choices. |
|
||||||
|
| AC-3: Erli Orders Preselect Shipment Provider | Pass | Shipment prepare includes Erli in delivery mapping lookup and preselects mapped local provider/service. |
|
||||||
|
| AC-4: Labels Are Generated Without Erli Carrier Agreement | Pass | Labels remain provider-driven through local InPost/Apaczka/Allegro WZA flow and keep writing `shipment_packages`. |
|
||||||
|
| AC-5: External Parcel Is Registered In Erli | Pass with live smoke pending | `ErliExternalShipmentService` registers external parcels only after tracking exists and logs non-critical errors. |
|
||||||
|
| AC-6: Documentation And Verification Cover The Flow | Pass with env gaps | PHP lint and diff checks passed; PHPUnit and Sonar are unavailable in this environment. |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Extended generic carrier delivery mappings with Erli-specific source service/vendor metadata.
|
||||||
|
- Added an Erli delivery tab that maps Erli delivery methods to local label providers and Erli vendor codes.
|
||||||
|
- Added native Erli shipping dictionary and external parcel client methods.
|
||||||
|
- Wired Erli shipment preparation into the existing local label flow.
|
||||||
|
- Added non-blocking external parcel sync to Erli after local tracking numbers become available.
|
||||||
|
- Updated database, architecture and technical changelog documentation.
|
||||||
|
|
||||||
|
## Task Results
|
||||||
|
|
||||||
|
| Task | Result | Commit |
|
||||||
|
|------|--------|--------|
|
||||||
|
| Task 1: Extend Erli API and mapping repository for shipping | Done | Phase commit |
|
||||||
|
| Task 2: Add Erli delivery mapping tab | Done | Phase commit |
|
||||||
|
| Task 3: Use mappings during shipment creation and sync external parcel to Erli | Done | Phase commit |
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `database/migrations/20260516_000117_extend_delivery_mappings_for_erli_shipping.sql` | Adds Erli source service/vendor metadata to delivery mappings. |
|
||||||
|
| `src/Modules/Settings/ErliDeliveryMappingController.php` | Loads and saves Erli delivery mapping tab data. |
|
||||||
|
| `src/Modules/Settings/ErliExternalShipmentService.php` | Registers local tracking numbers as Erli external parcels. |
|
||||||
|
| `tests/Unit/ErliExternalShipmentServiceTest.php` | Focused unit coverage for Erli external parcel sync behavior. |
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/Modules/Settings/ErliApiClient.php` | Added shipping dictionaries, price lists and external parcel API calls. |
|
||||||
|
| `src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php` | Added Erli source support and source vendor/service persistence. |
|
||||||
|
| `src/Modules/Settings/ErliIntegrationController.php` | Added delivery tab data and tab routing. |
|
||||||
|
| `src/Modules/Shipments/ShipmentController.php` | Uses Erli delivery mappings and triggers external parcel sync after tracking exists. |
|
||||||
|
| `routes/web.php` | Wires Erli delivery save route and external shipment service dependencies. |
|
||||||
|
| `resources/views/settings/erli.php` | Adds Erli delivery tab UI. |
|
||||||
|
| `resources/views/shipments/prepare.php` | Preselects mapped local provider/service for Erli shipments. |
|
||||||
|
| `resources/lang/pl.php` | Adds Polish labels for Erli delivery mapping UI. |
|
||||||
|
| `DOCS/DB_SCHEMA.md` | Documents mapping columns. |
|
||||||
|
| `DOCS/ARCHITECTURE.md` | Documents Erli shipment flow. |
|
||||||
|
| `DOCS/TECH_CHANGELOG.md` | Records Phase 130 technical changes. |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| Use local label providers for Erli labels | Operator does not want to ship on Erli's carrier agreement; existing provider labels are the source of truth. |
|
||||||
|
| Register Erli parcels through `POST /shipping/external` | Official Erli API supports external parcels with order id, vendor, status and tracking number. |
|
||||||
|
| Store Erli vendor separately from local provider service | Erli vendor and local provider/service are different contracts and should not overload the same field. |
|
||||||
|
| Make Erli external parcel sync non-critical | Local label generation must survive Erli API failures. |
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
| Type | Description | Impact |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| Scope addition | Added a small migration because the existing mapping schema could not cleanly store Erli vendor separately from the local provider service. | Low risk, improves contract clarity. |
|
||||||
|
| Implementation detail | Native InPost service data is preferred for Erli/InPost mappings where available, with existing Allegro WZA filtered service list as fallback. | Keeps Erli local-provider flow independent from Allegro where possible. |
|
||||||
|
| Verification gap | PHPUnit binary is missing and `sonar-scanner` is not available in PATH. | Tests exist but could not be executed locally. |
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| Erli `deliveryVendors` dictionary can return scalar rows | Normalization now preserves scalar values as both id and name. |
|
||||||
|
| MySQL `CREATE INDEX IF NOT EXISTS` support is environment-sensitive | Migration avoids that syntax and only adds required columns. |
|
||||||
|
| External parcel duplicate behavior could not be live-tested | Service stores successful sync payload and treats sync failures as non-critical activity entries. |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| PHP syntax lint for changed PHP/view/test files | Passed |
|
||||||
|
| `git diff --check -- . ':!.vscode/ftp-kr.sync.cache.json'` | Passed |
|
||||||
|
| `vendor/bin/phpunit tests/Unit/ErliExternalShipmentServiceTest.php` | Not run: `vendor/bin/phpunit` missing |
|
||||||
|
| `sonar-scanner --version` | Not run: command unavailable in PATH |
|
||||||
|
| Manual Erli delivery tab smoke | Pending operator after migration/live configuration |
|
||||||
|
| Manual Erli label + external parcel smoke | Pending operator after migration/live configuration |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
Phase 131 can build on:
|
||||||
|
- Erli delivery mappings with local provider and Erli vendor metadata.
|
||||||
|
- Local shipment packages with tracking numbers.
|
||||||
|
- Non-critical Erli external parcel sync payloads stored in `shipment_packages.payload_json`.
|
||||||
|
|
||||||
|
Remaining concerns for Phase 131:
|
||||||
|
- Live Erli credentials, migration execution and browser smoke are still pending operator environment.
|
||||||
|
- Delivery tracking automation should decide how to poll/update delivery status after the external parcel exists.
|
||||||
|
- Duplicate external parcel semantics should be confirmed against live Erli responses.
|
||||||
|
|
||||||
|
No blocker prevents planning Phase 131.
|
||||||
@@ -84,6 +84,8 @@ HTTP Request
|
|||||||
5. **Render** — `resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
|
5. **Render** — `resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
|
||||||
|
|
||||||
### Shipment Flow
|
### Shipment Flow
|
||||||
|
|
||||||
|
Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `ShipmentController` calls `ErliExternalShipmentService::syncPackage()` after a local provider has a tracking number. The service posts `vendor/status/trackingNumber/orderId` to Erli `POST /shipping/external` and stores the response in `shipment_packages.payload_json.erli_external_parcel`; failures are activity-log warnings and do not block local labels.
|
||||||
1. **Create** — `ShipmentController::create()` → `ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()` → `ShipmentPackageRepository::insert()`
|
1. **Create** — `ShipmentController::create()` → `ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()` → `ShipmentPackageRepository::insert()`
|
||||||
2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
|
2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
|
||||||
|
|
||||||
@@ -121,12 +123,14 @@ HTTP Request
|
|||||||
|
|
||||||
### Erli Integration Foundation
|
### Erli Integration Foundation
|
||||||
|
|
||||||
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.
|
1. **Settings** - `/settings/integrations/erli` renders tabbed integration/status/delivery/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. **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`.
|
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.
|
6. **Delivery mapping and labels** - Phase 130 adds `ErliDeliveryMappingController` and a Delivery tab. It maps imported Erli delivery method labels to local shipment providers (`inpost`/`apaczka`) and stores Erli `source_vendor_code` for external parcel registration. Label files are still produced by local providers, not by Erli carrier-contract parcels.
|
||||||
|
7. **External shipment sync** - Phase 130 extends `ErliApiClient` with shipping dictionary calls and `createExternalParcel()` (`POST /shipping/external`). `ErliExternalShipmentService` registers a local package in Erli only after `shipment_packages.tracking_number` exists; failures are activity-log warnings and do not block local labels.
|
||||||
|
8. **Deferred** - Carrier tracking automation and broader delivery-status hooks are planned for v3.8 Phase 131.
|
||||||
|
|
||||||
## Dependency Injection
|
## Dependency Injection
|
||||||
|
|
||||||
@@ -187,15 +191,25 @@ tests/
|
|||||||
- `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.
|
- Phase 129: `updateOrderStatus()` wysyla `PATCH /orders/{id}/status` z body `{"status": "..."}` dla recznych zmian statusu orderPRO mapowanych na status Erli.
|
||||||
|
- Phase 130: `getShippingMethods()`, `getDeliveryMethods()`, `getDeliveryVendors()`, `getPriceLists()`, `getPriceListsDetails()` i `createExternalParcel()` obsluguja natywne shipping API Erli bez wymuszania nadawania przez umowe 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`, `POST /settings/integrations/erli/statuses/save-pull`, `POST /settings/integrations/erli/statuses/save-push`.
|
- 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`, `POST /settings/integrations/erli/delivery/save`.
|
||||||
- `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()`.
|
- `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.
|
||||||
|
|
||||||
|
### ErliDeliveryMappingController (`src/Modules/Settings/ErliDeliveryMappingController.php`)
|
||||||
|
- Buduje dane zakladki Dostawy: metody z `orders.external_carrier_id` dla `source='erli'`, aktualne `carrier_delivery_method_mappings`, slowniki vendorow/metod z Erli oraz lokalne uslugi InPost/Apaczka.
|
||||||
|
- `saveDeliveryMappings()` zapisuje mapowanie globalne `source_system='erli'`, `source_integration_id=0` z lokalnym providerem oraz `source_vendor_code` wymaganym przez Erli `POST /shipping/external`.
|
||||||
|
|
||||||
|
### ErliExternalShipmentService (`src/Modules/Settings/ErliExternalShipmentService.php`)
|
||||||
|
- `syncPackage(int $packageId)` sprawdza, czy paczka nalezy do zamowienia Erli i ma tracking number.
|
||||||
|
- Pobiera vendor Erli z mapowania dostawy albo proboje go wywnioskowac z lokalnego providera/carrier id.
|
||||||
|
- Wysyla `POST /shipping/external` z `orderId`, `vendor`, `status='sent'`, `trackingNumber`; sukces zapisuje w `shipment_packages.payload_json.erli_external_parcel`, blad trafia do activity log jako niekrytyczny.
|
||||||
|
|
||||||
### ErliOrdersSyncService / ErliOrderMapper (`src/Modules/Settings/`)
|
### ErliOrdersSyncService / 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Database Schema
|
# Database Schema
|
||||||
|
|
||||||
**Updated:** 2026-05-15 | **Total tables:** 63 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
**Updated:** 2026-05-16 | **Total tables:** 63 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -385,6 +385,26 @@ UNIQUE: `(integration_id, shoppro_status_code)`
|
|||||||
|
|
||||||
## Shipments & Delivery
|
## Shipments & Delivery
|
||||||
|
|
||||||
|
**carrier_delivery_method_mappings** — Map marketplace delivery methods to local shipment providers
|
||||||
|
| Column | Type | Nullable | Notes |
|
||||||
|
|--------|------|----------|-------|
|
||||||
|
| `id` | INT UNSIGNED | NO | PK |
|
||||||
|
| `source_system` | VARCHAR(32) | NO | `allegro`, `shoppro`, `erli` |
|
||||||
|
| `source_integration_id` | INT UNSIGNED | NO | `0` for global Allegro/Erli, shopPRO integration id for shopPRO |
|
||||||
|
| `order_delivery_method` | VARCHAR(200) | NO | Delivery method label/code from imported order |
|
||||||
|
| `source_service_id` | VARCHAR(128) | YES | Source delivery/shipping method id when available; used by Erli shipping dictionaries |
|
||||||
|
| `source_vendor_code` | VARCHAR(64) | YES | Source carrier/vendor code; Erli uses this for `POST /shipping/external.vendor` |
|
||||||
|
| `provider` | VARCHAR(50) | NO | Local shipment provider, e.g. `allegro_wza`, `inpost`, `apaczka` |
|
||||||
|
| `provider_service_id` | VARCHAR(128) | NO | Local provider service id |
|
||||||
|
| `provider_account_id` | VARCHAR(128) | YES | Provider account/credentials id when required |
|
||||||
|
| `provider_carrier_id` | VARCHAR(128) | YES | Provider carrier code/id when required |
|
||||||
|
| `provider_service_name` | VARCHAR(255) | YES | Display snapshot of provider service name |
|
||||||
|
| `created_at` | DATETIME | NO | |
|
||||||
|
| `updated_at` | DATETIME | NO | |
|
||||||
|
|
||||||
|
UNIQUE: `(source_system, source_integration_id, order_delivery_method)`.
|
||||||
|
Phase 130 adds `source_service_id` and `source_vendor_code`; no new table is required for Erli delivery mappings.
|
||||||
|
|
||||||
**shipment_packages** — Prepared shipments with tracking
|
**shipment_packages** — Prepared shipments with tracking
|
||||||
| Column | Type | Nullable | Notes |
|
| Column | Type | Nullable | Notes |
|
||||||
|--------|------|----------|-------|
|
|--------|------|----------|-------|
|
||||||
@@ -414,7 +434,7 @@ UNIQUE: `(integration_id, shoppro_status_code)`
|
|||||||
| `sender_point_id` | VARCHAR(64) | YES | |
|
| `sender_point_id` | VARCHAR(64) | YES | |
|
||||||
| `reference_number` | VARCHAR(128) | YES | |
|
| `reference_number` | VARCHAR(128) | YES | |
|
||||||
| `error_message` | TEXT | YES | |
|
| `error_message` | TEXT | YES | |
|
||||||
| `payload_json` | JSON | YES | |
|
| `payload_json` | JSON | YES | Provider request/response snapshot; Phase 130 may add `erli_external_parcel` after successful Erli external shipment registration |
|
||||||
| `created_at` | DATETIME | NO | |
|
| `created_at` | DATETIME | NO | |
|
||||||
| `updated_at` | DATETIME | NO | |
|
| `updated_at` | DATETIME | NO | |
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Technical Changelog
|
# Technical Changelog
|
||||||
|
|
||||||
|
## 2026-05-16 - Phase 130 Plan 01: Erli Shipments + Labels
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Rozszerzono `carrier_delivery_method_mappings` o `source_service_id` i `source_vendor_code`, zeby mapowanie Erli przechowywalo osobno metode marketplace i vendora wymaganego przez `POST /shipping/external`.
|
||||||
|
- Dodano zakladke Dostawy w `/settings/integrations/erli` oraz `ErliDeliveryMappingController` do mapowania metod z zamowien Erli na lokalne uslugi InPost/Apaczka.
|
||||||
|
- Rozszerzono `ErliApiClient` o slowniki shipping/delivery/vendor/cenniki i tworzenie paczek zewnetrznych przez `POST /shipping/external`.
|
||||||
|
- `ShipmentController` uzywa mapowan Erli przy przygotowaniu przesylki i po uzyskaniu tracking number wywoluje `ErliExternalShipmentService`.
|
||||||
|
- `ErliExternalShipmentService` rejestruje paczke zewnetrzna w Erli, zapisuje odpowiedz w `shipment_packages.payload_json.erli_external_parcel` i loguje bledy jako niekrytyczne.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Operator chce nadawac zamowienia Erli z orderPRO, ale bez wymuszania nadawania na umowie Erli. Etykiety pozostaja u lokalnych providerow, a Erli dostaje informację o zewnetrznej paczce/tracking number.
|
||||||
|
|
||||||
|
**BREAKING / migracja:**
|
||||||
|
- Wymagana migracja `20260516_000117_extend_delivery_mappings_for_erli_shipping.sql`.
|
||||||
|
- Brak breaking changes w istniejacych mapowaniach Allegro/shopPRO; nowe kolumny sa opcjonalne.
|
||||||
|
|
||||||
## 2026-05-16 - Erli Settings Tabs UI Fix
|
## 2026-05-16 - Erli Settings Tabs UI Fix
|
||||||
|
|
||||||
**Co zrobiono:**
|
**Co zrobiono:**
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE carrier_delivery_method_mappings
|
||||||
|
ADD COLUMN IF NOT EXISTS source_service_id VARCHAR(128) NULL AFTER order_delivery_method,
|
||||||
|
ADD COLUMN IF NOT EXISTS source_vendor_code VARCHAR(64) NULL AFTER source_service_id;
|
||||||
@@ -867,6 +867,7 @@ return [
|
|||||||
'label' => 'Zakladki integracji Erli',
|
'label' => 'Zakladki integracji Erli',
|
||||||
'integration' => 'Integracja',
|
'integration' => 'Integracja',
|
||||||
'statuses' => 'Statusy',
|
'statuses' => 'Statusy',
|
||||||
|
'delivery' => 'Dostawy',
|
||||||
'settings' => 'Ustawienia',
|
'settings' => 'Ustawienia',
|
||||||
],
|
],
|
||||||
'config' => [
|
'config' => [
|
||||||
@@ -925,6 +926,29 @@ return [
|
|||||||
'save_failed' => 'Nie udalo sie zapisac mapowan statusow Erli.',
|
'save_failed' => 'Nie udalo sie zapisac mapowan statusow Erli.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'delivery' => [
|
||||||
|
'title' => 'Mapowanie dostaw Erli',
|
||||||
|
'description' => 'Przypisz metody dostawy z zamowien Erli do lokalnych providerow etykiet oraz vendora Erli uzywanego przy rejestracji paczki zewnetrznej.',
|
||||||
|
'metadata_not_connected' => 'Zapisz aktywna konfiguracje Erli, aby pobrac slowniki dostaw i vendorow.',
|
||||||
|
'empty_methods' => 'Brak metod dostawy Erli z zaimportowanych zamowien.',
|
||||||
|
'shipping_methods_title' => 'Slownik metod wysylki Erli',
|
||||||
|
'fields' => [
|
||||||
|
'order_method' => 'Metoda z zamowienia',
|
||||||
|
'erli_vendor' => 'Vendor Erli',
|
||||||
|
'provider_service' => 'Lokalna usluga etykiety',
|
||||||
|
'no_vendor' => '-- wybierz vendora --',
|
||||||
|
'no_provider' => '-- nie generuj etykiety --',
|
||||||
|
'shipping_method' => 'Metoda Erli',
|
||||||
|
'shipping_vendor' => 'Vendor',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'save' => 'Zapisz mapowanie dostaw',
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'saved' => 'Mapowanie dostaw Erli zostalo zapisane.',
|
||||||
|
'save_failed' => 'Nie udalo sie zapisac mapowania dostaw Erli.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'status' => [
|
'status' => [
|
||||||
'secret' => 'Sekret API',
|
'secret' => 'Sekret API',
|
||||||
'active' => 'Aktywna',
|
'active' => 'Aktywna',
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ $statusSyncIntervalMinutes = (int) ($statusSyncIntervalMinutes ?? 15);
|
|||||||
$orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : [];
|
$orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : [];
|
||||||
$erliStatusMappings = is_array($erliStatusMappings ?? null) ? $erliStatusMappings : [];
|
$erliStatusMappings = is_array($erliStatusMappings ?? null) ? $erliStatusMappings : [];
|
||||||
$erliPullStatusMappings = is_array($erliPullStatusMappings ?? null) ? $erliPullStatusMappings : [];
|
$erliPullStatusMappings = is_array($erliPullStatusMappings ?? null) ? $erliPullStatusMappings : [];
|
||||||
|
$erliDeliveryOrderMethods = is_array($erliDeliveryOrderMethods ?? null) ? $erliDeliveryOrderMethods : [];
|
||||||
|
$erliDeliveryMappings = is_array($erliDeliveryMappings ?? null) ? $erliDeliveryMappings : [];
|
||||||
|
$erliShippingMethods = is_array($erliShippingMethods ?? null) ? $erliShippingMethods : [];
|
||||||
|
$erliDeliveryVendors = is_array($erliDeliveryVendors ?? null) ? $erliDeliveryVendors : [];
|
||||||
|
$inpostDeliveryServices = is_array($inpostDeliveryServices ?? null) ? $inpostDeliveryServices : [];
|
||||||
|
$apaczkaDeliveryServices = is_array($apaczkaDeliveryServices ?? null) ? $apaczkaDeliveryServices : [];
|
||||||
|
$erliDeliveryMetadataError = trim((string) ($erliDeliveryMetadataError ?? ''));
|
||||||
|
$erliDeliveryMappingsByMethod = [];
|
||||||
|
foreach ($erliDeliveryMappings as $mappingRow) {
|
||||||
|
$methodKey = trim((string) ($mappingRow['order_delivery_method'] ?? ''));
|
||||||
|
if ($methodKey !== '') {
|
||||||
|
$erliDeliveryMappingsByMethod[$methodKey] = $mappingRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
$activeTab = (string) ($activeTab ?? 'integration');
|
$activeTab = (string) ($activeTab ?? 'integration');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -47,6 +61,9 @@ $activeTab = (string) ($activeTab ?? 'integration');
|
|||||||
<button type="button" class="content-tab-btn<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-target="erli-tab-statuses">
|
<button type="button" class="content-tab-btn<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-target="erli-tab-statuses">
|
||||||
<?= $e($t('settings.erli.tabs.statuses')) ?>
|
<?= $e($t('settings.erli.tabs.statuses')) ?>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="content-tab-btn<?= $activeTab === 'delivery' ? ' is-active' : '' ?>" data-tab-target="erli-tab-delivery">
|
||||||
|
<?= $e($t('settings.erli.tabs.delivery')) ?>
|
||||||
|
</button>
|
||||||
<button type="button" class="content-tab-btn<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-target="erli-tab-settings">
|
<button type="button" class="content-tab-btn<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-target="erli-tab-settings">
|
||||||
<?= $e($t('settings.erli.tabs.settings')) ?>
|
<?= $e($t('settings.erli.tabs.settings')) ?>
|
||||||
</button>
|
</button>
|
||||||
@@ -261,6 +278,149 @@ $activeTab = (string) ($activeTab ?? 'integration');
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="content-tab-panel<?= $activeTab === 'delivery' ? ' is-active' : '' ?>" data-tab-panel="erli-tab-delivery">
|
||||||
|
<section class="mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.erli.delivery.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.erli.delivery.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if ($erliDeliveryMetadataError !== ''): ?>
|
||||||
|
<div class="mt-12"><?php $type='warning'; $message=(string) $erliDeliveryMetadataError; $dismissible=true; include dirname(__DIR__) . '/components/alert.php'; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form action="/settings/integrations/erli/delivery/save" method="post" class="mt-12">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.erli.delivery.fields.order_method')) ?></th>
|
||||||
|
<th><?= $e($t('settings.erli.delivery.fields.erli_vendor')) ?></th>
|
||||||
|
<th><?= $e($t('settings.erli.delivery.fields.provider_service')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($erliDeliveryOrderMethods === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="muted"><?= $e($t('settings.erli.delivery.empty_methods')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($erliDeliveryOrderMethods as $orderMethod): ?>
|
||||||
|
<?php
|
||||||
|
$orderMethod = trim((string) $orderMethod);
|
||||||
|
if ($orderMethod === '') continue;
|
||||||
|
$mappingRow = is_array($erliDeliveryMappingsByMethod[$orderMethod] ?? null) ? $erliDeliveryMappingsByMethod[$orderMethod] : [];
|
||||||
|
$selectedVendor = trim((string) ($mappingRow['source_vendor_code'] ?? ''));
|
||||||
|
$selectedProvider = trim((string) ($mappingRow['provider'] ?? ''));
|
||||||
|
$selectedProviderServiceId = trim((string) ($mappingRow['provider_service_id'] ?? ''));
|
||||||
|
$selectedProviderCarrierId = trim((string) ($mappingRow['provider_carrier_id'] ?? ''));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><?= $e($orderMethod) ?></strong>
|
||||||
|
<input type="hidden" name="order_delivery_method[]" value="<?= $e($orderMethod) ?>">
|
||||||
|
<input type="hidden" name="source_service_id[]" value="<?= $e((string) ($mappingRow['source_service_id'] ?? '')) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="source_vendor_code[]">
|
||||||
|
<option value=""><?= $e($t('settings.erli.delivery.fields.no_vendor')) ?></option>
|
||||||
|
<?php foreach ($erliDeliveryVendors as $vendor): ?>
|
||||||
|
<?php
|
||||||
|
$vendorCode = trim((string) ($vendor['id'] ?? $vendor['code'] ?? $vendor['vendor'] ?? ''));
|
||||||
|
if ($vendorCode === '') continue;
|
||||||
|
$vendorName = trim((string) ($vendor['name'] ?? $vendorCode));
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($vendorCode) ?>"<?= $selectedVendor === $vendorCode ? ' selected' : '' ?>>
|
||||||
|
<?= $e($vendorName) ?> (<?= $e($vendorCode) ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="provider_service_choice[]">
|
||||||
|
<option value=""><?= $e($t('settings.erli.delivery.fields.no_provider')) ?></option>
|
||||||
|
<?php if ($inpostDeliveryServices !== []): ?>
|
||||||
|
<optgroup label="InPost">
|
||||||
|
<?php foreach ($inpostDeliveryServices as $service): ?>
|
||||||
|
<?php
|
||||||
|
$serviceId = trim((string) ($service['id'] ?? ''));
|
||||||
|
if ($serviceId === '') continue;
|
||||||
|
$serviceName = trim((string) ($service['name'] ?? $serviceId));
|
||||||
|
$carrierId = 'inpost';
|
||||||
|
$choice = 'inpost|' . $serviceId . '|' . $carrierId . '|' . $serviceName;
|
||||||
|
$isSelected = $selectedProvider === 'inpost' && $selectedProviderServiceId === $serviceId;
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($choice) ?>"<?= $isSelected ? ' selected' : '' ?>>
|
||||||
|
<?= $e($serviceName) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</optgroup>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($apaczkaDeliveryServices !== []): ?>
|
||||||
|
<optgroup label="Apaczka">
|
||||||
|
<?php foreach ($apaczkaDeliveryServices as $service): ?>
|
||||||
|
<?php
|
||||||
|
if (!is_array($service)) continue;
|
||||||
|
$serviceId = trim((string) ($service['service_id'] ?? $service['id'] ?? ''));
|
||||||
|
if ($serviceId === '') continue;
|
||||||
|
$serviceName = trim((string) ($service['name'] ?? ('ID ' . $serviceId)));
|
||||||
|
$carrierId = trim((string) ($service['carrier_code'] ?? ''));
|
||||||
|
$choice = 'apaczka|' . $serviceId . '|' . $carrierId . '|' . $serviceName;
|
||||||
|
$isSelected = $selectedProvider === 'apaczka'
|
||||||
|
&& $selectedProviderServiceId === $serviceId
|
||||||
|
&& ($selectedProviderCarrierId === '' || $selectedProviderCarrierId === $carrierId);
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($choice) ?>"<?= $isSelected ? ' selected' : '' ?>>
|
||||||
|
<?= $e($serviceName) ?><?= $carrierId !== '' ? ' [' . $e($carrierId) . ']' : '' ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</optgroup>
|
||||||
|
<?php endif; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($erliDeliveryOrderMethods !== []): ?>
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.erli.delivery.actions.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php if ($erliShippingMethods !== []): ?>
|
||||||
|
<h3 class="section-title mt-16"><?= $e($t('settings.erli.delivery.shipping_methods_title')) ?></h3>
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.erli.delivery.fields.shipping_method')) ?></th>
|
||||||
|
<th><?= $e($t('settings.erli.delivery.fields.shipping_vendor')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($erliShippingMethods as $method): ?>
|
||||||
|
<?php
|
||||||
|
$methodId = trim((string) ($method['id'] ?? $method['typeId'] ?? ''));
|
||||||
|
$methodName = trim((string) ($method['name'] ?? $methodId));
|
||||||
|
$methodVendor = trim((string) ($method['vendor'] ?? ''));
|
||||||
|
if ($methodId === '' && $methodName === '') continue;
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $e($methodName) ?><?= $methodId !== '' ? ' <code class="muted">' . $e($methodId) . '</code>' : '' ?></td>
|
||||||
|
<td><?= $e($methodVendor !== '' ? $methodVendor : '-') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="content-tab-panel<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-panel="erli-tab-settings">
|
<div class="content-tab-panel<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-panel="erli-tab-settings">
|
||||||
<section class="mt-16">
|
<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>
|
||||||
@@ -315,6 +475,7 @@ $activeTab = (string) ($activeTab ?? 'integration');
|
|||||||
var tabNameMap = {
|
var tabNameMap = {
|
||||||
'erli-tab-integration': 'integration',
|
'erli-tab-integration': 'integration',
|
||||||
'erli-tab-statuses': 'statuses',
|
'erli-tab-statuses': 'statuses',
|
||||||
|
'erli-tab-delivery': 'delivery',
|
||||||
'erli-tab-settings': 'settings'
|
'erli-tab-settings': 'settings'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ $mappedCredentialsId = trim((string) ($mapping['provider_account_id'] ?? ''));
|
|||||||
$mappedCarrierId = trim((string) ($mapping['provider_carrier_id'] ?? ''));
|
$mappedCarrierId = trim((string) ($mapping['provider_carrier_id'] ?? ''));
|
||||||
$mappedProvider = trim((string) ($mapping['provider'] ?? ''));
|
$mappedProvider = trim((string) ($mapping['provider'] ?? ''));
|
||||||
$mappedServiceName = trim((string) ($mapping['provider_service_name'] ?? ''));
|
$mappedServiceName = trim((string) ($mapping['provider_service_name'] ?? ''));
|
||||||
$mappedCarrier = $mappedProvider === 'apaczka' ? 'apaczka' : 'allegro';
|
$mappedCarrier = match ($mappedProvider) {
|
||||||
|
'apaczka' => 'apaczka',
|
||||||
|
'inpost' => 'inpost',
|
||||||
|
default => 'allegro',
|
||||||
|
};
|
||||||
if ($mappedCarrier !== 'apaczka' && stripos($mappedCarrierId, 'inpost') !== false) {
|
if ($mappedCarrier !== 'apaczka' && stripos($mappedCarrierId, 'inpost') !== false) {
|
||||||
$mappedCarrier = 'inpost';
|
$mappedCarrier = 'inpost';
|
||||||
}
|
}
|
||||||
@@ -144,16 +148,16 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
|
|
||||||
<div id="shipment-inpost-panel" style="<?= $preselectedCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
<div id="shipment-inpost-panel" style="<?= $preselectedCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||||
<?php if ($inpostSvcList === []): ?>
|
<?php if ($inpostSvcList === []): ?>
|
||||||
<div class="muted">Brak uslug InPost (sprawdz polaczenie z Allegro).</div>
|
<div class="muted">Brak uslug InPost (sprawdz konfiguracje InPost).</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<select class="form-control" id="shipment-inpost-select">
|
<select class="form-control" id="shipment-inpost-select">
|
||||||
<option value="">-- Wybierz usluge InPost --</option>
|
<option value="">-- Wybierz usluge InPost --</option>
|
||||||
<?php foreach ($inpostSvcList as $inSvc): ?>
|
<?php foreach ($inpostSvcList as $inSvc): ?>
|
||||||
<?php
|
<?php
|
||||||
$inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : [];
|
$inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : [];
|
||||||
$inSvcMethodId = trim((string) ($inSvcId['deliveryMethodId'] ?? ''));
|
$inSvcMethodId = trim((string) ($inSvcId['deliveryMethodId'] ?? ($inSvc['id'] ?? '')));
|
||||||
$inSvcCredentialsId = trim((string) ($inSvcId['credentialsId'] ?? ''));
|
$inSvcCredentialsId = trim((string) ($inSvcId['credentialsId'] ?? ''));
|
||||||
$inSvcCarrierId = trim((string) ($inSvc['carrierId'] ?? ''));
|
$inSvcCarrierId = trim((string) ($inSvc['carrierId'] ?? 'inpost'));
|
||||||
$inSvcName = trim((string) ($inSvc['name'] ?? ''));
|
$inSvcName = trim((string) ($inSvc['name'] ?? ''));
|
||||||
$inSvcSelected = $mappedCarrier === 'inpost' && $mappedMethodId === $inSvcMethodId;
|
$inSvcSelected = $mappedCarrier === 'inpost' && $mappedMethodId === $inSvcMethodId;
|
||||||
?>
|
?>
|
||||||
@@ -214,7 +218,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
|
|
||||||
<input type="hidden" name="credentials_id" id="shipment-credentials-id" value="">
|
<input type="hidden" name="credentials_id" id="shipment-credentials-id" value="">
|
||||||
<input type="hidden" name="carrier_id" id="shipment-carrier-id" value="">
|
<input type="hidden" name="carrier_id" id="shipment-carrier-id" value="">
|
||||||
<input type="hidden" name="provider_code" id="shipment-provider-code" value="<?= $e($preselectedCarrier === 'apaczka' ? 'apaczka' : 'allegro_wza') ?>">
|
<input type="hidden" name="provider_code" id="shipment-provider-code" value="<?= $e($preselectedCarrier === 'apaczka' ? 'apaczka' : ($preselectedCarrier === 'inpost' ? 'inpost' : 'allegro_wza')) ?>">
|
||||||
|
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span class="field-label">Typ paczki</span>
|
<span class="field-label">Typ paczki</span>
|
||||||
@@ -615,7 +619,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||||
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
||||||
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||||
if (providerInput) providerInput.value = carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
|
if (providerInput) providerInput.value = carrier === 'apaczka' ? 'apaczka' : (carrier === 'inpost' ? 'inpost' : 'allegro_wza');
|
||||||
}
|
}
|
||||||
|
|
||||||
var weekendWrap = document.getElementById('shipment-apaczka-weekend-wrap');
|
var weekendWrap = document.getElementById('shipment-apaczka-weekend-wrap');
|
||||||
@@ -655,7 +659,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
hiddenInput.value = inpostSelect.value;
|
hiddenInput.value = inpostSelect.value;
|
||||||
credentialsInput.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
credentialsInput.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||||
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||||
if (providerInput) providerInput.value = 'allegro_wza';
|
if (providerInput) providerInput.value = 'inpost';
|
||||||
}
|
}
|
||||||
inpostSelect.addEventListener('change', syncInpostFields);
|
inpostSelect.addEventListener('change', syncInpostFields);
|
||||||
if (carrierSelect.value === 'inpost' && inpostSelect.value !== '') {
|
if (carrierSelect.value === 'inpost' && inpostSelect.value !== '') {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ use App\Modules\Settings\ApaczkaIntegrationController;
|
|||||||
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
||||||
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
||||||
use App\Modules\Settings\ErliApiClient;
|
use App\Modules\Settings\ErliApiClient;
|
||||||
|
use App\Modules\Settings\ErliDeliveryMappingController;
|
||||||
|
use App\Modules\Settings\ErliExternalShipmentService;
|
||||||
use App\Modules\Settings\ErliIntegrationController;
|
use App\Modules\Settings\ErliIntegrationController;
|
||||||
use App\Modules\Settings\ErliIntegrationRepository;
|
use App\Modules\Settings\ErliIntegrationRepository;
|
||||||
use App\Modules\Settings\ErliOrderMapper;
|
use App\Modules\Settings\ErliOrderMapper;
|
||||||
@@ -397,19 +399,6 @@ return static function (Application $app): void {
|
|||||||
$automationService,
|
$automationService,
|
||||||
$erliPullStatusMappingRepository
|
$erliPullStatusMappingRepository
|
||||||
);
|
);
|
||||||
$erliIntegrationController = new ErliIntegrationController(
|
|
||||||
$template,
|
|
||||||
$translator,
|
|
||||||
$auth,
|
|
||||||
$erliIntegrationRepository,
|
|
||||||
new ErliApiClient(),
|
|
||||||
new IntegrationsRepository($app->db()),
|
|
||||||
$cronRepository,
|
|
||||||
$erliOrdersSyncService,
|
|
||||||
$app->orderStatuses(),
|
|
||||||
$erliStatusMappingRepository,
|
|
||||||
$erliPullStatusMappingRepository
|
|
||||||
);
|
|
||||||
$allegroIntegrationController = new AllegroIntegrationController(
|
$allegroIntegrationController = new AllegroIntegrationController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -498,6 +487,35 @@ return static function (Application $app): void {
|
|||||||
$apaczkaShipmentService,
|
$apaczkaShipmentService,
|
||||||
$inpostShipmentService,
|
$inpostShipmentService,
|
||||||
]);
|
]);
|
||||||
|
$erliDeliveryMappingController = new ErliDeliveryMappingController(
|
||||||
|
$translator,
|
||||||
|
$carrierDeliveryMappings,
|
||||||
|
$erliIntegrationRepository,
|
||||||
|
new ErliApiClient(),
|
||||||
|
$inpostShipmentService,
|
||||||
|
$apaczkaShipmentService
|
||||||
|
);
|
||||||
|
$erliExternalShipmentService = new ErliExternalShipmentService(
|
||||||
|
$erliIntegrationRepository,
|
||||||
|
new ErliApiClient(),
|
||||||
|
$carrierDeliveryMappings,
|
||||||
|
$shipmentPackageRepository,
|
||||||
|
new OrdersRepository($app->db())
|
||||||
|
);
|
||||||
|
$erliIntegrationController = new ErliIntegrationController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
$erliIntegrationRepository,
|
||||||
|
new ErliApiClient(),
|
||||||
|
new IntegrationsRepository($app->db()),
|
||||||
|
$cronRepository,
|
||||||
|
$erliOrdersSyncService,
|
||||||
|
$app->orderStatuses(),
|
||||||
|
$erliStatusMappingRepository,
|
||||||
|
$erliPullStatusMappingRepository,
|
||||||
|
$erliDeliveryMappingController
|
||||||
|
);
|
||||||
$shipmentController = new ShipmentController(
|
$shipmentController = new ShipmentController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -509,7 +527,8 @@ return static function (Application $app): void {
|
|||||||
$automationService,
|
$automationService,
|
||||||
$app->basePath('storage'),
|
$app->basePath('storage'),
|
||||||
$carrierDeliveryMappings,
|
$carrierDeliveryMappings,
|
||||||
$printJobRepository
|
$printJobRepository,
|
||||||
|
$erliExternalShipmentService
|
||||||
);
|
);
|
||||||
$authMiddleware = new AuthMiddleware($auth);
|
$authMiddleware = new AuthMiddleware($auth);
|
||||||
|
|
||||||
@@ -660,6 +679,7 @@ return static function (Application $app): void {
|
|||||||
$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-pull', [$erliIntegrationController, 'savePullStatusMappings'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/erli/statuses/save-push', [$erliIntegrationController, 'savePushStatusMappings'], [$authMiddleware]);
|
$router->post('/settings/integrations/erli/statuses/save-push', [$erliIntegrationController, 'savePushStatusMappings'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/erli/delivery/save', [$erliDeliveryMappingController, 'saveDeliveryMappings'], [$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]);
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ final class CarrierDeliveryMethodMappingRepository
|
|||||||
source_system,
|
source_system,
|
||||||
source_integration_id,
|
source_integration_id,
|
||||||
order_delivery_method,
|
order_delivery_method,
|
||||||
|
source_service_id,
|
||||||
|
source_vendor_code,
|
||||||
provider,
|
provider,
|
||||||
provider_service_id,
|
provider_service_id,
|
||||||
provider_account_id,
|
provider_account_id,
|
||||||
@@ -114,6 +116,8 @@ final class CarrierDeliveryMethodMappingRepository
|
|||||||
:source_system,
|
:source_system,
|
||||||
:source_integration_id,
|
:source_integration_id,
|
||||||
:order_delivery_method,
|
:order_delivery_method,
|
||||||
|
:source_service_id,
|
||||||
|
:source_vendor_code,
|
||||||
:provider,
|
:provider,
|
||||||
:provider_service_id,
|
:provider_service_id,
|
||||||
:provider_account_id,
|
:provider_account_id,
|
||||||
@@ -135,6 +139,8 @@ final class CarrierDeliveryMethodMappingRepository
|
|||||||
'source_system' => $normalizedSource,
|
'source_system' => $normalizedSource,
|
||||||
'source_integration_id' => $normalizedIntegrationId,
|
'source_integration_id' => $normalizedIntegrationId,
|
||||||
'order_delivery_method' => $orderMethod,
|
'order_delivery_method' => $orderMethod,
|
||||||
|
'source_service_id' => $this->nullableLimited((string) ($mapping['source_service_id'] ?? ''), 128),
|
||||||
|
'source_vendor_code' => $this->nullableLimited((string) ($mapping['source_vendor_code'] ?? ''), 64),
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
'provider_service_id' => $providerServiceId,
|
'provider_service_id' => $providerServiceId,
|
||||||
'provider_account_id' => $this->nullableLimited((string) ($mapping['provider_account_id'] ?? ''), 128),
|
'provider_account_id' => $this->nullableLimited((string) ($mapping['provider_account_id'] ?? ''), 128),
|
||||||
@@ -174,6 +180,20 @@ final class CarrierDeliveryMethodMappingRepository
|
|||||||
return is_array($rows) ? $rows : [];
|
return is_array($rows) ? $rows : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($normalizedSource === 'erli') {
|
||||||
|
$stmt = $this->pdo->query(
|
||||||
|
"SELECT DISTINCT external_carrier_id
|
||||||
|
FROM orders
|
||||||
|
WHERE external_carrier_id IS NOT NULL
|
||||||
|
AND external_carrier_id <> ''
|
||||||
|
AND source = 'erli'
|
||||||
|
ORDER BY external_carrier_id ASC"
|
||||||
|
);
|
||||||
|
|
||||||
|
$rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_COLUMN) : [];
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $this->pdo->query(
|
$stmt = $this->pdo->query(
|
||||||
"SELECT DISTINCT external_carrier_id
|
"SELECT DISTINCT external_carrier_id
|
||||||
FROM orders
|
FROM orders
|
||||||
@@ -191,7 +211,7 @@ final class CarrierDeliveryMethodMappingRepository
|
|||||||
private function normalizeSourceSystem(string $value): string
|
private function normalizeSourceSystem(string $value): string
|
||||||
{
|
{
|
||||||
$source = strtolower(trim($value));
|
$source = strtolower(trim($value));
|
||||||
return in_array($source, ['allegro', 'shoppro'], true) ? $source : 'allegro';
|
return in_array($source, ['allegro', 'shoppro', 'erli'], true) ? $source : 'allegro';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function nullableLimited(string $value, int $max): ?string
|
private function nullableLimited(string $value, int $max): ?string
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ final class ErliApiClient
|
|||||||
foreach ($decoded as $row) {
|
foreach ($decoded as $row) {
|
||||||
if (is_array($row)) {
|
if (is_array($row)) {
|
||||||
$items[] = $row;
|
$items[] = $row;
|
||||||
|
} elseif (is_scalar($row)) {
|
||||||
|
$value = trim((string) $row);
|
||||||
|
if ($value !== '') {
|
||||||
|
$items[] = ['id' => $value, 'name' => $value];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +158,135 @@ final class ErliApiClient
|
|||||||
return ['ok' => true, 'http_code' => $httpCode, 'message' => 'OK'];
|
return ['ok' => true, 'http_code' => $httpCode, 'message' => 'OK'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
public function getShippingMethods(array $credentials): array
|
||||||
|
{
|
||||||
|
return $this->fetchArrayEndpoint($credentials, '/dictionaries/shippingMethods');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
public function getDeliveryMethods(array $credentials): array
|
||||||
|
{
|
||||||
|
return $this->fetchArrayEndpoint($credentials, '/dictionaries/deliveryMethods');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
public function getDeliveryVendors(array $credentials): array
|
||||||
|
{
|
||||||
|
return $this->fetchArrayEndpoint($credentials, '/dictionaries/deliveryVendors');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
public function getPriceLists(array $credentials): array
|
||||||
|
{
|
||||||
|
return $this->fetchArrayEndpoint($credentials, '/delivery/priceLists');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
public function getPriceListsDetails(array $credentials): array
|
||||||
|
{
|
||||||
|
return $this->fetchArrayEndpoint($credentials, '/delivery/priceListsDetails');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @param array<int, array<string, mixed>> $parcels
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
public function createExternalParcel(array $credentials, array $parcels): array
|
||||||
|
{
|
||||||
|
$baseUrl = rtrim(trim((string) ($credentials['base_url'] ?? '')), '/');
|
||||||
|
$apiKey = trim((string) ($credentials['api_key'] ?? ''));
|
||||||
|
if ($baseUrl === '' || $apiKey === '' || $parcels === []) {
|
||||||
|
return ['ok' => false, 'http_code' => 0, 'items' => [], 'message' => 'Brak danych do utworzenia paczki zewnetrznej Erli.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$body, $httpCode, $curlError] = $this->httpRequest('POST', $baseUrl . '/shipping/external', $apiKey, $parcels);
|
||||||
|
if ($curlError !== null) {
|
||||||
|
return ['ok' => false, 'http_code' => $httpCode, 'items' => [], 'message' => 'Blad polaczenia: ' . $curlError];
|
||||||
|
}
|
||||||
|
if ($httpCode < 200 || $httpCode >= 300) {
|
||||||
|
return ['ok' => false, 'http_code' => $httpCode, 'items' => [], 'message' => $this->resolveFailureMessage($body, $httpCode)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'items' => $this->normalizeItems($decoded),
|
||||||
|
'message' => 'OK',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{base_url: string, api_key: string, timeout_seconds?: int} $credentials
|
||||||
|
* @return array{ok: bool, http_code: int, items: array<int, array<string, mixed>>, message: string}
|
||||||
|
*/
|
||||||
|
private function fetchArrayEndpoint(array $credentials, string $path): array
|
||||||
|
{
|
||||||
|
$baseUrl = rtrim(trim((string) ($credentials['base_url'] ?? '')), '/');
|
||||||
|
$apiKey = trim((string) ($credentials['api_key'] ?? ''));
|
||||||
|
if ($baseUrl === '' || $apiKey === '') {
|
||||||
|
return ['ok' => false, 'http_code' => 0, 'items' => [], 'message' => 'Brak danych API Erli.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$body, $httpCode, $curlError] = $this->httpRequest('GET', $baseUrl . $path, $apiKey);
|
||||||
|
if ($curlError !== null) {
|
||||||
|
return ['ok' => false, 'http_code' => $httpCode, 'items' => [], 'message' => 'Blad polaczenia: ' . $curlError];
|
||||||
|
}
|
||||||
|
if ($httpCode < 200 || $httpCode >= 300) {
|
||||||
|
return ['ok' => false, 'http_code' => $httpCode, 'items' => [], 'message' => $this->resolveFailureMessage($body, $httpCode)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'items' => $this->normalizeItems($decoded),
|
||||||
|
'message' => 'OK',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $decoded
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function normalizeItems(mixed $decoded): array
|
||||||
|
{
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach ($decoded as $row) {
|
||||||
|
if (is_array($row)) {
|
||||||
|
$items[] = $row;
|
||||||
|
} elseif (is_scalar($row)) {
|
||||||
|
$value = trim((string) $row);
|
||||||
|
if ($value !== '') {
|
||||||
|
$items[] = ['id' => $value, 'name' => $value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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}
|
||||||
|
|||||||
194
src/Modules/Settings/ErliDeliveryMappingController.php
Normal file
194
src/Modules/Settings/ErliDeliveryMappingController.php
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Constants\IntegrationSources;
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
use App\Core\Http\Response;
|
||||||
|
use App\Core\I18n\Translator;
|
||||||
|
use App\Core\Security\Csrf;
|
||||||
|
use App\Core\Support\Flash;
|
||||||
|
use App\Modules\Shipments\ApaczkaShipmentService;
|
||||||
|
use App\Modules\Shipments\InpostShipmentService;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ErliDeliveryMappingController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Translator $translator,
|
||||||
|
private readonly CarrierDeliveryMethodMappingRepository $deliveryMappings,
|
||||||
|
private readonly ErliIntegrationRepository $erliRepository,
|
||||||
|
private readonly ErliApiClient $erliApiClient,
|
||||||
|
private readonly ?InpostShipmentService $inpostShipmentService = null,
|
||||||
|
private readonly ?ApaczkaShipmentService $apaczkaShipmentService = null
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function buildViewData(): array
|
||||||
|
{
|
||||||
|
[$shippingMethods, $deliveryVendors, $metadataError] = $this->loadErliMetadata();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'erliDeliveryOrderMethods' => $this->deliveryMappings->getDistinctOrderDeliveryMethods(IntegrationSources::ERLI, 0),
|
||||||
|
'erliDeliveryMappings' => $this->deliveryMappings->listMappings(IntegrationSources::ERLI, 0),
|
||||||
|
'erliShippingMethods' => $shippingMethods,
|
||||||
|
'erliDeliveryVendors' => $deliveryVendors,
|
||||||
|
'erliDeliveryMetadataError' => $metadataError,
|
||||||
|
'inpostDeliveryServices' => $this->loadInpostServices(),
|
||||||
|
'apaczkaDeliveryServices' => $this->loadApaczkaServices(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveDeliveryMappings(Request $request): Response
|
||||||
|
{
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect('/settings/integrations/erli?tab=delivery');
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderMethods = (array) $request->input('order_delivery_method', []);
|
||||||
|
$sourceMethodIds = (array) $request->input('source_service_id', []);
|
||||||
|
$sourceVendorCodes = (array) $request->input('source_vendor_code', []);
|
||||||
|
$providers = (array) $request->input('provider', []);
|
||||||
|
$providerServiceIds = (array) $request->input('provider_service_id', []);
|
||||||
|
$providerCarrierIds = (array) $request->input('provider_carrier_id', []);
|
||||||
|
$providerServiceNames = (array) $request->input('provider_service_name', []);
|
||||||
|
$providerChoices = (array) $request->input('provider_service_choice', []);
|
||||||
|
|
||||||
|
$mappings = [];
|
||||||
|
foreach ($orderMethods as $index => $rawOrderMethod) {
|
||||||
|
$orderMethod = trim((string) $rawOrderMethod);
|
||||||
|
[$provider, $providerServiceId, $providerCarrierId, $providerServiceName] = $this->parseProviderChoice(
|
||||||
|
(string) ($providerChoices[$index] ?? '')
|
||||||
|
);
|
||||||
|
if ($provider === '') {
|
||||||
|
$provider = $this->normalizeProvider((string) ($providers[$index] ?? ''));
|
||||||
|
$providerServiceId = trim((string) ($providerServiceIds[$index] ?? ''));
|
||||||
|
$providerCarrierId = trim((string) ($providerCarrierIds[$index] ?? ''));
|
||||||
|
$providerServiceName = trim((string) ($providerServiceNames[$index] ?? ''));
|
||||||
|
}
|
||||||
|
if ($orderMethod === '' || $provider === '' || $providerServiceId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings[] = [
|
||||||
|
'order_delivery_method' => $orderMethod,
|
||||||
|
'source_service_id' => trim((string) ($sourceMethodIds[$index] ?? '')),
|
||||||
|
'source_vendor_code' => trim((string) ($sourceVendorCodes[$index] ?? '')),
|
||||||
|
'provider' => $provider,
|
||||||
|
'provider_service_id' => $providerServiceId,
|
||||||
|
'provider_account_id' => '',
|
||||||
|
'provider_carrier_id' => $providerCarrierId,
|
||||||
|
'provider_service_name' => $providerServiceName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->deliveryMappings->saveMappings(IntegrationSources::ERLI, 0, $mappings);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.erli.delivery.flash.saved'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.erli.delivery.flash.save_failed') . ' ' . $exception->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect('/settings/integrations/erli?tab=delivery');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
|
||||||
|
*/
|
||||||
|
private function loadErliMetadata(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$credentials = $this->erliRepository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
return [[], [], $this->translator->get('settings.erli.delivery.metadata_not_connected')];
|
||||||
|
}
|
||||||
|
|
||||||
|
$shippingMethodsResult = $this->erliApiClient->getShippingMethods($credentials);
|
||||||
|
$vendorsResult = $this->erliApiClient->getDeliveryVendors($credentials);
|
||||||
|
$error = '';
|
||||||
|
if (!$shippingMethodsResult['ok']) {
|
||||||
|
$error = $shippingMethodsResult['message'];
|
||||||
|
} elseif (!$vendorsResult['ok']) {
|
||||||
|
$error = $vendorsResult['message'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
$shippingMethodsResult['ok'] ? $shippingMethodsResult['items'] : [],
|
||||||
|
$vendorsResult['ok'] ? $vendorsResult['items'] : [],
|
||||||
|
$error,
|
||||||
|
];
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
return [[], [], $exception->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function loadInpostServices(): array
|
||||||
|
{
|
||||||
|
if ($this->inpostShipmentService === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->inpostShipmentService->getDeliveryServices();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function loadApaczkaServices(): array
|
||||||
|
{
|
||||||
|
if ($this->apaczkaShipmentService === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->apaczkaShipmentService->getDeliveryServices();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeProvider(string $value): string
|
||||||
|
{
|
||||||
|
$provider = strtolower(trim($value));
|
||||||
|
return in_array($provider, ['inpost', 'apaczka'], true) ? $provider : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: string, 1: string, 2: string, 3: string}
|
||||||
|
*/
|
||||||
|
private function parseProviderChoice(string $value): array
|
||||||
|
{
|
||||||
|
$parts = explode('|', $value, 4);
|
||||||
|
if (count($parts) < 2) {
|
||||||
|
return ['', '', '', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = $this->normalizeProvider((string) ($parts[0] ?? ''));
|
||||||
|
$serviceId = trim((string) ($parts[1] ?? ''));
|
||||||
|
if ($provider === '' || $serviceId === '') {
|
||||||
|
return ['', '', '', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
$provider,
|
||||||
|
$serviceId,
|
||||||
|
trim((string) ($parts[2] ?? '')),
|
||||||
|
trim((string) ($parts[3] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/Modules/Settings/ErliExternalShipmentService.php
Normal file
169
src/Modules/Settings/ErliExternalShipmentService.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Constants\IntegrationSources;
|
||||||
|
use App\Modules\Orders\OrdersRepository;
|
||||||
|
use App\Modules\Shipments\ShipmentPackageRepository;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ErliExternalShipmentService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ErliIntegrationRepository $erliRepository,
|
||||||
|
private readonly ErliApiClient $apiClient,
|
||||||
|
private readonly CarrierDeliveryMethodMappingRepository $deliveryMappings,
|
||||||
|
private readonly ShipmentPackageRepository $packageRepository,
|
||||||
|
private readonly OrdersRepository $ordersRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok: bool, skipped: bool, message: string}
|
||||||
|
*/
|
||||||
|
public function syncPackage(int $packageId): array
|
||||||
|
{
|
||||||
|
$package = $this->packageRepository->findById($packageId);
|
||||||
|
if ($package === null) {
|
||||||
|
return ['ok' => false, 'skipped' => true, 'message' => 'Paczka nie istnieje.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$trackingNumber = trim((string) ($package['tracking_number'] ?? ''));
|
||||||
|
if ($trackingNumber === '') {
|
||||||
|
return ['ok' => true, 'skipped' => true, 'message' => 'Paczka nie ma jeszcze numeru nadania.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderId = (int) ($package['order_id'] ?? 0);
|
||||||
|
$details = $orderId > 0 ? $this->ordersRepository->findDetails($orderId) : null;
|
||||||
|
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||||
|
if (strtolower(trim((string) ($order['source'] ?? ''))) !== IntegrationSources::ERLI) {
|
||||||
|
return ['ok' => true, 'skipped' => true, 'message' => 'Zamowienie nie pochodzi z Erli.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payloadJson = $this->decodePayload((string) ($package['payload_json'] ?? ''));
|
||||||
|
if ($this->alreadySynced($payloadJson, $trackingNumber)) {
|
||||||
|
return ['ok' => true, 'skipped' => true, 'message' => 'Paczka jest juz zarejestrowana w Erli.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceOrderId = trim((string) ($order['source_order_id'] ?? $order['external_order_id'] ?? ''));
|
||||||
|
$vendor = $this->resolveVendor($order, $package);
|
||||||
|
if ($sourceOrderId === '' || $vendor === '') {
|
||||||
|
return ['ok' => false, 'skipped' => true, 'message' => 'Brak ID zamowienia Erli albo vendora Erli dla paczki.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->erliRepository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
return ['ok' => false, 'skipped' => true, 'message' => 'Brak aktywnej konfiguracji Erli.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$externalParcel = [
|
||||||
|
'orderId' => $sourceOrderId,
|
||||||
|
'vendor' => $vendor,
|
||||||
|
'status' => 'sent',
|
||||||
|
'trackingNumber' => $trackingNumber,
|
||||||
|
];
|
||||||
|
$result = $this->apiClient->createExternalParcel($credentials, [$externalParcel]);
|
||||||
|
if (!$result['ok']) {
|
||||||
|
$this->recordActivity($orderId, false, $packageId, $result['message']);
|
||||||
|
return ['ok' => false, 'skipped' => false, 'message' => $result['message']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payloadJson['erli_external_parcel'] = [
|
||||||
|
'synced_at' => date('Y-m-d H:i:s'),
|
||||||
|
'request' => $externalParcel,
|
||||||
|
'response' => $result['items'],
|
||||||
|
'trackingNumber' => $trackingNumber,
|
||||||
|
];
|
||||||
|
$this->packageRepository->update($packageId, ['payload_json' => $payloadJson]);
|
||||||
|
$this->recordActivity($orderId, true, $packageId, 'Paczka zewnetrzna Erli zostala zarejestrowana.');
|
||||||
|
|
||||||
|
return ['ok' => true, 'skipped' => false, 'message' => 'OK'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
* @param array<string, mixed> $package
|
||||||
|
*/
|
||||||
|
private function resolveVendor(array $order, array $package): string
|
||||||
|
{
|
||||||
|
$orderMethod = trim((string) ($order['external_carrier_id'] ?? ''));
|
||||||
|
if ($orderMethod !== '') {
|
||||||
|
$mapping = $this->deliveryMappings->findByOrderMethod(IntegrationSources::ERLI, 0, $orderMethod);
|
||||||
|
$vendor = trim((string) ($mapping['source_vendor_code'] ?? ''));
|
||||||
|
if ($vendor !== '') {
|
||||||
|
return $vendor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->inferVendorFromPackage($package);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $package
|
||||||
|
*/
|
||||||
|
private function inferVendorFromPackage(array $package): string
|
||||||
|
{
|
||||||
|
$provider = strtolower(trim((string) ($package['provider'] ?? '')));
|
||||||
|
$carrier = strtolower(trim((string) ($package['carrier_id'] ?? '')));
|
||||||
|
$deliveryMethod = strtolower(trim((string) ($package['delivery_method_id'] ?? '')));
|
||||||
|
$combined = $provider . ' ' . $carrier . ' ' . $deliveryMethod;
|
||||||
|
|
||||||
|
$known = [
|
||||||
|
'inpost' => 'inpost',
|
||||||
|
'dpd' => 'dpd',
|
||||||
|
'dhl' => 'dhl',
|
||||||
|
'ups' => 'ups',
|
||||||
|
'gls' => 'gls',
|
||||||
|
'orlen' => 'orlen',
|
||||||
|
'pocztex' => 'pocztex24',
|
||||||
|
'poczta' => 'pocztaPolska',
|
||||||
|
'fedex' => 'fedex',
|
||||||
|
];
|
||||||
|
foreach ($known as $needle => $vendor) {
|
||||||
|
if (str_contains($combined, $needle)) {
|
||||||
|
return $vendor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function decodePayload(string $payload): array
|
||||||
|
{
|
||||||
|
$decoded = json_decode($payload, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
private function alreadySynced(array $payload, string $trackingNumber): bool
|
||||||
|
{
|
||||||
|
$erliParcel = is_array($payload['erli_external_parcel'] ?? null) ? $payload['erli_external_parcel'] : [];
|
||||||
|
return trim((string) ($erliParcel['trackingNumber'] ?? '')) === $trackingNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function recordActivity(int $orderId, bool $success, int $packageId, string $message): void
|
||||||
|
{
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->ordersRepository->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
$success ? 'erli_external_shipment_synced' : 'erli_external_shipment_error',
|
||||||
|
$success ? 'Zarejestrowano paczke zewnetrzna w Erli.' : 'Nie udalo sie zarejestrowac paczki w Erli: ' . $message,
|
||||||
|
['package_id' => $packageId],
|
||||||
|
'system',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
// Logging nie moze blokowac lokalnej etykiety.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,8 @@ final class ErliIntegrationController
|
|||||||
private readonly ErliOrdersSyncService $ordersSyncService,
|
private readonly ErliOrdersSyncService $ordersSyncService,
|
||||||
private readonly OrderStatusRepository $orderStatuses,
|
private readonly OrderStatusRepository $orderStatuses,
|
||||||
private readonly ErliStatusMappingRepository $statusMappings,
|
private readonly ErliStatusMappingRepository $statusMappings,
|
||||||
private readonly ErliPullStatusMappingRepository $pullStatusMappings
|
private readonly ErliPullStatusMappingRepository $pullStatusMappings,
|
||||||
|
private readonly ?ErliDeliveryMappingController $deliveryMappingController = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ final class ErliIntegrationController
|
|||||||
{
|
{
|
||||||
$activeTab = $this->resolveTab((string) $request->input('tab', 'integration'));
|
$activeTab = $this->resolveTab((string) $request->input('tab', 'integration'));
|
||||||
|
|
||||||
$html = $this->template->render('settings/erli', [
|
$viewData = [
|
||||||
'title' => $this->translator->get('settings.erli.title'),
|
'title' => $this->translator->get('settings.erli.title'),
|
||||||
'activeMenu' => 'settings',
|
'activeMenu' => 'settings',
|
||||||
'activeSettings' => 'integrations',
|
'activeSettings' => 'integrations',
|
||||||
@@ -65,7 +66,12 @@ final class ErliIntegrationController
|
|||||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
'testMessage' => (string) Flash::get('erli_test', ''),
|
'testMessage' => (string) Flash::get('erli_test', ''),
|
||||||
'importMessage' => (string) Flash::get('erli_import', ''),
|
'importMessage' => (string) Flash::get('erli_import', ''),
|
||||||
], 'layouts/app');
|
];
|
||||||
|
if ($this->deliveryMappingController !== null) {
|
||||||
|
$viewData = array_merge($viewData, $this->deliveryMappingController->buildViewData());
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = $this->template->render('settings/erli', $viewData, 'layouts/app');
|
||||||
|
|
||||||
return Response::html($html);
|
return Response::html($html);
|
||||||
}
|
}
|
||||||
@@ -244,7 +250,7 @@ final class ErliIntegrationController
|
|||||||
private function resolveTab(string $tab): string
|
private function resolveTab(string $tab): string
|
||||||
{
|
{
|
||||||
$normalized = trim($tab);
|
$normalized = trim($tab);
|
||||||
if (in_array($normalized, ['integration', 'statuses', 'settings'], true)) {
|
if (in_array($normalized, ['integration', 'statuses', 'delivery', 'settings'], true)) {
|
||||||
return $normalized;
|
return $normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Modules\Auth\AuthService;
|
|||||||
use App\Modules\Orders\OrdersRepository;
|
use App\Modules\Orders\OrdersRepository;
|
||||||
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
||||||
use App\Modules\Settings\CompanySettingsRepository;
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
|
use App\Modules\Settings\ErliExternalShipmentService;
|
||||||
use App\Core\Exceptions\ShipmentException;
|
use App\Core\Exceptions\ShipmentException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ final class ShipmentController
|
|||||||
private readonly AutomationService $automationService,
|
private readonly AutomationService $automationService,
|
||||||
private readonly string $storagePath,
|
private readonly string $storagePath,
|
||||||
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
|
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
|
||||||
private readonly ?\App\Modules\Printing\PrintJobRepository $printJobRepo = null
|
private readonly ?\App\Modules\Printing\PrintJobRepository $printJobRepo = null,
|
||||||
|
private readonly ?ErliExternalShipmentService $erliExternalShipmentService = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,10 +91,23 @@ final class ShipmentController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$inpostServices = array_values(array_filter(
|
$inpostServices = [];
|
||||||
$deliveryServices,
|
$inpostProvider = $this->providerRegistry->get('inpost');
|
||||||
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
if ($inpostProvider !== null) {
|
||||||
));
|
try {
|
||||||
|
$inpostServices = $inpostProvider->getDeliveryServices();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
if ($deliveryServicesError === '') {
|
||||||
|
$deliveryServicesError = $exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($inpostServices === []) {
|
||||||
|
$inpostServices = array_values(array_filter(
|
||||||
|
$deliveryServices,
|
||||||
|
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
$flashSuccess = (string) Flash::get('shipment.success', '');
|
$flashSuccess = (string) Flash::get('shipment.success', '');
|
||||||
$flashError = (string) Flash::get('shipment.error', '');
|
$flashError = (string) Flash::get('shipment.error', '');
|
||||||
@@ -104,7 +119,7 @@ final class ShipmentController
|
|||||||
$sourceIntegrationId = $source === 'shoppro'
|
$sourceIntegrationId = $source === 'shoppro'
|
||||||
? max(0, (int) ($order['integration_id'] ?? 0))
|
? max(0, (int) ($order['integration_id'] ?? 0))
|
||||||
: 0;
|
: 0;
|
||||||
if ($orderCarrierName !== '' && $this->deliveryMappings !== null && in_array($source, ['allegro', 'shoppro'], true)) {
|
if ($orderCarrierName !== '' && $this->deliveryMappings !== null && in_array($source, ['allegro', 'shoppro', 'erli'], true)) {
|
||||||
$deliveryMapping = $this->deliveryMappings->findByOrderMethod($source, $sourceIntegrationId, $orderCarrierName);
|
$deliveryMapping = $this->deliveryMappings->findByOrderMethod($source, $sourceIntegrationId, $orderCarrierName);
|
||||||
if ($deliveryMapping === null) {
|
if ($deliveryMapping === null) {
|
||||||
$hasMappingsForSource = $this->deliveryMappings->hasMappingsForSource($source, $sourceIntegrationId);
|
$hasMappingsForSource = $this->deliveryMappings->hasMappingsForSource($source, $sourceIntegrationId);
|
||||||
@@ -218,6 +233,7 @@ final class ShipmentController
|
|||||||
);
|
);
|
||||||
|
|
||||||
$this->triggerShipmentCreatedAutomation($orderId, $packageId, $providerCode);
|
$this->triggerShipmentCreatedAutomation($orderId, $packageId, $providerCode);
|
||||||
|
$this->syncErliExternalShipment($packageId);
|
||||||
Flash::set('order.success', 'Przesylka utworzona. Sprawdz status w zakladce Przesylki.');
|
Flash::set('order.success', 'Przesylka utworzona. Sprawdz status w zakladce Przesylki.');
|
||||||
return Response::redirect('/orders/' . $orderId . '?printLast=1');
|
return Response::redirect('/orders/' . $orderId . '?printLast=1');
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
@@ -264,6 +280,9 @@ final class ShipmentController
|
|||||||
// label generation failed, user can retry manually
|
// label generation failed, user can retry manually
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (in_array((string) ($result['status'] ?? ''), ['created', 'label_ready'], true)) {
|
||||||
|
$this->syncErliExternalShipment($packageId);
|
||||||
|
}
|
||||||
|
|
||||||
return Response::json($result);
|
return Response::json($result);
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
@@ -302,6 +321,7 @@ final class ShipmentController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $provider->downloadLabel($packageId, $this->storagePath);
|
$result = $provider->downloadLabel($packageId, $this->storagePath);
|
||||||
|
$this->syncErliExternalShipment($packageId);
|
||||||
$fullPath = (string) ($result['full_path'] ?? '');
|
$fullPath = (string) ($result['full_path'] ?? '');
|
||||||
if ($fullPath !== '' && file_exists($fullPath)) {
|
if ($fullPath !== '' && file_exists($fullPath)) {
|
||||||
$labelFormat = strtoupper(trim((string) ($package['label_format'] ?? 'PDF')));
|
$labelFormat = strtoupper(trim((string) ($package['label_format'] ?? 'PDF')));
|
||||||
@@ -473,6 +493,19 @@ final class ShipmentController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncErliExternalShipment(int $packageId): void
|
||||||
|
{
|
||||||
|
if ($this->erliExternalShipmentService === null || $packageId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->erliExternalShipmentService->syncPackage($packageId);
|
||||||
|
} catch (Throwable) {
|
||||||
|
// Synchronizacja Erli nie moze blokowac lokalnej etykiety.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed>|null $deliveryAddr
|
* @param array<string, mixed>|null $deliveryAddr
|
||||||
* @param array<string, mixed>|null $customerAddr
|
* @param array<string, mixed>|null $customerAddr
|
||||||
|
|||||||
130
tests/Unit/ErliExternalShipmentServiceTest.php
Normal file
130
tests/Unit/ErliExternalShipmentServiceTest.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Modules\Orders\OrdersRepository;
|
||||||
|
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
||||||
|
use App\Modules\Settings\ErliApiClient;
|
||||||
|
use App\Modules\Settings\ErliExternalShipmentService;
|
||||||
|
use App\Modules\Settings\ErliIntegrationRepository;
|
||||||
|
use App\Modules\Shipments\ShipmentPackageRepository;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ErliExternalShipmentServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private ErliIntegrationRepository&MockObject $erliRepository;
|
||||||
|
private ErliApiClient&MockObject $apiClient;
|
||||||
|
private CarrierDeliveryMethodMappingRepository&MockObject $deliveryMappings;
|
||||||
|
private ShipmentPackageRepository&MockObject $packageRepository;
|
||||||
|
private OrdersRepository&MockObject $ordersRepository;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->erliRepository = $this->createMock(ErliIntegrationRepository::class);
|
||||||
|
$this->apiClient = $this->createMock(ErliApiClient::class);
|
||||||
|
$this->deliveryMappings = $this->createMock(CarrierDeliveryMethodMappingRepository::class);
|
||||||
|
$this->packageRepository = $this->createMock(ShipmentPackageRepository::class);
|
||||||
|
$this->ordersRepository = $this->createMock(OrdersRepository::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncPackageRegistersExternalParcelWithMappedVendor(): void
|
||||||
|
{
|
||||||
|
$this->packageRepository
|
||||||
|
->method('findById')
|
||||||
|
->with(55)
|
||||||
|
->willReturn([
|
||||||
|
'id' => 55,
|
||||||
|
'order_id' => 1001,
|
||||||
|
'provider' => 'inpost',
|
||||||
|
'carrier_id' => 'inpost',
|
||||||
|
'tracking_number' => '1234567890',
|
||||||
|
'payload_json' => '{}',
|
||||||
|
]);
|
||||||
|
$this->ordersRepository
|
||||||
|
->method('findDetails')
|
||||||
|
->with(1001)
|
||||||
|
->willReturn([
|
||||||
|
'order' => [
|
||||||
|
'source' => 'erli',
|
||||||
|
'source_order_id' => '240101x12345',
|
||||||
|
'external_carrier_id' => 'Erli Paczkomat',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->deliveryMappings
|
||||||
|
->method('findByOrderMethod')
|
||||||
|
->with('erli', 0, 'Erli Paczkomat')
|
||||||
|
->willReturn(['source_vendor_code' => 'inpost']);
|
||||||
|
$this->erliRepository
|
||||||
|
->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->apiClient
|
||||||
|
->expects($this->once())
|
||||||
|
->method('createExternalParcel')
|
||||||
|
->with(
|
||||||
|
$this->anything(),
|
||||||
|
[[
|
||||||
|
'orderId' => '240101x12345',
|
||||||
|
'vendor' => 'inpost',
|
||||||
|
'status' => 'sent',
|
||||||
|
'trackingNumber' => '1234567890',
|
||||||
|
]]
|
||||||
|
)
|
||||||
|
->willReturn(['ok' => true, 'http_code' => 200, 'items' => [['id' => 777]], 'message' => 'OK']);
|
||||||
|
$this->packageRepository
|
||||||
|
->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
55,
|
||||||
|
$this->callback(static function (array $data): bool {
|
||||||
|
$payload = $data['payload_json'] ?? [];
|
||||||
|
return is_array($payload)
|
||||||
|
&& isset($payload['erli_external_parcel'])
|
||||||
|
&& ($payload['erli_external_parcel']['trackingNumber'] ?? '') === '1234567890';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->createService()->syncPackage(55);
|
||||||
|
|
||||||
|
self::assertTrue($result['ok']);
|
||||||
|
self::assertFalse($result['skipped']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncPackageSkipsUntilTrackingNumberExists(): void
|
||||||
|
{
|
||||||
|
$this->packageRepository
|
||||||
|
->method('findById')
|
||||||
|
->willReturn([
|
||||||
|
'id' => 55,
|
||||||
|
'order_id' => 1001,
|
||||||
|
'tracking_number' => '',
|
||||||
|
]);
|
||||||
|
$this->apiClient
|
||||||
|
->expects($this->never())
|
||||||
|
->method('createExternalParcel');
|
||||||
|
|
||||||
|
$result = $this->createService()->syncPackage(55);
|
||||||
|
|
||||||
|
self::assertTrue($result['ok']);
|
||||||
|
self::assertTrue($result['skipped']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createService(): ErliExternalShipmentService
|
||||||
|
{
|
||||||
|
return new ErliExternalShipmentService(
|
||||||
|
$this->erliRepository,
|
||||||
|
$this->apiClient,
|
||||||
|
$this->deliveryMappings,
|
||||||
|
$this->packageRepository,
|
||||||
|
$this->ordersRepository
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user