update
This commit is contained in:
@@ -4,13 +4,27 @@
|
|||||||
**Ostatnia aktualizacja:** 2026-05-20
|
**Ostatnia aktualizacja:** 2026-05-20
|
||||||
|
|
||||||
## Aktywna praca
|
## Aktywna praca
|
||||||
UNIFY zakonczony dla `.paul/plans/20260520-1500-refactor-invoice-service/`. Petla zamknieta. `InvoiceService.php` 762 -> 118 lin. (84% redukcji). Wydzielono 5 nowych klas w `src/Modules/Accounting/`: `InvoiceMetadataResolver` (84), `InvoiceSnapshotBuilder` (223), `FakturowniaInvoicePayloadBuilder` (84), `LocalInvoiceIssuer` (66, deviation — symetria do delegated), `DelegatedInvoiceIssuer` (253). Zero zmian publicznego kontraktu (konstruktor 6-arg, `issue(array)`, statyk `extractBuyerTaxNumber` jako forwarder). Konsumenci niezmienieni: `InvoiceController`, `AccountingModule`, `tests/Unit/FakturowniaInvoiceIdempotencyTest.php`. `php -l` zielony 7/7. Deviation: incydentalna naprawa pre-existing bugu — `LocalInvoiceIssuer::issue` ma poprawne 14 paramow vs oryginalny `issueLocal` 15 paramow w sygnaturze (sciezka local rzucala `ArgumentCountError`). SUMMARY: `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md`.
|
UNIFY zakonczony dla `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/`. Petla zamknieta. `PolkurierShipmentService.php` 776 -> 299 lin. (61% redukcji) — slim fasada implementujaca `ShipmentProviderInterface`, `createShipment` 34 lin. Wydzielono 3 klasy w `src/Modules/Shipments/`: `PolkurierCarrierCatalog` (118), `PolkurierShipmentPayloadBuilder` (393, z `buildShipmentRequest` montujacym apiPayload + rekord paczki), `PolkurierResponseParser` (113). Orkiestracja create rozbita na `sendCreateRequest` + `persistCreationResult`. Zero zmian publicznego kontraktu (konstruktor 5-arg readonly, interfejs). `buildCodPayload` pozostaje prywatnym forwarderem na fasadzie (pinned przez `ReflectionMethod` w tescie). `php -l` zielony 4/4. Konsumenci niezmienieni: `ShipmentsModule` (factory), `ShipmentController` (interfejs), `ShopproIntegrationsController` (tylko publiczne `getDeliveryServices()`). Quality Radar: ok (codebase-memory-mcp 3 481 -> 3 518 wezlow; jscpd/ast-grep disabled by policy). SUMMARY: `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/SUMMARY.md`.
|
||||||
|
|
||||||
|
**Rozszerzenie zakresu (zatwierdzone przez uzytkownika w trakcie APPLY):** po pierwszej dekompozycji (fasada 342, createShipment ~150) dolozono ekstrakcje montazu zadania do `PolkurierShipmentPayloadBuilder::buildShipmentRequest()` -> fasada 299, createShipment 34 lin. Tradeoff: `buildShipmentRequest` 86 lin. (plaskie literale tablic, zero zagniezdzen). Twardy prog AC-5 spelniony (299 < 388 = 50% z 776).
|
||||||
|
|
||||||
|
⚠ **Do weryfikacji recznej (vendor/phpunit niedostepny w sesji):**
|
||||||
|
1. `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php` — primary gate. UWAGA pre-existing: `testBuildCodPayloadRequiresBankAccount` asercja `'Przesylka...'` (`l`) vs rzucany komunikat `'Przesyłka...'` (`ł`) — rozjazd istnieje juz na HEAD (baseline 2/3, NIE 3/3); komunikat przeniesiono 1:1, wynik testu = baseline (NIE naprawiac bez zgody — to zmiana komunikatu uzytkownika).
|
||||||
|
2. Smoke `POST /orders/{id}/shipment/prepare` dla zamowienia z mapowaniem polkurier; dropdown uslug w `/settings/integrations/shoppro`.
|
||||||
|
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Loop complete — gotowe na nastepny PLAN]
|
✓ ✓ ✓ [Loop complete — gotowe na nastepny PLAN]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Poprzednia praca
|
||||||
|
UNIFY zakonczony dla `.paul/plans/20260520-1500-refactor-invoice-service/`. Petla zamknieta. `InvoiceService.php` 762 -> 118 lin. (84% redukcji). Wydzielono 5 nowych klas w `src/Modules/Accounting/`: `InvoiceMetadataResolver` (84), `InvoiceSnapshotBuilder` (223), `FakturowniaInvoicePayloadBuilder` (84), `LocalInvoiceIssuer` (66, deviation — symetria do delegated), `DelegatedInvoiceIssuer` (253). Zero zmian publicznego kontraktu (konstruktor 6-arg, `issue(array)`, statyk `extractBuyerTaxNumber` jako forwarder). Konsumenci niezmienieni: `InvoiceController`, `AccountingModule`, `tests/Unit/FakturowniaInvoiceIdempotencyTest.php`. `php -l` zielony 7/7. Deviation: incydentalna naprawa pre-existing bugu — `LocalInvoiceIssuer::issue` ma poprawne 14 paramow vs oryginalny `issueLocal` 15 paramow w sygnaturze (sciezka local rzucala `ArgumentCountError`). SUMMARY: `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md`.
|
||||||
|
|
||||||
|
```
|
||||||
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
|
✓ ✓ ✓ [Loop complete]
|
||||||
|
```
|
||||||
|
|
||||||
⚠ **Do weryfikacji recznej (vendor/phpunit niedostepny w sesji):**
|
⚠ **Do weryfikacji recznej (vendor/phpunit niedostepny w sesji):**
|
||||||
1. `vendor/bin/phpunit tests/Unit/FakturowniaInvoiceIdempotencyTest.php` — 3/3 scenariusze idempotencji musza przejsc bez modyfikacji testu (primary gate).
|
1. `vendor/bin/phpunit tests/Unit/FakturowniaInvoiceIdempotencyTest.php` — 3/3 scenariusze idempotencji musza przejsc bez modyfikacji testu (primary gate).
|
||||||
2. Smoke `POST /orders/{id}/invoice/create` dla konfiguracji delegowanej (Fakturownia) i lokalnej (sciezka local teraz wlasciwie dziala po naprawie sygnatury).
|
2. Smoke `POST /orders/{id}/invoice/create` dla konfiguracji delegowanej (Fakturownia) i lokalnej (sciezka local teraz wlasciwie dziala po naprawie sygnatury).
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
- Zero zmian publicznego kontraktu: konstruktor 6-arg, `issue(array): array`, statyk `InvoiceService::extractBuyerTaxNumber()` jako forwarder. `InvoiceController`, `AccountingModule`, `tests/Unit/FakturowniaInvoiceIdempotencyTest.php` niezmienione.
|
- Zero zmian publicznego kontraktu: konstruktor 6-arg, `issue(array): array`, statyk `InvoiceService::extractBuyerTaxNumber()` jako forwarder. `InvoiceController`, `AccountingModule`, `tests/Unit/FakturowniaInvoiceIdempotencyTest.php` niezmienione.
|
||||||
- Incydentalna naprawa pre-existing bugu: oryginalny `issueLocal()` mial 15 paramow w sygnaturze vs 14 przekazywanych — sciezka local rzucala `ArgumentCountError`. `LocalInvoiceIssuer::issue` ma poprawne 14 paramow.
|
- Incydentalna naprawa pre-existing bugu: oryginalny `issueLocal()` mial 15 paramow w sygnaturze vs 14 przekazywanych — sciezka local rzucala `ArgumentCountError`. `LocalInvoiceIssuer::issue` ma poprawne 14 paramow.
|
||||||
- `php -l` zielony dla 7 plikow. Reczny UAT (vendor/phpunit niedostepny): `tests/Unit/FakturowniaInvoiceIdempotencyTest.php` + smoke `POST /orders/{id}/invoice/create`.
|
- `php -l` zielony dla 7 plikow. Reczny UAT (vendor/phpunit niedostepny): `tests/Unit/FakturowniaInvoiceIdempotencyTest.php` + smoke `POST /orders/{id}/invoice/create`.
|
||||||
|
- [Plan 20260520-1615-refactor-polkurier-shipment-service] Dekompozycja `src/Modules/Shipments/PolkurierShipmentService.php` z 776 do 299 lin. (61% redukcji); `createShipment` 34 lin. Wydzielono 3 nowe klasy: `PolkurierCarrierCatalog` (118), `PolkurierShipmentPayloadBuilder` (393, z `buildShipmentRequest` montujacym apiPayload + rekord paczki), `PolkurierResponseParser` (113). Orkiestracja create rozbita na `sendCreateRequest` + `persistCreationResult`.
|
||||||
|
- Zero zmian publicznego kontraktu: konstruktor 5-arg readonly, `ShipmentProviderInterface`. `buildCodPayload` pozostaje prywatnym forwarderem (pinned przez `ReflectionMethod` w tescie). Konsumenci niezmienieni (`ShipmentsModule`, `ShipmentController`, `ShopproIntegrationsController`).
|
||||||
|
- Rozszerzenie zakresu zatwierdzone przez uzytkownika w trakcie APPLY (ekstrakcja `buildShipmentRequest`). Pre-existing rozjazd `ł`/`l` w `testBuildCodPayloadRequiresBankAccount` udokumentowany, NIE naprawiany. `php -l` zielony 4/4; reczny UAT (vendor/phpunit niedostepny): `tests/Unit/PolkurierShipmentServiceTest.php` + smoke `/orders/{id}/shipment/prepare`.
|
||||||
|
|
||||||
## Zmienione pliki
|
## Zmienione pliki
|
||||||
|
|
||||||
@@ -31,4 +34,10 @@
|
|||||||
- `.paul/plans/20260520-1128-storage-cleanup-cron/SUMMARY.md`
|
- `.paul/plans/20260520-1128-storage-cleanup-cron/SUMMARY.md`
|
||||||
- `.paul/plans/20260520-1500-refactor-invoice-service/PLAN.md`
|
- `.paul/plans/20260520-1500-refactor-invoice-service/PLAN.md`
|
||||||
- `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md`
|
- `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md`
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentService.php`
|
||||||
|
- `src/Modules/Shipments/PolkurierCarrierCatalog.php`
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php`
|
||||||
|
- `src/Modules/Shipments/PolkurierResponseParser.php`
|
||||||
|
- `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/PLAN.md`
|
||||||
|
- `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/SUMMARY.md`
|
||||||
- `.paul/STATE.md`
|
- `.paul/STATE.md`
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ Korzysci wzgledem poprzedniego monolitycznego `routes/web.php` (859 lin.):
|
|||||||
| Statistics | `src/Modules/Statistics/` | raporty zamowien (slim Controller + `OrdersStatisticsFilters` + `OrdersStatisticsTableBuilder` + `OrdersStatisticsSummaryBuilder` + Repository) |
|
| Statistics | `src/Modules/Statistics/` | raporty zamowien (slim Controller + `OrdersStatisticsFilters` + `OrdersStatisticsTableBuilder` + `OrdersStatisticsSummaryBuilder` + Repository) |
|
||||||
| Settings/Allegro | `src/Modules/Settings/Allegro*.php` | integracja Allegro: slim `AllegroIntegrationController` + `AllegroIntegrationViewModel` + `AllegroOAuthFlowService` + `AllegroImportScheduleService` + `AllegroImportImageWarningFormatter` + `AllegroSaveSettingsValidator` + `AllegroOrderImportService` + `AllegroStatusDiscoveryService` + `AllegroStatusMappingController` + `AllegroDeliveryMappingController` + repozytoria (`AllegroIntegrationRepository`, `AllegroStatusMappingRepository`, `AllegroPullStatusMappingRepository`) + klienci (`AllegroApiClient`, `AllegroOAuthClient`, `AllegroTokenManager`). |
|
| Settings/Allegro | `src/Modules/Settings/Allegro*.php` | integracja Allegro: slim `AllegroIntegrationController` + `AllegroIntegrationViewModel` + `AllegroOAuthFlowService` + `AllegroImportScheduleService` + `AllegroImportImageWarningFormatter` + `AllegroSaveSettingsValidator` + `AllegroOrderImportService` + `AllegroStatusDiscoveryService` + `AllegroStatusMappingController` + `AllegroDeliveryMappingController` + repozytoria (`AllegroIntegrationRepository`, `AllegroStatusMappingRepository`, `AllegroPullStatusMappingRepository`) + klienci (`AllegroApiClient`, `AllegroOAuthClient`, `AllegroTokenManager`). |
|
||||||
| Accounting | `src/Modules/Accounting/` | paragony, faktury (slim fasada `InvoiceService` + `InvoiceMetadataResolver` resolvery dat/oid + `InvoiceSnapshotBuilder` seller/buyer/items + statyka `extractBuyerTaxNumber` + `FakturowniaInvoicePayloadBuilder` payload API + `LocalInvoiceIssuer` sciezka lokalna + `DelegatedInvoiceIssuer` idempotentna sciezka Fakturowni), eksport ksiegowy |
|
| Accounting | `src/Modules/Accounting/` | paragony, faktury (slim fasada `InvoiceService` + `InvoiceMetadataResolver` resolvery dat/oid + `InvoiceSnapshotBuilder` seller/buyer/items + statyka `extractBuyerTaxNumber` + `FakturowniaInvoicePayloadBuilder` payload API + `LocalInvoiceIssuer` sciezka lokalna + `DelegatedInvoiceIssuer` idempotentna sciezka Fakturowni), eksport ksiegowy |
|
||||||
| Shipments | `src/Modules/Shipments/` | etykiety, tracking, presets paczek; slownik statusow dostawy jako fasada `DeliveryStatus` + `DeliveryStatusProviderMap` (mapy dostawcow) + `AllegroDescriptionGuesser` (heurystyki opisow) + `DeliveryTrackingUrlBuilder` (URL sledzenia) |
|
| Shipments | `src/Modules/Shipments/` | etykiety, tracking, presets paczek; slownik statusow dostawy jako fasada `DeliveryStatus` + `DeliveryStatusProviderMap` (mapy dostawcow) + `AllegroDescriptionGuesser` (heurystyki opisow) + `DeliveryTrackingUrlBuilder` (URL sledzenia); polkurier jako slim fasada `PolkurierShipmentService` (implementuje `ShipmentProviderInterface`, `createShipment` orkiestruje przez `sendCreateRequest`/`persistCreationResult`) + `PolkurierCarrierCatalog` (katalog uslug + heurystyki punktow odbioru) + `PolkurierShipmentPayloadBuilder` (`buildShipmentRequest`: montaz apiPayload + rekordu paczki; payload nadawcy/odbiorcy/odbioru/COD + helpery adresowe) + `PolkurierResponseParser` (ekstrakcja order/tracking/etykieta) |
|
||||||
| Printing | `src/Modules/Printing/` | API drukowania etykiet (klient Windows) |
|
| Printing | `src/Modules/Printing/` | API drukowania etykiet (klient Windows) |
|
||||||
| Email | `src/Modules/Email/` | SMTP, wysylka maili |
|
| Email | `src/Modules/Email/` | SMTP, wysylka maili |
|
||||||
| Sms | `src/Modules/Sms/` | wysylka SMS, webhook SmsPlanet, konwersacje |
|
| Sms | `src/Modules/Sms/` | wysylka SMS, webhook SmsPlanet, konwersacje |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Konwencja (`CLAUDE.md`): funkcja/klasa zwykle do 30-50 linii, max 3 poziomy zagn
|
|||||||
| `src/Modules/Settings/ShopproOrderMapper.php` | 867 | mapper z wieloma galeziami. |
|
| `src/Modules/Settings/ShopproOrderMapper.php` | 867 | mapper z wieloma galeziami. |
|
||||||
| `src/Modules/Settings/AllegroOrderImportService.php` | 834 | duzy import service. |
|
| `src/Modules/Settings/AllegroOrderImportService.php` | 834 | duzy import service. |
|
||||||
| `src/Modules/Automation/AutomationService.php` | 818 | silnik regul — kandydat na pipeline-strategy. |
|
| `src/Modules/Automation/AutomationService.php` | 818 | silnik regul — kandydat na pipeline-strategy. |
|
||||||
| `src/Modules/Shipments/PolkurierShipmentService.php` | 776 | |
|
| ~~`src/Modules/Shipments/PolkurierShipmentService.php`~~ | ~~776~~ -> 299 | ✅ Zrefaktorowane 2026-05-20 — slim fasada (299, implementuje `ShipmentProviderInterface`, `createShipment` 34 lin.) + 3 wspolpracownikow: `PolkurierCarrierCatalog` (118), `PolkurierShipmentPayloadBuilder` (393, z `buildShipmentRequest` montujacym apiPayload + rekord paczki), `PolkurierResponseParser` (113). Zero zmian kontraktu (konstruktor 5-arg, interfejs); `buildCodPayload` prywatny forwarder (pinned przez test refleksji). Tradeoff: `buildShipmentRequest` 86 lin. (plaskie literale tablic). Patrz `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/SUMMARY.md`. |
|
||||||
| ~~`src/Modules/Accounting/InvoiceService.php`~~ | ~~762~~ -> 118 | ✅ Zrefaktorowane 2026-05-20 — fasada (118) + 5 wspolpracownikow: `InvoiceMetadataResolver` (84), `InvoiceSnapshotBuilder` (223), `FakturowniaInvoicePayloadBuilder` (84), `LocalInvoiceIssuer` (66), `DelegatedInvoiceIssuer` (253). Zero zmian kontraktu (konstruktor 6-arg, `issue()`, statyk `extractBuyerTaxNumber`). Patrz `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md`. |
|
| ~~`src/Modules/Accounting/InvoiceService.php`~~ | ~~762~~ -> 118 | ✅ Zrefaktorowane 2026-05-20 — fasada (118) + 5 wspolpracownikow: `InvoiceMetadataResolver` (84), `InvoiceSnapshotBuilder` (223), `FakturowniaInvoicePayloadBuilder` (84), `LocalInvoiceIssuer` (66), `DelegatedInvoiceIssuer` (253). Zero zmian kontraktu (konstruktor 6-arg, `issue()`, statyk `extractBuyerTaxNumber`). Patrz `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md`. |
|
||||||
| ~~`src/Modules/Automation/AutomationController.php`~~ | ~~677~~ -> 221 | ✅ Zrefaktorowane 2026-05-20 — wydzielono `AutomationRequestParser` (422), `AutomationFormViewModel` (80), `AutomationHistoryFilters` (58); szablon `destroy/duplicate/toggleStatus` zlozony do `runIdAction()`. Zero zmian kontraktu HTTP/widokow/DB. Patrz `.paul/plans/20260520-1400-refactor-automation-controller/SUMMARY.md`. |
|
| ~~`src/Modules/Automation/AutomationController.php`~~ | ~~677~~ -> 221 | ✅ Zrefaktorowane 2026-05-20 — wydzielono `AutomationRequestParser` (422), `AutomationFormViewModel` (80), `AutomationHistoryFilters` (58); szablon `destroy/duplicate/toggleStatus` zlozony do `runIdAction()`. Zero zmian kontraktu HTTP/widokow/DB. Patrz `.paul/plans/20260520-1400-refactor-automation-controller/SUMMARY.md`. |
|
||||||
| ~~`src/Modules/Shipments/DeliveryStatus.php`~~ | ~~657~~ -> 170 | ✅ Zrefaktorowane 2026-05-19 — fasada zachowujaca pelny kontrakt + wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow. Patrz `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`. |
|
| ~~`src/Modules/Shipments/DeliveryStatus.php`~~ | ~~657~~ -> 170 | ✅ Zrefaktorowane 2026-05-19 — fasada zachowujaca pelny kontrakt + wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow. Patrz `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`. |
|
||||||
|
|||||||
@@ -2,6 +2,43 @@
|
|||||||
|
|
||||||
Chronologiczny log zmian technicznych (co i dlaczego). Najnowsze na gorze.
|
Chronologiczny log zmian technicznych (co i dlaczego). Najnowsze na gorze.
|
||||||
|
|
||||||
|
## 2026-05-20 — Dekompozycja `PolkurierShipmentService` (776 -> 299 lin., fasada + 3 wspolpracownikow)
|
||||||
|
|
||||||
|
### Co
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentService.php` zredukowany z 776 do 299 lin. (61% redukcji) — slim fasada implementujaca `ShipmentProviderInterface`, delegujaca do wspolpracownikow. `createShipment()` skrocony z ~150 do 34 lin.
|
||||||
|
- Wydzielono 3 nowe klasy w `src/Modules/Shipments/`:
|
||||||
|
- `PolkurierCarrierCatalog` (118) — `getDeliveryServices()` (z memoizacja `carriersCache`), `resolveCarrierLabel()` + prywatne heurystyki `detectPickupPointSupport`, `resolvePointCourierKey`.
|
||||||
|
- `PolkurierShipmentPayloadBuilder` (393) — `buildShipmentRequest()` (montaz `apiPayload` + rekordu `shipment_packages` w jednym przebiegu), `validateSender`, `buildSenderPayload`, `buildRecipient`, `buildPickup`, `buildCodPayload`, `normalizeShipmentType` + prywatne helpery adresowe (`splitStreetAndNumber`, `composeStreet`, `firstNonEmpty`, `normalizePhone`, `nextBusinessDay`). Zalezy od `CompanySettingsRepository` (numer konta COD) i `PolkurierCarrierCatalog` (etykieta przewoznika w rekordzie paczki — wstrzykniety, by zachowac pierwotna kolejnosc rozwiazania labela).
|
||||||
|
- `PolkurierResponseParser` (113) — bezstanowy: `extractLabelBase64`, `extractOrderNumber`, `extractTrackingNumber`.
|
||||||
|
- Fasada komponuje wspolpracownikow w konstruktorze (poor man's DI, bez nowych zaleznosci zewnetrznych). Orkiestracja `createShipment` rozbita na prywatne `sendCreateRequest()` (wywolanie API + obsluga bledu) i `persistCreationResult()` (parsowanie odpowiedzi + update paczki + synchroniczne pobranie etykiety).
|
||||||
|
- `PolkurierShipmentService::buildCodPayload()` pozostaje jako PRYWATNY forwarder do `PolkurierShipmentPayloadBuilder::buildCodPayload()` — `tests/Unit/PolkurierShipmentServiceTest.php` siega po nia przez `ReflectionMethod(PolkurierShipmentService::class, 'buildCodPayload')`.
|
||||||
|
|
||||||
|
### Dlaczego
|
||||||
|
- `PolkurierShipmentService` byl najwiekszym nieruszonym serwisem kurierskim (`quality_risks.md`: 776 lin., kandydat do podzialu), laczacym 4 odpowiedzialnosci: orkiestracja, katalog uslug, budowa payloadu, parsowanie odpowiedzi. Naruszal regule `CLAUDE.md`.
|
||||||
|
- Wzorzec sprawdzony w projekcie (InvoiceService 762->118, DeliveryStatus 657->170): slim fasada + wspolpracownicy, zero zmian publicznego kontraktu.
|
||||||
|
|
||||||
|
### Zachowane gwarancje
|
||||||
|
- Konstruktor `PolkurierShipmentService(5 zaleznosci readonly)` bez zmian — `ShipmentsModule` (factory `shipments.polkurier.service`) i `tests/Unit/PolkurierShipmentServiceTest.php::setUp()` dzialaja bez modyfikacji.
|
||||||
|
- Implementacja `ShipmentProviderInterface` (`code`, `getDeliveryServices`, `createShipment`, `checkCreationStatus`, `downloadLabel`) — sygnatury i ksztalt zwrotow niezmienione.
|
||||||
|
- `ShopproIntegrationsController::loadPolkurierServices()` korzysta tylko z publicznego `getDeliveryServices()` — bez zmian.
|
||||||
|
- Komunikaty bledow, statusy paczki, zapis `payload_json`, synchroniczne pobranie etykiety i memoizacja katalogu — przeniesione 1:1.
|
||||||
|
|
||||||
|
### Obserwacja (pre-existing, NIE adresowane w tym planie)
|
||||||
|
- `testBuildCodPayloadRequiresBankAccount` (`tests/Unit/PolkurierShipmentServiceTest.php:76`) asercja `expectExceptionMessage('Przesylka COD wymaga numeru konta bankowego.')` (litera `l`), a rzucany komunikat (HEAD i po refaktorze) to `'Przesyłka COD wymaga numeru konta bankowego. ...'` (litera `ł`). Substring nie pasuje -> ten scenariusz padal juz na baseline (HEAD). Komunikat przeniesiono BAJT-W-BAJT, wiec wynik testu jest identyczny jak przed refaktorem. Naprawa (ujednolicenie `ł`/`l`) zmienia komunikat uzytkownika -> wymaga osobnej zgody/planu.
|
||||||
|
|
||||||
|
### Deviation (rozszerzenie zakresu zatwierdzone przez uzytkownika w trakcie APPLY)
|
||||||
|
- Pierwotnie fasada wyszla 342 lin. (createShipment ~150). Na zyczenie uzytkownika dolozono ekstrakcje montazu zadania do `PolkurierShipmentPayloadBuilder::buildShipmentRequest()` -> fasada 299 lin., `createShipment` 34 lin. (cel < 50 osiagniety).
|
||||||
|
- Tradeoff: `buildShipmentRequest()` ma 86 lin. (zdominowane dwoma deklaratywnymi literalami tablic: `apiPayload` + rekord paczki; zero zagniezdzen, niska zlozonosc). Nie rozbijano na `buildApiPayload` + `buildPackageRecord`, bo wspoldziela ~10 wartosci posrednich (dim/typ/sourceOrderId/insurance/cod) — rozbicie wymusiloby albo rekalkulacje, albo metode z ~10 parametrami (gorszy zapach niz dlugi, ale plaski literal).
|
||||||
|
- Spelniony twardy prog AC-5 (< 50% oryginalu: 299 < 388).
|
||||||
|
|
||||||
|
### Weryfikacja w sesji
|
||||||
|
- `php -l` zielone dla 4 plikow (fasada + 3 nowe klasy) — dwukrotnie (po pierwszej dekompozycji i po ekstrakcji `buildShipmentRequest`).
|
||||||
|
- `vendor/phpunit` niedostepny -> uruchomienie `PolkurierShipmentServiceTest` do recznej weryfikacji.
|
||||||
|
- Kontrola statyczna: forwarder `buildCodPayload` prywatny na fasadzie; konstruktor 5-arg; guard `cod<=0` przed `getSettings()`.
|
||||||
|
|
||||||
|
### Plan
|
||||||
|
- `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/`
|
||||||
|
|
||||||
## 2026-05-20 — Dekompozycja `InvoiceService` (762 -> 118 lin., fasada + 5 wspolpracownikow)
|
## 2026-05-20 — Dekompozycja `InvoiceService` (762 -> 118 lin., fasada + 5 wspolpracownikow)
|
||||||
|
|
||||||
### Co
|
### Co
|
||||||
|
|||||||
@@ -1,7 +1,31 @@
|
|||||||
# Tooling Status
|
# Tooling Status
|
||||||
|
|
||||||
**Timestamp:** 2026-05-20 (last post-apply: refactor InvoiceService)
|
**Timestamp:** 2026-05-20 (last post-apply: refactor PolkurierShipmentService)
|
||||||
**Tryb skanu:** full (`/paul:map-codebase` 2026-05-19) + post-apply (2026-05-20)
|
**Tryb skanu:** full (`/paul:map-codebase` 2026-05-19) + post-apply + plan (2026-05-20)
|
||||||
|
|
||||||
|
## Post-apply 2026-05-20 — refactor `PolkurierShipmentService`
|
||||||
|
|
||||||
|
- Plan: `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/`
|
||||||
|
- Tryb: post-apply (radar refresh po zmianach).
|
||||||
|
- codebase-memory-mcp: `index_repository(mode=moderate)` -> **3 518 wezlow / 10 535 krawedzi** (przed apply: 3 481 / 10 297; +3 klasy: `PolkurierCarrierCatalog`, `PolkurierShipmentPayloadBuilder`, `PolkurierResponseParser`; po ekstrakcji `buildShipmentRequest`/`sendCreateRequest`/`persistCreationResult`).
|
||||||
|
- `php -l` (XAMPP `C:\xampp\php\php.exe`) na 4 plikach modulu `Shipments/` -> No syntax errors (2x: po dekompozycji i po ekstrakcji montazu zadania).
|
||||||
|
- Wynik koncowy: fasada 299, `PolkurierCarrierCatalog` 118, `PolkurierShipmentPayloadBuilder` 393, `PolkurierResponseParser` 113; `createShipment` 34 lin.
|
||||||
|
- `vendor/phpunit` NIEDOSTEPNY -> `PolkurierShipmentServiceTest` przelozony na reczna weryfikacje.
|
||||||
|
- Obserwacja: `testBuildCodPayloadRequiresBankAccount` ma pre-existing rozjazd `ł`/`l` w asercji komunikatu (baseline HEAD) — komunikat przeniesiony 1:1, wynik niezmieniony.
|
||||||
|
- jscpd / ast-grep: nadal disabled by policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan scan 2026-05-20 — refactor `PolkurierShipmentService`
|
||||||
|
|
||||||
|
- Plan: `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/`
|
||||||
|
- Tryb: targeted (`$paul-plan` impact scan).
|
||||||
|
- codebase-memory-mcp: re-index sesyjny `index_repository(mode=moderate)` -> projekt `C-visual-studio-code-orderPRO`, **3 481 wezlow / 10 297 krawedzi** (artifact_present=true). Rozbieznosc vs poprzedni snapshot (4 217/11 649) wynika z trybu `moderate` vs `full` — przy nastepnym `$paul-map-codebase` odswiezyc pelny indeks.
|
||||||
|
- Zapytania grafu: `search_graph` (PolkurierShipmentService, ShipmentProviderInterface, *ShipmentService), `get_code_snippet` (klasa + test + interfejs).
|
||||||
|
- Zweryfikowane sprzezenia: `ShipmentsModule` (factory 5-arg), `ShopproIntegrationsController::loadPolkurierServices` (tylko publiczne `getDeliveryServices()`, linia 963), `PolkurierShipmentServiceTest` (refleksja na prywatnym `buildCodPayload`).
|
||||||
|
- jscpd / ast-grep: nadal disabled by policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Post-apply 2026-05-20 — refactor `InvoiceService`
|
## Post-apply 2026-05-20 — refactor `InvoiceService`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
---
|
||||||
|
plan_id: 20260520-1615-refactor-polkurier-shipment-service
|
||||||
|
title: Refaktoring PolkurierShipmentService (776 -> fasada + 3 wspolpracownikow)
|
||||||
|
storage: plan-first
|
||||||
|
legacy_phase: null
|
||||||
|
created: 2026-05-20T16:15:00
|
||||||
|
status: planned
|
||||||
|
type: execute
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Shipments/PolkurierShipmentService.php
|
||||||
|
- src/Modules/Shipments/PolkurierCarrierCatalog.php
|
||||||
|
- src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php
|
||||||
|
- src/Modules/Shipments/PolkurierResponseParser.php
|
||||||
|
quality_radar: ok
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Rozbic `src/Modules/Shipments/PolkurierShipmentService.php` (776 lin.) na slim fasade zachowujaca pelny
|
||||||
|
publiczny kontrakt (`ShipmentProviderInterface`) + 3 wspolpracownikow o jasno rozdzielonej odpowiedzialnosci,
|
||||||
|
zgodnie z `quality_risks.md` (plik > 500 lin.) i konwencja `CLAUDE.md` (klasa/metoda 30-50 lin., max 3 poziomy zagniezdzen).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Plik to najwiekszy nieruszony serwis kurierski po refaktorach z 2026-05-19/20 (InvoiceService, DeliveryStatus,
|
||||||
|
Allegro, Statistics). Laczy 5 odpowiedzialnosci w jednej klasie: orkiestracja przesylki, katalog uslug
|
||||||
|
przewozniczych, budowa payloadu API, parsowanie odpowiedzi, helpery adresowe. Rozdzielenie poprawia czytelnosc,
|
||||||
|
testowalnosc i lokalnosc zmian bez ryzyka dla konsumentow.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- `PolkurierShipmentService` jako slim fasada (~200-220 lin.) implementujaca `ShipmentProviderInterface`.
|
||||||
|
- `PolkurierCarrierCatalog` (~135 lin.) — katalog uslug przewozniczych + heurystyki punktow odbioru.
|
||||||
|
- `PolkurierShipmentPayloadBuilder` (~290 lin.) — budowa payloadu nadawcy/odbiorcy/odbioru/COD + helpery adresowe.
|
||||||
|
- `PolkurierResponseParser` (~115 lin.) — ekstrakcja numeru zamowienia / trackingu / etykiety base64.
|
||||||
|
- Zero zmian publicznego kontraktu i konsumentow; test jednostkowy zielony bez modyfikacji.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Docs
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
@.paul/codebase/impact_map.md
|
||||||
|
@.paul/codebase/quality_risks.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Shipments/PolkurierShipmentService.php
|
||||||
|
@src/Modules/Shipments/ShipmentProviderInterface.php
|
||||||
|
@src/Modules/Shipments/ShipmentsModule.php
|
||||||
|
@tests/Unit/PolkurierShipmentServiceTest.php
|
||||||
|
@src/Modules/Settings/ShopproIntegrationsController.php
|
||||||
|
|
||||||
|
## Wzorce referencyjne (analogiczne refaktory)
|
||||||
|
- `.paul/plans/20260520-1500-refactor-invoice-service/SUMMARY.md` (fasada + 5 klas, forwarder statyczny, zero zmian kontraktu).
|
||||||
|
- `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md` (fasada + 3 klasy, kontrakt nietkniety).
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<clarifications>
|
||||||
|
- Bez pytan. Wzorzec "slim fasada + wspolpracownicy, zero zmian kontraktu" jest ustalony w projekcie i potwierdzony 5 poprzednimi refaktorami.
|
||||||
|
- Decyzja techniczna potwierdzona: `buildCodPayload` MUSI pozostac prywatna metoda na klasie `PolkurierShipmentService` (forwarder), poniewaz `PolkurierShipmentServiceTest` siega po nia przez `ReflectionMethod(PolkurierShipmentService::class, 'buildCodPayload')`.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
<impact_scan>
|
||||||
|
## Quality Radar
|
||||||
|
|
||||||
|
**Status:** ok
|
||||||
|
**Tools:** codebase-memory-mcp (3481 nodes / 10297 edges, reindex sesyjny 2026-05-20); jscpd/ast-grep — disabled by policy (`.paul/config.md`).
|
||||||
|
|
||||||
|
## Affected Areas
|
||||||
|
|
||||||
|
- Shipments — `src/Modules/Shipments/PolkurierShipmentService.php` (refaktor) + 3 nowe pliki w tym samym katalogu.
|
||||||
|
- Shipments DI — `src/Modules/Shipments/ShipmentsModule.php` (`shipments.polkurier.service`): factory MUSI pozostac bez zmian (konstruktor 5-arg).
|
||||||
|
- Konsument publiczny (interfejs) — `ShipmentController` przez `ShipmentProviderInterface` (`createShipment`, `checkCreationStatus`, `downloadLabel`).
|
||||||
|
- Konsument publiczny (dropdown uslug) — `src/Modules/Settings/ShopproIntegrationsController::loadPolkurierServices()` uzywa WYLACZNIE publicznego `getDeliveryServices()` (zweryfikowane: linia 963).
|
||||||
|
- Test (primary gate) — `tests/Unit/PolkurierShipmentServiceTest.php` siega po prywatny `buildCodPayload` przez refleksje.
|
||||||
|
|
||||||
|
## Duplicate / Hardcoded Risks
|
||||||
|
|
||||||
|
- Heurystyki punktow odbioru (`paczkomat/inpost/orlen/pocztex/kurier48`) i mapowanie `point_courier` — przeniesione do `PolkurierCarrierCatalog`, jedno zrodlo prawdy. Handled w Task 1.
|
||||||
|
- Lista dozwolonych typow przesylki polkurier (`box/envelope/palette/small_parcel/parcel_size_20`) + aliasy — przeniesione do `PolkurierShipmentPayloadBuilder::normalizeShipmentType`, jedno zrodlo prawdy. Handled w Task 2.
|
||||||
|
- Lista dozwolonych formatow etykiety (`PDF/ZPL/EPL`) wystepuje 2x (w `createShipment` i `downloadLabel`) — pozostaje w fasadzie (oba miejsca to orkiestracja); deferral, patrz nizej (niski zysk, ryzyko zmiany zachowania).
|
||||||
|
|
||||||
|
## Explicit Deferrals
|
||||||
|
|
||||||
|
- Walidacja formatu etykiety (`PDF/ZPL/EPL`) zduplikowana w dwoch metodach fasady — NIE konsolidujemy w tym planie (out of scope: refaktor ma byc bezpieczna ekstrakcja, nie zmiana logiki orkiestracji). Do rozwazenia osobno.
|
||||||
|
- Luka testowa InPost/Shoppro z `quality_risks.md` — poza zakresem (dotyczy innych plikow).
|
||||||
|
</impact_scan>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
Brak `.paul/SPECIAL-FLOWS.md` — sekcja skills pominieta.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Publiczny kontrakt fasady niezmieniony
|
||||||
|
```gherkin
|
||||||
|
Given PolkurierShipmentService implementuje ShipmentProviderInterface
|
||||||
|
When refaktor jest zakonczony
|
||||||
|
Then klasa nadal implementuje ShipmentProviderInterface z metodami code(), getDeliveryServices(), createShipment(int, array), checkCreationStatus(int), downloadLabel(int, string)
|
||||||
|
And konstruktor ma niezmienione 5 argumentow readonly (PolkurierIntegrationRepository, PolkurierApiClient, ShipmentPackageRepository, CompanySettingsRepository, OrdersRepository)
|
||||||
|
And ShipmentsModule::register dla 'shipments.polkurier.service' nie wymaga zmiany
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Test jednostkowy zielony bez modyfikacji (primary gate)
|
||||||
|
```gherkin
|
||||||
|
Given tests/Unit/PolkurierShipmentServiceTest.php uzywa ReflectionMethod(PolkurierShipmentService::class, 'buildCodPayload')
|
||||||
|
When uruchomie vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php
|
||||||
|
Then 3/3 scenariusze przechodza bez jakiejkolwiek modyfikacji pliku testu
|
||||||
|
And buildCodPayload pozostaje prywatna metoda na klasie PolkurierShipmentService
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: buildCodPayload — kolejnosc i kontrakt zachowane
|
||||||
|
```gherkin
|
||||||
|
Given formData z cod_amount <= 0
|
||||||
|
When wywolam buildCodPayload
|
||||||
|
Then zwraca null BEZ wywolania companySettings->getSettings() (testBuildCodPayloadReturnsNullWithoutCodAmount: expects never())
|
||||||
|
|
||||||
|
Given formData z cod_amount > 0 oraz pusty bank_account w companySettings
|
||||||
|
When wywolam buildCodPayload
|
||||||
|
Then rzuca ShipmentException z komunikatem 'Przesylka COD wymaga numeru konta bankowego.'
|
||||||
|
|
||||||
|
Given formData z cod_amount = '123.456' oraz bank_account = '12 3456-7890 1234 5678 9012 3456'
|
||||||
|
When wywolam buildCodPayload
|
||||||
|
Then zwraca codtype='S', codamount=123.46, codbankaccount='12345678901234567890123456', return_cod='BA'
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Zachowanie createShipment/downloadLabel/getDeliveryServices identyczne
|
||||||
|
```gherkin
|
||||||
|
Given dotychczasowe sciezki (sukces, brak order number, blad API, COD, etykieta synchroniczna, cache uslug)
|
||||||
|
When fasada deleguje do wspolpracownikow
|
||||||
|
Then ksztalt zwracanych tablic, statusy paczki (pending/created/error/label_ready), komunikaty bledow i zapis payload_json sa niezmienione
|
||||||
|
And carriersCache (memoizacja) dziala tak samo (przeniesiony do PolkurierCarrierCatalog)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Higiena rozmiaru i skladni
|
||||||
|
```gherkin
|
||||||
|
Given 4 pliki po refaktorze
|
||||||
|
When uruchomie php -l na kazdym
|
||||||
|
Then wszystkie przechodza bez bledow skladni
|
||||||
|
And PolkurierShipmentService jest istotnie mniejszy (cel ~200-220 lin., < 50% oryginalu)
|
||||||
|
And zadna nowa klasa nie wprowadza zewnetrznych zaleznosci spoza obecnych 5 (kompozycja wewnatrz fasady)
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Wydzielic PolkurierCarrierCatalog (katalog uslug przewozniczych)</name>
|
||||||
|
<files>src/Modules/Shipments/PolkurierCarrierCatalog.php (nowy), src/Modules/Shipments/PolkurierShipmentService.php</files>
|
||||||
|
<action>
|
||||||
|
Utworz `final class PolkurierCarrierCatalog` w namespace `App\Modules\Shipments`.
|
||||||
|
Konstruktor: `(private readonly PolkurierIntegrationRepository $integrationRepository, private readonly PolkurierApiClient $apiClient)`.
|
||||||
|
Przenies z fasady (bez zmiany logiki):
|
||||||
|
- pole `private ?array $carriersCache = null` (memoizacja w katalogu),
|
||||||
|
- public `getDeliveryServices(): array` (cala normalizacja + usort + cache; klucze id/name/supports_pickup_point/point_courier/foreign_shipments/raw bez zmian),
|
||||||
|
- public `resolveCarrierLabel(string $courierCode): string` (iteruje po getDeliveryServices()),
|
||||||
|
- private `detectPickupPointSupport(string $code, array $carrier): bool`,
|
||||||
|
- private `resolvePointCourierKey(string $code): ?string`.
|
||||||
|
W fasadzie: zbuduj `$this->carrierCatalog = new PolkurierCarrierCatalog($integrationRepository, $apiClient)` w konstruktorze; `getDeliveryServices()` deleguje `return $this->carrierCatalog->getDeliveryServices();`. Usun z fasady pole `carriersCache` oraz metody detectPickupPointSupport/resolvePointCourierKey/resolveCarrierLabel (przeniesione). W `createShipment` zamien `$this->resolveCarrierLabel(...)` na `$this->carrierCatalog->resolveCarrierLabel(...)`.
|
||||||
|
</action>
|
||||||
|
<verify>php -l src/Modules/Shipments/PolkurierCarrierCatalog.php; php -l src/Modules/Shipments/PolkurierShipmentService.php</verify>
|
||||||
|
<done>AC-1 (getDeliveryServices dziala przez delegacje), AC-4 (cache + ksztalt uslug identyczne), AC-5.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Wydzielic PolkurierShipmentPayloadBuilder (budowa payloadu + helpery adresowe)</name>
|
||||||
|
<files>src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php (nowy), src/Modules/Shipments/PolkurierShipmentService.php</files>
|
||||||
|
<action>
|
||||||
|
Utworz `final class PolkurierShipmentPayloadBuilder` w namespace `App\Modules\Shipments`.
|
||||||
|
Konstruktor: `(private readonly CompanySettingsRepository $companySettings)` — potrzebny wylacznie dla COD (bank_account).
|
||||||
|
Przenies z fasady BEZ zmiany logiki:
|
||||||
|
- public `validateSender(array $sender): void`,
|
||||||
|
- public `buildSenderPayload(array $sender): array`,
|
||||||
|
- public `buildRecipient(array $order, array $formData, string $receiverPointId): array`,
|
||||||
|
- public `buildPickup(array $formData): array`,
|
||||||
|
- public `buildCodPayload(array $formData): ?array` (kolejnosc: najpierw guard cod<=0 -> null, dopiero potem getSettings()),
|
||||||
|
- public `normalizeShipmentType(string $input): string`,
|
||||||
|
- private helpery: `splitStreetAndNumber`, `composeStreet`, `firstNonEmpty`, `normalizePhone`, `nextBusinessDay`.
|
||||||
|
W fasadzie: zbuduj `$this->payloadBuilder = new PolkurierShipmentPayloadBuilder($companySettings)` w konstruktorze.
|
||||||
|
W `createShipment` przekieruj wywolania: `$this->payloadBuilder->validateSender($sender)`, `->buildSenderPayload($sender)`, `->buildRecipient(...)`, `->buildPickup($formData)`, `->normalizeShipmentType(...)`.
|
||||||
|
Zachowaj w fasadzie PRYWATNY forwarder (wymog testu refleksji):
|
||||||
|
`private function buildCodPayload(array $formData): ?array { /* pinned by PolkurierShipmentServiceTest reflection */ return $this->payloadBuilder->buildCodPayload($formData); }`.
|
||||||
|
`createShipment` woła COD przez `$this->buildCodPayload($formData)` (forwarder), aby istniala jedna sciezka.
|
||||||
|
Usun z fasady oryginalne implementacje przeniesionych metod (poza forwarderem buildCodPayload).
|
||||||
|
</action>
|
||||||
|
<verify>php -l src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php; php -l src/Modules/Shipments/PolkurierShipmentService.php</verify>
|
||||||
|
<done>AC-2 (forwarder zachowany), AC-3 (kolejnosc COD + komunikat), AC-4 (payload identyczny), AC-5.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Wydzielic PolkurierResponseParser (parsowanie odpowiedzi API)</name>
|
||||||
|
<files>src/Modules/Shipments/PolkurierResponseParser.php (nowy), src/Modules/Shipments/PolkurierShipmentService.php</files>
|
||||||
|
<action>
|
||||||
|
Utworz `final class PolkurierResponseParser` w namespace `App\Modules\Shipments` (bezstanowy, bez konstruktora/zaleznosci).
|
||||||
|
Przenies z fasady BEZ zmiany logiki (lacznie z rekurencja po 'order'/[0] i listami fallbackow):
|
||||||
|
- public `extractLabelBase64(mixed $response): string`,
|
||||||
|
- public `extractOrderNumber(array $response): string`,
|
||||||
|
- public `extractTrackingNumber(array $response, string $orderno): string`.
|
||||||
|
W fasadzie: zbuduj `$this->responseParser = new PolkurierResponseParser()` w konstruktorze.
|
||||||
|
W `createShipment` zamien `$this->extractOrderNumber($response)` -> `$this->responseParser->extractOrderNumber($response)` oraz `$this->extractTrackingNumber($response, $orderno)` -> `$this->responseParser->extractTrackingNumber(...)`.
|
||||||
|
W `downloadLabel` zamien `$this->extractLabelBase64($response)` -> `$this->responseParser->extractLabelBase64($response)`.
|
||||||
|
Usun z fasady przeniesione metody.
|
||||||
|
</action>
|
||||||
|
<verify>php -l src/Modules/Shipments/PolkurierResponseParser.php; php -l src/Modules/Shipments/PolkurierShipmentService.php</verify>
|
||||||
|
<done>AC-4 (parsowanie order/tracking/label identyczne), AC-5.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Domkniecie fasady + dokumentacja techniczna</name>
|
||||||
|
<files>src/Modules/Shipments/PolkurierShipmentService.php, .paul/codebase/architecture.md, .paul/codebase/quality_risks.md, .paul/codebase/tech_changelog.md</files>
|
||||||
|
<action>
|
||||||
|
Fasada po Task 1-3 zawiera: konstruktor 5-arg (niezmieniony) budujacy 3 wspolpracownikow, `code()`,
|
||||||
|
`getDeliveryServices()` (delegacja), `createShipment()` (orkiestracja), `checkCreationStatus()`,
|
||||||
|
`downloadLabel()` (orkiestracja), private `requireCredentials()`, private `resolveStorageRoot()` (ZOSTAJE w fasadzie —
|
||||||
|
zalezy od `dirname(__DIR__, 3)`), oraz private forwarder `buildCodPayload()`.
|
||||||
|
Zweryfikuj brak martwych `use` po przeniesieniu (np. jesli ktoras stala/wyjatek nie jest juz uzywana w fasadzie)
|
||||||
|
oraz dodaj wymagane `use` w nowych plikach (IntegrationConfigException, ShipmentException, repozytoria, OrdersRepository, CompanySettingsRepository wg potrzeb klasy).
|
||||||
|
Zaktualizuj dokumentacje:
|
||||||
|
- `architecture.md` (wiersz modulu Shipments): dopisz fasade `PolkurierShipmentService` + `PolkurierCarrierCatalog` + `PolkurierShipmentPayloadBuilder` + `PolkurierResponseParser` (analogicznie do opisu DeliveryStatus).
|
||||||
|
- `quality_risks.md`: oznacz wiersz `PolkurierShipmentService.php` jako zrefaktorowany (przekreslenie + nowe liczby + odnosnik do tego planu), wzorem InvoiceService/DeliveryStatus.
|
||||||
|
- `tech_changelog.md`: wpis chronologiczny (co + dlaczego).
|
||||||
|
</action>
|
||||||
|
<verify>php -l src/Modules/Shipments/PolkurierShipmentService.php; (Measure-Object -Line) na pliku fasady — cel ~200-220 lin.</verify>
|
||||||
|
<done>AC-1, AC-5; dokumentacja zsynchronizowana zgodnie z CLAUDE.md.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 5: Weryfikacja kontraktu i testu (primary gate)</name>
|
||||||
|
<files>tests/Unit/PolkurierShipmentServiceTest.php (tylko odczyt/uruchomienie — bez modyfikacji)</files>
|
||||||
|
<action>
|
||||||
|
Uruchom `php -l` na 4 plikach (fasada + 3 nowe). Uruchom `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php`
|
||||||
|
i potwierdz 3/3 zielone BEZ modyfikacji testu. Jezeli vendor/phpunit niedostepny w sesji — udokumentuj jako
|
||||||
|
wymaganą weryfikacje reczna w SUMMARY/STATE (jak w poprzednich planach) i wykonaj statyczna kontrole, ze:
|
||||||
|
(a) `buildCodPayload` istnieje jako prywatna metoda na PolkurierShipmentService,
|
||||||
|
(b) konstruktor 5-arg niezmieniony,
|
||||||
|
(c) `companySettings->getSettings()` nie jest wolane gdy cod<=0.
|
||||||
|
</action>
|
||||||
|
<verify>vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php (lub udokumentowany fallback offline)</verify>
|
||||||
|
<done>AC-2, AC-3.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
## Do Not Change
|
||||||
|
- Sygnatura konstruktora `PolkurierShipmentService` — 5 argumentow readonly w tej samej kolejnosci. Brak nowych zewnetrznych zaleznosci (wspolpracownicy budowani wewnatrz fasady, kompozycja jak w InvoiceService).
|
||||||
|
- Implementacja `ShipmentProviderInterface` — fasada nadal implementuje interfejs; sygnatury i ksztalt zwrotow 5 metod niezmienione.
|
||||||
|
- `ShipmentsModule` factory `shipments.polkurier.service` — bez zmian.
|
||||||
|
- `tests/Unit/PolkurierShipmentServiceTest.php` — ZAKAZ modyfikacji (primary gate).
|
||||||
|
- `buildCodPayload` — pozostaje PRYWATNA metoda na klasie `PolkurierShipmentService` (forwarder dozwolony); kolejnosc guard cod<=0 PRZED getSettings() i dokladny komunikat wyjatku 'Przesylka COD wymaga numeru konta bankowego.'.
|
||||||
|
- `resolveStorageRoot()` — zostaje w fasadzie (sciezko-wrazliwe `dirname(__DIR__, 3)`).
|
||||||
|
- Zachowanie runtime: statusy paczki, komunikaty bledow, zapis `payload_json`, synchroniczne pobranie etykiety po utworzeniu, memoizacja katalogu uslug — identyczne.
|
||||||
|
|
||||||
|
## Scope Limits
|
||||||
|
- Bez zmian w `PolkurierApiClient`, `PolkurierIntegrationRepository`, `ShipmentPackageRepository`, `ShipmentController`, `ShopproIntegrationsController`.
|
||||||
|
- Bez zmian schematu DB, migracji, widokow, routingu.
|
||||||
|
- Bez konsolidacji walidacji formatu etykiety (PDF/ZPL/EPL) — jawnie odroczone w `<impact_scan>`.
|
||||||
|
- Czysty refaktor: zero zmian funkcjonalnych/poprawek bugow (jesli wykryto pre-existing bug, zaraportowac w SUMMARY, nie naprawiac w tym planie bez zgody).
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- [ ] `php -l` zielony dla: PolkurierShipmentService.php, PolkurierCarrierCatalog.php, PolkurierShipmentPayloadBuilder.php, PolkurierResponseParser.php.
|
||||||
|
- [ ] `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php` — 3/3 bez modyfikacji testu (lub udokumentowany fallback offline + kontrola statyczna).
|
||||||
|
- [ ] `PolkurierShipmentService` implementuje `ShipmentProviderInterface`; konstruktor 5-arg niezmieniony; `buildCodPayload` prywatna na klasie.
|
||||||
|
- [ ] Fasada ~200-220 lin. (< 50% oryginalu 776).
|
||||||
|
- [ ] Smoke (reczny, jesli DB dostepna): `POST /orders/{id}/shipment/prepare` dla zamowienia z mapowaniem polkurier; dropdown uslug w `/settings/integrations/shoppro` (publiczne getDeliveryServices).
|
||||||
|
- [ ] Dokumentacja zaktualizowana: architecture.md, quality_risks.md (wiersz oznaczony ✅), tech_changelog.md, tooling_status.md (wpis radaru).
|
||||||
|
- [ ] Quality Radar relevant risks handled or deferred (patrz `<impact_scan>`).
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] Wszystkie AC (AC-1..AC-5) spelnione.
|
||||||
|
- [ ] Verification kompletna.
|
||||||
|
- [ ] Docs/radar reports zaktualizowane (architecture.md, quality_risks.md, tech_changelog.md, tooling_status.md).
|
||||||
|
- [ ] Zero zmian publicznego kontraktu i konsumentow.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
SUMMARY.md path: `.paul/plans/20260520-1615-refactor-polkurier-shipment-service/SUMMARY.md`
|
||||||
|
|
||||||
|
Notatka do UNIFY: oznaczyc wiersz `PolkurierShipmentService.php` w `.paul/codebase/quality_risks.md` jako ✅
|
||||||
|
(776 -> liczba_fasady) z odnosnikiem do tego planu; dopisac delty radaru i tooling_status.md.
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
plan_id: 20260520-1615-refactor-polkurier-shipment-service
|
||||||
|
title: Refaktoring PolkurierShipmentService (776 -> fasada + 3 wspolpracownikow)
|
||||||
|
completed: 2026-05-20T17:10:00
|
||||||
|
storage: plan-first
|
||||||
|
quality_radar: ok
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary: Refaktoring `PolkurierShipmentService`
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Rozbic `src/Modules/Shipments/PolkurierShipmentService.php` (776 lin.) na slim fasade zachowujaca pelny
|
||||||
|
publiczny kontrakt (`ShipmentProviderInterface`) + wspolpracownikow o jednej odpowiedzialnosci, zgodnie z
|
||||||
|
`quality_risks.md` (plik > 500 lin.) i konwencja `CLAUDE.md`. Zero zmian kontraktu i konsumentow.
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
| Area | Result |
|
||||||
|
|------|--------|
|
||||||
|
| Fasada | `PolkurierShipmentService` 776 -> **299** lin. (61% redukcji); `createShipment` ~150 -> **34** lin.; orkiestracja create rozbita na prywatne `sendCreateRequest` + `persistCreationResult` |
|
||||||
|
| Katalog uslug | `PolkurierCarrierCatalog` (118) — `getDeliveryServices` (memoizacja), `resolveCarrierLabel` + heurystyki punktow odbioru |
|
||||||
|
| Builder payloadu | `PolkurierShipmentPayloadBuilder` (393) — `buildShipmentRequest` (montaz apiPayload + rekordu paczki), sender/recipient/pickup/COD/typ + helpery adresowe |
|
||||||
|
| Parser odpowiedzi | `PolkurierResponseParser` (113) — bezstanowy `extractLabelBase64`/`extractOrderNumber`/`extractTrackingNumber` |
|
||||||
|
| Kontrakt | Konstruktor 5-arg readonly i interfejs niezmienione; `buildCodPayload` prywatny forwarder (pinned przez test refleksji) |
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentService.php` — slim fasada (orkiestracja + delegacja + forwarder COD).
|
||||||
|
- `src/Modules/Shipments/PolkurierCarrierCatalog.php` — NOWY (katalog uslug przewozniczych).
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php` — NOWY (montaz zadania + budowa sekcji payloadu).
|
||||||
|
- `src/Modules/Shipments/PolkurierResponseParser.php` — NOWY (parsowanie odpowiedzi API).
|
||||||
|
- `.paul/codebase/architecture.md`, `quality_risks.md`, `tech_changelog.md`, `tooling_status.md`, `.paul/STATE.md` — dokumentacja.
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| AC-1 Kontrakt fasady niezmieniony | Pass | Implementuje `ShipmentProviderInterface` (5 metod); konstruktor 5-arg readonly bez zmian; `ShipmentsModule` factory `shipments.polkurier.service` nietkniety |
|
||||||
|
| AC-2 Test bez modyfikacji (primary gate) | Pass (offline) | Plik testu NIE modyfikowany; `buildCodPayload` pozostaje prywatna metoda na `PolkurierShipmentService` (forwarder). **Korekta wzgledem brzmienia AC:** baseline HEAD to 2/3, NIE 3/3 — `testBuildCodPayloadRequiresBankAccount` pada na pre-existing rozjezdzie `ł`/`l` w komunikacie. Komunikat przeniesiono 1:1, wiec wynik = baseline (brak regresji). `vendor/phpunit` niedostepny w sesji -> uruchomienie reczne |
|
||||||
|
| AC-3 COD kolejnosc + komunikat | Pass (static) | `buildCodPayload`: guard `cod<=0` -> `null` PRZED `getSettings()`; komunikat `'Przesyłka COD wymaga numeru konta bankowego. ...'` 1:1; `codtype=S`, `codamount=round(2)`, `codbankaccount` digits-only, `return_cod=BA` |
|
||||||
|
| AC-4 Zachowanie create/download/getDeliveryServices | Pass (static) | Kod przeniesiony 1:1; kolejnosc rozwiazania `resolveCarrierLabel` zachowana (katalog wstrzykniety do buildera, label liczony po COD, przed rekordem paczki); chain `label_format` rownowazny dla wszystkich przypadkow wejscia |
|
||||||
|
| AC-5 Higiena rozmiaru/skladni | Pass | `php -l` 4/4 czyste; fasada 299 < 388 (50% z 776); brak nowych zaleznosci zewnetrznych (kompozycja w konstruktorze) |
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
| Check | Result | Notes |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| `php -l` (4 pliki) | Pass | XAMPP `C:\xampp\php\php.exe` — No syntax errors; uruchomione 2x (po dekompozycji i po ekstrakcji `buildShipmentRequest`) |
|
||||||
|
| `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php` | Skipped | `vendor/` niedostepny w sesji -> reczna weryfikacja (primary gate) |
|
||||||
|
| Kontrola statyczna kontraktu | Pass | konstruktor 5-arg; `buildCodPayload` prywatny na fasadzie; konsumenci uzywaja tylko publicznego API |
|
||||||
|
| Line counts | Pass | fasada 299, catalog 118, builder 393, parser 113; `createShipment` 34; `buildShipmentRequest` 86 |
|
||||||
|
| `git diff --stat` | Info | fasada -535/+~ (Write x2); 3 nowe pliki untracked |
|
||||||
|
|
||||||
|
## Quality Radar Results
|
||||||
|
|
||||||
|
**Status:** ok
|
||||||
|
|
||||||
|
- New risks: brak.
|
||||||
|
- Resolved risks: `PolkurierShipmentService.php` (776 lin.) skreslony z listy plikow > 500 lin. w `quality_risks.md` (oznaczony ✅).
|
||||||
|
- Deferred risks:
|
||||||
|
- Walidacja formatu etykiety `PDF/ZPL/EPL` zduplikowana w `createShipment`/`downloadLabel` (orkiestracja) — poza zakresem refaktoru.
|
||||||
|
- Pre-existing rozjazd `ł`/`l` w `testBuildCodPayloadRequiresBankAccount` — naprawa zmienia komunikat uzytkownika, wymaga osobnej zgody/planu.
|
||||||
|
- codebase-memory-mcp: 3 481/10 297 (plan) -> 3 514/10 521 (post-apply) -> 3 518/10 535 (po ekstrakcji). jscpd/ast-grep disabled by policy.
|
||||||
|
- Raw outputs: `.paul/codebase/radar/`, `.paul/codebase/tooling_status.md`.
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
- **Rozszerzenie zakresu (zatwierdzone przez uzytkownika w trakcie APPLY):** plan zakladal fasade + 3 klasy z `createShipment` jako orkiestracja. Pierwsza dekompozycja dala fasade 342 / `createShipment` ~150. Na zyczenie uzytkownika dolozono ekstrakcje montazu zadania do `PolkurierShipmentPayloadBuilder::buildShipmentRequest()` oraz rozbicie orkiestracji na `sendCreateRequest` + `persistCreationResult` -> fasada 299, `createShipment` 34.
|
||||||
|
- **Tradeoff:** `buildShipmentRequest` ma 86 lin. (zdominowane dwoma plaskimi literalami tablic: `apiPayload` + rekord `shipment_packages`; zero zagniezdzen, niska zlozonosc). Nie rozbito na `buildApiPayload` + `buildPackageRecord`, bo wspoldziela ~10 wartosci posrednich — rozbicie wymusiloby rekalkulacje albo metode z ~10 parametrami (gorszy zapach niz dlugi, plaski literal).
|
||||||
|
- **Builder zalezy od katalogu:** `PolkurierShipmentPayloadBuilder` dostaje `PolkurierCarrierCatalog` w konstruktorze, by `resolveCarrierLabel` wykonalo sie w tym samym miejscu co w oryginale (zachowanie kolejnosci i braku dodatkowego callu API w sciezce bledu).
|
||||||
|
|
||||||
|
## Key Decisions / Patterns
|
||||||
|
|
||||||
|
- Wzorzec projektu potwierdzony: slim fasada + wspolpracownicy, kompozycja w konstruktorze (poor man's DI), zero zmian publicznego kontraktu (jak InvoiceService, DeliveryStatus).
|
||||||
|
- Test refleksyjny jako twarde ograniczenie: prywatna metoda pinned przez `ReflectionMethod` zostaje na klasie jako forwarder (analogicznie do statyka `extractBuyerTaxNumber` w InvoiceService).
|
||||||
|
- Refaktor czysty: zero zmian funkcjonalnych; wykryty pre-existing defekt (`ł`/`l`) zaraportowany, nie naprawiony.
|
||||||
|
|
||||||
|
## Follow-up
|
||||||
|
|
||||||
|
- **Reczna weryfikacja (primary gate):** `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php` — potwierdzic wynik = baseline (2/3 z pre-existing `ł`/`l`); smoke `POST /orders/{id}/shipment/prepare` (polkurier) + dropdown `/settings/integrations/shoppro`.
|
||||||
|
- **Osobny plan (opcjonalny):** ujednolicenie komunikatu COD `ł`/`l` (test vs kod) — male, ale zmienia komunikat uzytkownika.
|
||||||
|
- **Kolejni kandydaci z `quality_risks.md`:** `OrdersController.php` (1490), `OrdersRepository.php` (1243), `ShopproIntegrationsController.php` (1076), `ApaczkaShipmentService.php` (1044).
|
||||||
118
src/Modules/Shipments/PolkurierCarrierCatalog.php
Normal file
118
src/Modules/Shipments/PolkurierCarrierCatalog.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Shipments;
|
||||||
|
|
||||||
|
use App\Modules\Settings\PolkurierApiClient;
|
||||||
|
use App\Modules\Settings\PolkurierIntegrationRepository;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Katalog uslug przewozniczych polkurier.
|
||||||
|
*
|
||||||
|
* Pobiera i normalizuje liste przewoznikow (z memoizacja per instancja), wykrywa wsparcie
|
||||||
|
* punktow odbioru oraz rozwiazuje czytelna etykiete przewoznika po jego kodzie uslugi.
|
||||||
|
*/
|
||||||
|
final class PolkurierCarrierCatalog
|
||||||
|
{
|
||||||
|
/** @var array<int, array<string, mixed>>|null */
|
||||||
|
private ?array $carriersCache = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PolkurierIntegrationRepository $integrationRepository,
|
||||||
|
private readonly PolkurierApiClient $apiClient
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function getDeliveryServices(): array
|
||||||
|
{
|
||||||
|
if ($this->carriersCache !== null) {
|
||||||
|
return $this->carriersCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->integrationRepository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
return $this->carriersCache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$carriers = $this->apiClient->getAvailableCarriers(
|
||||||
|
$credentials['login'],
|
||||||
|
$credentials['api_token']
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return $this->carriersCache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizacja: ujednolicony shape `{id, name, supports_pickup_point, foreign_shipments}`
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($carriers as $carrier) {
|
||||||
|
if (!is_array($carrier)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$code = trim((string) ($carrier['servicecode'] ?? $carrier['code'] ?? ''));
|
||||||
|
$name = trim((string) ($carrier['name'] ?? $code));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$supportsPoint = $this->detectPickupPointSupport($code, $carrier);
|
||||||
|
$normalized[] = [
|
||||||
|
'id' => $code,
|
||||||
|
'name' => $name !== '' ? $name : $code,
|
||||||
|
'supports_pickup_point' => $supportsPoint,
|
||||||
|
'point_courier' => $supportsPoint ? $this->resolvePointCourierKey($code) : null,
|
||||||
|
'foreign_shipments' => !empty($carrier['foreign_shipments']),
|
||||||
|
'raw' => $carrier,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($normalized, static fn ($a, $b) => strcasecmp((string) $a['name'], (string) $b['name']));
|
||||||
|
return $this->carriersCache = $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveCarrierLabel(string $courierCode): string
|
||||||
|
{
|
||||||
|
foreach ($this->getDeliveryServices() as $service) {
|
||||||
|
if (strcasecmp((string) ($service['id'] ?? ''), $courierCode) === 0) {
|
||||||
|
return (string) ($service['name'] ?? $courierCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $courierCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $carrier
|
||||||
|
*/
|
||||||
|
private function detectPickupPointSupport(string $code, array $carrier): bool
|
||||||
|
{
|
||||||
|
$haystack = strtolower($code . ' ' . trim((string) ($carrier['name'] ?? '')));
|
||||||
|
return str_contains($haystack, 'paczkomat')
|
||||||
|
|| str_contains($haystack, 'parcel')
|
||||||
|
|| str_contains($haystack, 'inpost')
|
||||||
|
|| str_contains($haystack, 'orlen')
|
||||||
|
|| str_contains($haystack, 'pocztex')
|
||||||
|
|| str_contains($haystack, 'kurier48')
|
||||||
|
|| str_contains($haystack, 'punkt');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePointCourierKey(string $code): ?string
|
||||||
|
{
|
||||||
|
$lower = strtolower($code);
|
||||||
|
if (str_contains($lower, 'inpost') || str_contains($lower, 'paczkomat')) {
|
||||||
|
return 'inpost';
|
||||||
|
}
|
||||||
|
if (str_contains($lower, 'orlen')) {
|
||||||
|
return 'orlen';
|
||||||
|
}
|
||||||
|
if (str_contains($lower, 'pocztex')) {
|
||||||
|
return 'pocztex';
|
||||||
|
}
|
||||||
|
if (str_contains($lower, 'kurier48')) {
|
||||||
|
return 'kurier48';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Modules/Shipments/PolkurierResponseParser.php
Normal file
113
src/Modules/Shipments/PolkurierResponseParser.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Shipments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser odpowiedzi API polkurier.
|
||||||
|
*
|
||||||
|
* Bezstanowa ekstrakcja numeru zamowienia, numeru trackingu i etykiety base64
|
||||||
|
* z roznych wariantow ksztaltu odpowiedzi (opakowanie w {order:...}, listy, fallbacki kluczy).
|
||||||
|
*/
|
||||||
|
final class PolkurierResponseParser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* polkurier get_label zwraca base64 PDF pod kluczem 'file' (zweryfikowane w SDK GetLabel.php).
|
||||||
|
*/
|
||||||
|
public function extractLabelBase64(mixed $response): string
|
||||||
|
{
|
||||||
|
if (is_string($response)) {
|
||||||
|
return trim($response);
|
||||||
|
}
|
||||||
|
if (is_array($response)) {
|
||||||
|
foreach (['file', 'label', 'pdf', 'data', 'content', 'zpl', 'epl'] as $key) {
|
||||||
|
if (isset($response[$key]) && is_string($response[$key])) {
|
||||||
|
$candidate = trim((string) $response[$key]);
|
||||||
|
if ($candidate !== '') {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
return $this->extractLabelBase64($response[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier create_order zwraca Order entity. Numer zamówienia jest w polu 'number'
|
||||||
|
* (zmapowańe z setNumber() w SDK Order.php). Fallbacki dla mozliwych wariantow.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $response
|
||||||
|
*/
|
||||||
|
public function extractOrderNumber(array $response): string
|
||||||
|
{
|
||||||
|
// Czasami polkurier opakowuje w {order: {...}} lub zwraca listę
|
||||||
|
if (isset($response['order']) && is_array($response['order'])) {
|
||||||
|
$inner = $this->extractOrderNumber($response['order']);
|
||||||
|
if ($inner !== '') {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
$inner = $this->extractOrderNumber($response[0]);
|
||||||
|
if ($inner !== '') {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['number', 'orderno', 'order_no', 'order_number', 'order_id', 'id'] as $key) {
|
||||||
|
$value = trim((string) ($response[$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waybill polkurier zazwyczaj w `waybills[0].number` (OrderWaybill entity).
|
||||||
|
* Fallbacki dla starszych wariantow odpowiedźi.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $response
|
||||||
|
*/
|
||||||
|
public function extractTrackingNumber(array $response, string $orderno): string
|
||||||
|
{
|
||||||
|
if (isset($response['order']) && is_array($response['order'])) {
|
||||||
|
$inner = $this->extractTrackingNumber($response['order'], $orderno);
|
||||||
|
if ($inner !== '') {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$waybills = $response['waybills'] ?? $response['waybill'] ?? null;
|
||||||
|
if (is_array($waybills)) {
|
||||||
|
// Lista OrderWaybill
|
||||||
|
if (isset($waybills[0]) && is_array($waybills[0])) {
|
||||||
|
foreach (['number', 'waybillno', 'waybill_number', 'tracking_number'] as $key) {
|
||||||
|
$value = trim((string) ($waybills[0][$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pojedynczy obiekt
|
||||||
|
foreach (['number', 'waybillno', 'waybill_number', 'tracking_number'] as $key) {
|
||||||
|
$value = trim((string) ($waybills[$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['waybillno', 'waybill_number', 'parcel_number', 'tracking_number'] as $key) {
|
||||||
|
$value = trim((string) ($response[$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $orderno;
|
||||||
|
}
|
||||||
|
}
|
||||||
393
src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php
Normal file
393
src/Modules/Shipments/PolkurierShipmentPayloadBuilder.php
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Shipments;
|
||||||
|
|
||||||
|
use App\Core\Exceptions\ShipmentException;
|
||||||
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Budowniczy payloadu przesylki polkurier.
|
||||||
|
*
|
||||||
|
* Skupia walidacje nadawcy, montaz pelnego zadania przesylki (payload API + rekord shipment_packages),
|
||||||
|
* budowe poszczegolnych sekcji payloadu (sender/recipient/pickup/COD), normalizacje typu przesylki
|
||||||
|
* oraz pomocnicze parsowanie danych adresowych.
|
||||||
|
* CompanySettingsRepository jest potrzebny wylacznie dla numeru konta bankowego (COD);
|
||||||
|
* PolkurierCarrierCatalog sluzy do rozwiazania czytelnej etykiety przewoznika do rekordu paczki.
|
||||||
|
*/
|
||||||
|
final class PolkurierShipmentPayloadBuilder
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CompanySettingsRepository $companySettings,
|
||||||
|
private readonly PolkurierCarrierCatalog $carrierCatalog
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buduje komplet danych przesylki: payload do API oraz rekord do zapisu w shipment_packages.
|
||||||
|
* Kolejnosc obliczen (w tym moment rozwiazania etykiety przewoznika) zachowana 1:1 z pierwotnym createShipment.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
* @param array<string, mixed> $formData
|
||||||
|
* @param array<string, mixed> $sender
|
||||||
|
* @return array{api: array<string, mixed>, package: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
public function buildShipmentRequest(
|
||||||
|
array $order,
|
||||||
|
array $formData,
|
||||||
|
array $sender,
|
||||||
|
string $courierCode,
|
||||||
|
int $orderId,
|
||||||
|
string $defaultLabelFormat
|
||||||
|
): array {
|
||||||
|
$orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
|
||||||
|
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ($orderData['external_order_number'] ?? '')));
|
||||||
|
$description = 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId);
|
||||||
|
|
||||||
|
$weightKg = max(0.001, (float) ($formData['weight_kg'] ?? 1.0));
|
||||||
|
$lengthCm = max(1.0, (float) ($formData['length_cm'] ?? 25.0));
|
||||||
|
$widthCm = max(1.0, (float) ($formData['width_cm'] ?? 20.0));
|
||||||
|
$heightCm = max(1.0, (float) ($formData['height_cm'] ?? 8.0));
|
||||||
|
$shipmentType = $this->normalizeShipmentType((string) ($formData['package_type'] ?? 'BOX'));
|
||||||
|
|
||||||
|
$receiverPointId = trim((string) ($formData['receiver_point_id'] ?? ''));
|
||||||
|
$recipient = $this->buildRecipient($order, $formData, $receiverPointId);
|
||||||
|
$senderPayload = $this->buildSenderPayload($sender);
|
||||||
|
|
||||||
|
$packs = [[
|
||||||
|
'length' => (int) round($lengthCm),
|
||||||
|
'width' => (int) round($widthCm),
|
||||||
|
'height' => (int) round($heightCm),
|
||||||
|
'weight' => round($weightKg, 3),
|
||||||
|
'amount' => 1,
|
||||||
|
'type' => $shipmentType,
|
||||||
|
]];
|
||||||
|
|
||||||
|
$pickup = $this->buildPickup($formData);
|
||||||
|
|
||||||
|
$apiPayload = [
|
||||||
|
'shipmenttype' => $shipmentType,
|
||||||
|
'courier' => $courierCode,
|
||||||
|
'description' => $description,
|
||||||
|
'sender' => $senderPayload,
|
||||||
|
'recipient' => $recipient,
|
||||||
|
'packs' => $packs,
|
||||||
|
'pickup' => $pickup,
|
||||||
|
];
|
||||||
|
|
||||||
|
$insurance = max(0.0, (float) ($formData['insurance_amount'] ?? 0));
|
||||||
|
if ($insurance > 0) {
|
||||||
|
$apiPayload['insurance'] = round($insurance, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cod = max(0.0, (float) ($formData['cod_amount'] ?? 0));
|
||||||
|
$codPayload = $this->buildCodPayload($formData);
|
||||||
|
if ($codPayload !== null) {
|
||||||
|
$apiPayload['COD'] = $codPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carrierLabel = $this->carrierCatalog->resolveCarrierLabel($courierCode);
|
||||||
|
$labelFormat = strtoupper(trim((string) ($formData['label_format'] ?? $defaultLabelFormat)));
|
||||||
|
if (!in_array($labelFormat, ['PDF', 'ZPL', 'EPL'], true)) {
|
||||||
|
$labelFormat = 'PDF';
|
||||||
|
}
|
||||||
|
|
||||||
|
$package = [
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'provider' => 'polkurier',
|
||||||
|
'delivery_method_id' => $courierCode,
|
||||||
|
'credentials_id' => null,
|
||||||
|
'command_id' => null,
|
||||||
|
'status' => 'pending',
|
||||||
|
'carrier_id' => $carrierLabel,
|
||||||
|
'package_type' => $shipmentType,
|
||||||
|
'weight_kg' => $weightKg,
|
||||||
|
'length_cm' => $lengthCm,
|
||||||
|
'width_cm' => $widthCm,
|
||||||
|
'height_cm' => $heightCm,
|
||||||
|
'insurance_amount' => $insurance > 0 ? $insurance : null,
|
||||||
|
'insurance_currency' => 'PLN',
|
||||||
|
'cod_amount' => $cod > 0 ? $cod : null,
|
||||||
|
'cod_currency' => $cod > 0 ? 'PLN' : null,
|
||||||
|
'label_format' => $labelFormat,
|
||||||
|
'receiver_point_id' => $receiverPointId,
|
||||||
|
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
||||||
|
'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
||||||
|
'payload_json' => $apiPayload,
|
||||||
|
];
|
||||||
|
|
||||||
|
return ['api' => $apiPayload, 'package' => $package];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $sender
|
||||||
|
*/
|
||||||
|
public function validateSender(array $sender): void
|
||||||
|
{
|
||||||
|
$required = ['street', 'city', 'postalCode'];
|
||||||
|
foreach ($required as $key) {
|
||||||
|
if (trim((string) ($sender[$key] ?? '')) === '') {
|
||||||
|
throw new ShipmentException('Niekompletne dane nadawcy w Ustawieniach firmy (ulica/miasto/kod pocztowy).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$name = trim((string) ($sender['name'] ?? ''));
|
||||||
|
$company = trim((string) ($sender['company'] ?? ''));
|
||||||
|
if ($name === '' && $company === '') {
|
||||||
|
throw new ShipmentException('Niekompletne dane nadawcy w Ustawieniach firmy (imie/nazwisko lub nazwa firmy).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $sender
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function buildSenderPayload(array $sender): array
|
||||||
|
{
|
||||||
|
$street = trim((string) ($sender['street'] ?? ''));
|
||||||
|
$parsed = $this->splitStreetAndNumber($street);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'company' => trim((string) ($sender['company'] ?? '')),
|
||||||
|
'person' => trim((string) ($sender['contactPerson'] ?? $sender['name'] ?? '')),
|
||||||
|
'street' => $parsed['street'],
|
||||||
|
'housenumber' => $parsed['house'],
|
||||||
|
'flatnumber' => $parsed['flat'],
|
||||||
|
'postcode' => trim((string) ($sender['postalCode'] ?? '')),
|
||||||
|
'city' => trim((string) ($sender['city'] ?? '')),
|
||||||
|
'email' => trim((string) ($sender['email'] ?? '')),
|
||||||
|
'phone' => $this->normalizePhone((string) ($sender['phone'] ?? '')),
|
||||||
|
'country' => strtoupper(trim((string) ($sender['countryCode'] ?? 'PL'))) ?: 'PL',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
* @param array<string, mixed> $formData
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function buildRecipient(array $order, array $formData, string $receiverPointId): array
|
||||||
|
{
|
||||||
|
$addresses = is_array($order['addresses'] ?? null) ? $order['addresses'] : [];
|
||||||
|
$delivery = [];
|
||||||
|
$customer = [];
|
||||||
|
foreach ($addresses as $addr) {
|
||||||
|
$type = (string) ($addr['address_type'] ?? '');
|
||||||
|
if ($type === 'delivery') {
|
||||||
|
$delivery = is_array($addr) ? $addr : [];
|
||||||
|
} elseif ($type === 'customer') {
|
||||||
|
$customer = is_array($addr) ? $addr : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_name'] ?? null,
|
||||||
|
$delivery['name'] ?? null,
|
||||||
|
$customer['name'] ?? null,
|
||||||
|
$delivery['company_name'] ?? null,
|
||||||
|
$customer['company_name'] ?? null,
|
||||||
|
'Klient',
|
||||||
|
]);
|
||||||
|
$company = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_company'] ?? null,
|
||||||
|
$delivery['company_name'] ?? null,
|
||||||
|
$customer['company_name'] ?? null,
|
||||||
|
]);
|
||||||
|
$streetLine = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_street'] ?? null,
|
||||||
|
$this->composeStreet($delivery),
|
||||||
|
$this->composeStreet($customer),
|
||||||
|
]);
|
||||||
|
$parsed = $this->splitStreetAndNumber($streetLine);
|
||||||
|
$postcode = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_postal_code'] ?? null,
|
||||||
|
$delivery['zip_code'] ?? null,
|
||||||
|
$customer['zip_code'] ?? null,
|
||||||
|
]);
|
||||||
|
$city = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_city'] ?? null,
|
||||||
|
$delivery['city'] ?? null,
|
||||||
|
$customer['city'] ?? null,
|
||||||
|
]);
|
||||||
|
$country = strtoupper($this->firstNonEmpty([
|
||||||
|
$formData['receiver_country_code'] ?? null,
|
||||||
|
$delivery['country'] ?? null,
|
||||||
|
$customer['country'] ?? null,
|
||||||
|
'PL',
|
||||||
|
]));
|
||||||
|
$phone = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_phone'] ?? null,
|
||||||
|
$delivery['phone'] ?? null,
|
||||||
|
$customer['phone'] ?? null,
|
||||||
|
]);
|
||||||
|
$email = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_email'] ?? null,
|
||||||
|
$delivery['email'] ?? null,
|
||||||
|
$customer['email'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($name === '' || $postcode === '' || $city === '') {
|
||||||
|
throw new ShipmentException('Brak wymaganych danych adresowych odbiorcy (imie/kod pocztowy/miasto).');
|
||||||
|
}
|
||||||
|
if ($receiverPointId === '' && $parsed['street'] === '') {
|
||||||
|
throw new ShipmentException('Brak ulicy odbiorcy (wymagana dla usług kurierskich).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'company' => $company,
|
||||||
|
'person' => $name,
|
||||||
|
'street' => $parsed['street'],
|
||||||
|
'housenumber' => $parsed['house'],
|
||||||
|
'flatnumber' => $parsed['flat'],
|
||||||
|
'postcode' => $postcode,
|
||||||
|
'city' => $city,
|
||||||
|
'email' => $email,
|
||||||
|
'phone' => $this->normalizePhone($phone),
|
||||||
|
'country' => $country !== '' ? $country : 'PL',
|
||||||
|
'point_id' => $receiverPointId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $formData
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function buildPickup(array $formData): array
|
||||||
|
{
|
||||||
|
$date = trim((string) ($formData['pickup_date'] ?? ''));
|
||||||
|
if ($date === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
|
||||||
|
$date = $this->nextBusinessDay();
|
||||||
|
}
|
||||||
|
$from = trim((string) ($formData['pickup_time_from'] ?? '10:00'));
|
||||||
|
$to = trim((string) ($formData['pickup_time_to'] ?? '16:00'));
|
||||||
|
$noCourierOrder = !empty($formData['no_courier_order']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pickupdate' => $date,
|
||||||
|
'pickuptimefrom' => $from,
|
||||||
|
'pickuptimeto' => $to,
|
||||||
|
'nocourierorder' => $noCourierOrder,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $formData
|
||||||
|
* @return array{codtype: string, codamount: float, codbankaccount: string, return_cod: string}|null
|
||||||
|
*/
|
||||||
|
public function buildCodPayload(array $formData): ?array
|
||||||
|
{
|
||||||
|
$cod = max(0.0, (float) ($formData['cod_amount'] ?? 0));
|
||||||
|
if ($cod <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$companySettings = $this->companySettings->getSettings();
|
||||||
|
$bankAccount = preg_replace('/[^0-9]/', '', (string) ($companySettings['bank_account'] ?? '')) ?? '';
|
||||||
|
if ($bankAccount === '') {
|
||||||
|
throw new ShipmentException('Przesyłka COD wymaga numeru konta bankowego. Uzupelnij go w Ustawienia > Firma.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'codtype' => 'S',
|
||||||
|
'codamount' => round($cod, 2),
|
||||||
|
'codbankaccount' => $bankAccount,
|
||||||
|
'return_cod' => 'BA',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier API wymaga lowercase z dozwolonego zbioru:
|
||||||
|
* [box, envelope, palette, small_parcel, parcel_size_20].
|
||||||
|
* Mapuje istniejące orderPRO wartości (PACKAGE/BOX/ENVELOPE/...) na format polkurier.
|
||||||
|
*/
|
||||||
|
public function normalizeShipmentType(string $input): string
|
||||||
|
{
|
||||||
|
$raw = strtolower(trim($input));
|
||||||
|
$allowed = ['box', 'envelope', 'palette', 'small_parcel', 'parcel_size_20'];
|
||||||
|
if (in_array($raw, $allowed, true)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliases = [
|
||||||
|
'package' => 'box',
|
||||||
|
'parcel' => 'box',
|
||||||
|
'paczka' => 'box',
|
||||||
|
'koperta' => 'envelope',
|
||||||
|
'paleta' => 'palette',
|
||||||
|
'mala_paczka' => 'small_parcel',
|
||||||
|
'small' => 'small_parcel',
|
||||||
|
];
|
||||||
|
return $aliases[$raw] ?? 'box';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextBusinessDay(): string
|
||||||
|
{
|
||||||
|
$ts = time();
|
||||||
|
do {
|
||||||
|
$ts = strtotime('+1 day', $ts);
|
||||||
|
$dow = (int) date('N', $ts ?: time());
|
||||||
|
} while ($dow >= 6);
|
||||||
|
return date('Y-m-d', $ts ?: time());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{street: string, house: string, flat: string}
|
||||||
|
*/
|
||||||
|
private function splitStreetAndNumber(string $streetLine): array
|
||||||
|
{
|
||||||
|
$street = trim($streetLine);
|
||||||
|
if ($street === '') {
|
||||||
|
return ['street' => '', 'house' => '', 'flat' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wzorce: "Marszalkowska 10/5", "Marszalkowska 10 m. 5", "Marszalkowska 10A"
|
||||||
|
if (preg_match('/^(.*?)\s+(\d+[A-Za-z]?)(?:\s*[\/\-]\s*|\s*m\.?\s*)?(\d+[A-Za-z]?)?$/u', $street, $matches) === 1) {
|
||||||
|
return [
|
||||||
|
'street' => trim((string) $matches[1]),
|
||||||
|
'house' => trim((string) ($matches[2] ?? '')),
|
||||||
|
'flat' => trim((string) ($matches[3] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['street' => $street, 'house' => '', 'flat' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $address
|
||||||
|
*/
|
||||||
|
private function composeStreet(array $address): string
|
||||||
|
{
|
||||||
|
$street = trim((string) ($address['street_name'] ?? ''));
|
||||||
|
$number = trim((string) ($address['street_number'] ?? ''));
|
||||||
|
if ($street === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $number !== '' ? trim($street . ' ' . $number) : $street;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $candidates
|
||||||
|
*/
|
||||||
|
private function firstNonEmpty(array $candidates): string
|
||||||
|
{
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
$value = trim((string) $candidate);
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePhone(string $phone): string
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D+/', '', $phone) ?? '';
|
||||||
|
if ($digits === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// Polkurier akceptuje cyfry. Usuń prefiks 48 jezeli jest podwojny.
|
||||||
|
if (str_starts_with($digits, '48') && strlen($digits) === 11) {
|
||||||
|
$digits = substr($digits, 2);
|
||||||
|
}
|
||||||
|
return $digits;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,13 +14,18 @@ use Throwable;
|
|||||||
/**
|
/**
|
||||||
* polkurier.pl ShipmentProvider (Phase 128).
|
* polkurier.pl ShipmentProvider (Phase 128).
|
||||||
*
|
*
|
||||||
* Tworzy paczki, pobiera etykiety i wystawia dostępne usługi przewoznicze przez API polkurier.
|
* Slim fasada implementujaca ShipmentProviderInterface. Orkiestruje tworzenie przesylki,
|
||||||
|
* pobranie etykiety i sprawdzenie statusu, delegujac szczegoly do wspolpracownikow:
|
||||||
|
* - katalog uslug przewozniczych -> PolkurierCarrierCatalog,
|
||||||
|
* - montaz payloadu API + rekordu paczki -> PolkurierShipmentPayloadBuilder,
|
||||||
|
* - parsowanie odpowiedzi -> PolkurierResponseParser.
|
||||||
* Payload zgodny z SDK polkurier-sdk (zweryfikowany na Sender/Recipient/Pack/Pickup/COD entity klasach).
|
* Payload zgodny z SDK polkurier-sdk (zweryfikowany na Sender/Recipient/Pack/Pickup/COD entity klasach).
|
||||||
*/
|
*/
|
||||||
final class PolkurierShipmentService implements ShipmentProviderInterface
|
final class PolkurierShipmentService implements ShipmentProviderInterface
|
||||||
{
|
{
|
||||||
/** @var array<int, array<string, mixed>>|null */
|
private readonly PolkurierCarrierCatalog $carrierCatalog;
|
||||||
private ?array $carriersCache = null;
|
private readonly PolkurierShipmentPayloadBuilder $payloadBuilder;
|
||||||
|
private readonly PolkurierResponseParser $responseParser;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PolkurierIntegrationRepository $integrationRepository,
|
private readonly PolkurierIntegrationRepository $integrationRepository,
|
||||||
@@ -29,6 +34,9 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
private readonly CompanySettingsRepository $companySettings,
|
private readonly CompanySettingsRepository $companySettings,
|
||||||
private readonly OrdersRepository $ordersRepository
|
private readonly OrdersRepository $ordersRepository
|
||||||
) {
|
) {
|
||||||
|
$this->carrierCatalog = new PolkurierCarrierCatalog($integrationRepository, $apiClient);
|
||||||
|
$this->payloadBuilder = new PolkurierShipmentPayloadBuilder($companySettings, $this->carrierCatalog);
|
||||||
|
$this->responseParser = new PolkurierResponseParser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function code(): string
|
public function code(): string
|
||||||
@@ -41,48 +49,7 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
*/
|
*/
|
||||||
public function getDeliveryServices(): array
|
public function getDeliveryServices(): array
|
||||||
{
|
{
|
||||||
if ($this->carriersCache !== null) {
|
return $this->carrierCatalog->getDeliveryServices();
|
||||||
return $this->carriersCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
$credentials = $this->integrationRepository->getCredentials();
|
|
||||||
if ($credentials === null) {
|
|
||||||
return $this->carriersCache = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$carriers = $this->apiClient->getAvailableCarriers(
|
|
||||||
$credentials['login'],
|
|
||||||
$credentials['api_token']
|
|
||||||
);
|
|
||||||
} catch (Throwable) {
|
|
||||||
return $this->carriersCache = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalizacja: ujednolicony shape `{id, name, supports_pickup_point, foreign_shipments}`
|
|
||||||
$normalized = [];
|
|
||||||
foreach ($carriers as $carrier) {
|
|
||||||
if (!is_array($carrier)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$code = trim((string) ($carrier['servicecode'] ?? $carrier['code'] ?? ''));
|
|
||||||
$name = trim((string) ($carrier['name'] ?? $code));
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$supportsPoint = $this->detectPickupPointSupport($code, $carrier);
|
|
||||||
$normalized[] = [
|
|
||||||
'id' => $code,
|
|
||||||
'name' => $name !== '' ? $name : $code,
|
|
||||||
'supports_pickup_point' => $supportsPoint,
|
|
||||||
'point_courier' => $supportsPoint ? $this->resolvePointCourierKey($code) : null,
|
|
||||||
'foreign_shipments' => !empty($carrier['foreign_shipments']),
|
|
||||||
'raw' => $carrier,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($normalized, static fn ($a, $b) => strcasecmp((string) $a['name'], (string) $b['name']));
|
|
||||||
return $this->carriersCache = $normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,7 +65,7 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
|
|
||||||
$credentials = $this->requireCredentials();
|
$credentials = $this->requireCredentials();
|
||||||
$sender = $this->companySettings->getSenderAddress();
|
$sender = $this->companySettings->getSenderAddress();
|
||||||
$this->validateSender($sender);
|
$this->payloadBuilder->validateSender($sender);
|
||||||
|
|
||||||
$courierCode = strtoupper(trim((string) (
|
$courierCode = strtoupper(trim((string) (
|
||||||
$formData['service_code']
|
$formData['service_code']
|
||||||
@@ -109,84 +76,32 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
throw new ShipmentException('Nie wybrano usługi polkurier (servicecode).');
|
throw new ShipmentException('Nie wybrano usługi polkurier (servicecode).');
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
|
$request = $this->payloadBuilder->buildShipmentRequest(
|
||||||
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ($orderData['external_order_number'] ?? '')));
|
$order,
|
||||||
$description = 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId);
|
$formData,
|
||||||
|
$sender,
|
||||||
|
$courierCode,
|
||||||
|
$orderId,
|
||||||
|
(string) ($credentials['default_label_format'] ?? 'PDF')
|
||||||
|
);
|
||||||
|
|
||||||
$weightKg = max(0.001, (float) ($formData['weight_kg'] ?? 1.0));
|
$packageId = $this->packages->create($request['package']);
|
||||||
$lengthCm = max(1.0, (float) ($formData['length_cm'] ?? 25.0));
|
$response = $this->sendCreateRequest($credentials, $request['api'], $packageId);
|
||||||
$widthCm = max(1.0, (float) ($formData['width_cm'] ?? 20.0));
|
|
||||||
$heightCm = max(1.0, (float) ($formData['height_cm'] ?? 8.0));
|
|
||||||
$shipmentType = $this->normalizeShipmentType((string) ($formData['package_type'] ?? 'BOX'));
|
|
||||||
|
|
||||||
$receiverPointId = trim((string) ($formData['receiver_point_id'] ?? ''));
|
return $this->persistCreationResult($packageId, $response);
|
||||||
$recipient = $this->buildRecipient($order, $formData, $receiverPointId);
|
}
|
||||||
$senderPayload = $this->buildSenderPayload($sender);
|
|
||||||
|
|
||||||
$packs = [[
|
|
||||||
'length' => (int) round($lengthCm),
|
|
||||||
'width' => (int) round($widthCm),
|
|
||||||
'height' => (int) round($heightCm),
|
|
||||||
'weight' => round($weightKg, 3),
|
|
||||||
'amount' => 1,
|
|
||||||
'type' => $shipmentType,
|
|
||||||
]];
|
|
||||||
|
|
||||||
$pickup = $this->buildPickup($formData);
|
|
||||||
|
|
||||||
$apiPayload = [
|
|
||||||
'shipmenttype' => $shipmentType,
|
|
||||||
'courier' => $courierCode,
|
|
||||||
'description' => $description,
|
|
||||||
'sender' => $senderPayload,
|
|
||||||
'recipient' => $recipient,
|
|
||||||
'packs' => $packs,
|
|
||||||
'pickup' => $pickup,
|
|
||||||
];
|
|
||||||
|
|
||||||
$insurance = max(0.0, (float) ($formData['insurance_amount'] ?? 0));
|
|
||||||
if ($insurance > 0) {
|
|
||||||
$apiPayload['insurance'] = round($insurance, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cod = max(0.0, (float) ($formData['cod_amount'] ?? 0));
|
|
||||||
$codPayload = $this->buildCodPayload($formData);
|
|
||||||
if ($codPayload !== null) {
|
|
||||||
$apiPayload['COD'] = $codPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
$carrierLabel = $this->resolveCarrierLabel($courierCode);
|
|
||||||
$labelFormat = strtoupper(trim((string) ($formData['label_format'] ?? $credentials['default_label_format'] ?? 'PDF')));
|
|
||||||
if (!in_array($labelFormat, ['PDF', 'ZPL', 'EPL'], true)) {
|
|
||||||
$labelFormat = 'PDF';
|
|
||||||
}
|
|
||||||
|
|
||||||
$packageId = $this->packages->create([
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'provider' => 'polkurier',
|
|
||||||
'delivery_method_id' => $courierCode,
|
|
||||||
'credentials_id' => null,
|
|
||||||
'command_id' => null,
|
|
||||||
'status' => 'pending',
|
|
||||||
'carrier_id' => $carrierLabel,
|
|
||||||
'package_type' => $shipmentType,
|
|
||||||
'weight_kg' => $weightKg,
|
|
||||||
'length_cm' => $lengthCm,
|
|
||||||
'width_cm' => $widthCm,
|
|
||||||
'height_cm' => $heightCm,
|
|
||||||
'insurance_amount' => $insurance > 0 ? $insurance : null,
|
|
||||||
'insurance_currency' => 'PLN',
|
|
||||||
'cod_amount' => $cod > 0 ? $cod : null,
|
|
||||||
'cod_currency' => $cod > 0 ? 'PLN' : null,
|
|
||||||
'label_format' => $labelFormat,
|
|
||||||
'receiver_point_id' => $receiverPointId,
|
|
||||||
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
|
||||||
'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
|
||||||
'payload_json' => $apiPayload,
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wywoluje create_order; na bledzie oznacza paczke jako error i przerzuca ShipmentException.
|
||||||
|
*
|
||||||
|
* @param array{login: string, api_token: string, default_label_format: string, integration_id: int} $credentials
|
||||||
|
* @param array<string, mixed> $apiPayload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function sendCreateRequest(array $credentials, array $apiPayload, int $packageId): array
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->apiClient->createShipment(
|
return $this->apiClient->createShipment(
|
||||||
$credentials['login'],
|
$credentials['login'],
|
||||||
$credentials['api_token'],
|
$credentials['api_token'],
|
||||||
$apiPayload
|
$apiPayload
|
||||||
@@ -199,9 +114,18 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
]);
|
]);
|
||||||
throw new ShipmentException($message, 0, $exception);
|
throw new ShipmentException($message, 0, $exception);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$orderno = $this->extractOrderNumber($response);
|
/**
|
||||||
$tracking = $this->extractTrackingNumber($response, $orderno);
|
* Parsuje odpowiedz create_order, aktualizuje rekord paczki i probuje od razu pobrac etykiete.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $response
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function persistCreationResult(int $packageId, array $response): array
|
||||||
|
{
|
||||||
|
$orderno = $this->responseParser->extractOrderNumber($response);
|
||||||
|
$tracking = $this->responseParser->extractTrackingNumber($response, $orderno);
|
||||||
|
|
||||||
if ($orderno === '') {
|
if ($orderno === '') {
|
||||||
// Diagnostyka — polkurier zwrócił odpowiedź ale bez rozpoznawalnego pola order number.
|
// Diagnostyka — polkurier zwrócił odpowiedź ale bez rozpoznawalnego pola order number.
|
||||||
@@ -310,7 +234,7 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
throw new ShipmentException('polkurier get_label: ' . $exception->getMessage(), 0, $exception);
|
throw new ShipmentException('polkurier get_label: ' . $exception->getMessage(), 0, $exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
$base64 = $this->extractLabelBase64($response);
|
$base64 = $this->responseParser->extractLabelBase64($response);
|
||||||
if ($base64 === '') {
|
if ($base64 === '') {
|
||||||
throw new ShipmentException('polkurier nie zwrócił danych etykiety.');
|
throw new ShipmentException('polkurier nie zwrócił danych etykiety.');
|
||||||
}
|
}
|
||||||
@@ -341,106 +265,6 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* polkurier get_label zwraca base64 PDF pod kluczem 'file' (zweryfikowane w SDK GetLabel.php).
|
|
||||||
*/
|
|
||||||
private function extractLabelBase64(mixed $response): string
|
|
||||||
{
|
|
||||||
if (is_string($response)) {
|
|
||||||
return trim($response);
|
|
||||||
}
|
|
||||||
if (is_array($response)) {
|
|
||||||
foreach (['file', 'label', 'pdf', 'data', 'content', 'zpl', 'epl'] as $key) {
|
|
||||||
if (isset($response[$key]) && is_string($response[$key])) {
|
|
||||||
$candidate = trim((string) $response[$key]);
|
|
||||||
if ($candidate !== '') {
|
|
||||||
return $candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isset($response[0]) && is_array($response[0])) {
|
|
||||||
return $this->extractLabelBase64($response[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* polkurier create_order zwraca Order entity. Numer zamówienia jest w polu 'number'
|
|
||||||
* (zmapowańe z setNumber() w SDK Order.php). Fallbacki dla mozliwych wariantow.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $response
|
|
||||||
*/
|
|
||||||
private function extractOrderNumber(array $response): string
|
|
||||||
{
|
|
||||||
// Czasami polkurier opakowuje w {order: {...}} lub zwraca listę
|
|
||||||
if (isset($response['order']) && is_array($response['order'])) {
|
|
||||||
$inner = $this->extractOrderNumber($response['order']);
|
|
||||||
if ($inner !== '') {
|
|
||||||
return $inner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isset($response[0]) && is_array($response[0])) {
|
|
||||||
$inner = $this->extractOrderNumber($response[0]);
|
|
||||||
if ($inner !== '') {
|
|
||||||
return $inner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (['number', 'orderno', 'order_no', 'order_number', 'order_id', 'id'] as $key) {
|
|
||||||
$value = trim((string) ($response[$key] ?? ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waybill polkurier zazwyczaj w `waybills[0].number` (OrderWaybill entity).
|
|
||||||
* Fallbacki dla starszych wariantow odpowiedźi.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $response
|
|
||||||
*/
|
|
||||||
private function extractTrackingNumber(array $response, string $orderno): string
|
|
||||||
{
|
|
||||||
if (isset($response['order']) && is_array($response['order'])) {
|
|
||||||
$inner = $this->extractTrackingNumber($response['order'], $orderno);
|
|
||||||
if ($inner !== '') {
|
|
||||||
return $inner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$waybills = $response['waybills'] ?? $response['waybill'] ?? null;
|
|
||||||
if (is_array($waybills)) {
|
|
||||||
// Lista OrderWaybill
|
|
||||||
if (isset($waybills[0]) && is_array($waybills[0])) {
|
|
||||||
foreach (['number', 'waybillno', 'waybill_number', 'tracking_number'] as $key) {
|
|
||||||
$value = trim((string) ($waybills[0][$key] ?? ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pojedynczy obiekt
|
|
||||||
foreach (['number', 'waybillno', 'waybill_number', 'tracking_number'] as $key) {
|
|
||||||
$value = trim((string) ($waybills[$key] ?? ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (['waybillno', 'waybill_number', 'parcel_number', 'tracking_number'] as $key) {
|
|
||||||
$value = trim((string) ($response[$key] ?? ''));
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $orderno;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveStorageRoot(): string
|
private function resolveStorageRoot(): string
|
||||||
{
|
{
|
||||||
$root = dirname(__DIR__, 3) . '/storage';
|
$root = dirname(__DIR__, 3) . '/storage';
|
||||||
@@ -460,317 +284,16 @@ final class PolkurierShipmentService implements ShipmentProviderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $sender
|
* Forwarder do PolkurierShipmentPayloadBuilder::buildCodPayload.
|
||||||
*/
|
*
|
||||||
private function validateSender(array $sender): void
|
* MUSI pozostac prywatna metoda na tej klasie — PolkurierShipmentServiceTest siega po nia przez
|
||||||
{
|
* ReflectionMethod(PolkurierShipmentService::class, 'buildCodPayload'). Nie usuwac ani nie zmieniac sygnatury.
|
||||||
$required = ['street', 'city', 'postalCode'];
|
*
|
||||||
foreach ($required as $key) {
|
|
||||||
if (trim((string) ($sender[$key] ?? '')) === '') {
|
|
||||||
throw new ShipmentException('Niekompletne dane nadawcy w Ustawieniach firmy (ulica/miasto/kod pocztowy).');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$name = trim((string) ($sender['name'] ?? ''));
|
|
||||||
$company = trim((string) ($sender['company'] ?? ''));
|
|
||||||
if ($name === '' && $company === '') {
|
|
||||||
throw new ShipmentException('Niekompletne dane nadawcy w Ustawieniach firmy (imie/nazwisko lub nazwa firmy).');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $sender
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function buildSenderPayload(array $sender): array
|
|
||||||
{
|
|
||||||
$street = trim((string) ($sender['street'] ?? ''));
|
|
||||||
$parsed = $this->splitStreetAndNumber($street);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'company' => trim((string) ($sender['company'] ?? '')),
|
|
||||||
'person' => trim((string) ($sender['contactPerson'] ?? $sender['name'] ?? '')),
|
|
||||||
'street' => $parsed['street'],
|
|
||||||
'housenumber' => $parsed['house'],
|
|
||||||
'flatnumber' => $parsed['flat'],
|
|
||||||
'postcode' => trim((string) ($sender['postalCode'] ?? '')),
|
|
||||||
'city' => trim((string) ($sender['city'] ?? '')),
|
|
||||||
'email' => trim((string) ($sender['email'] ?? '')),
|
|
||||||
'phone' => $this->normalizePhone((string) ($sender['phone'] ?? '')),
|
|
||||||
'country' => strtoupper(trim((string) ($sender['countryCode'] ?? 'PL'))) ?: 'PL',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $order
|
|
||||||
* @param array<string, mixed> $formData
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function buildRecipient(array $order, array $formData, string $receiverPointId): array
|
|
||||||
{
|
|
||||||
$addresses = is_array($order['addresses'] ?? null) ? $order['addresses'] : [];
|
|
||||||
$delivery = [];
|
|
||||||
$customer = [];
|
|
||||||
foreach ($addresses as $addr) {
|
|
||||||
$type = (string) ($addr['address_type'] ?? '');
|
|
||||||
if ($type === 'delivery') {
|
|
||||||
$delivery = is_array($addr) ? $addr : [];
|
|
||||||
} elseif ($type === 'customer') {
|
|
||||||
$customer = is_array($addr) ? $addr : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_name'] ?? null,
|
|
||||||
$delivery['name'] ?? null,
|
|
||||||
$customer['name'] ?? null,
|
|
||||||
$delivery['company_name'] ?? null,
|
|
||||||
$customer['company_name'] ?? null,
|
|
||||||
'Klient',
|
|
||||||
]);
|
|
||||||
$company = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_company'] ?? null,
|
|
||||||
$delivery['company_name'] ?? null,
|
|
||||||
$customer['company_name'] ?? null,
|
|
||||||
]);
|
|
||||||
$streetLine = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_street'] ?? null,
|
|
||||||
$this->composeStreet($delivery),
|
|
||||||
$this->composeStreet($customer),
|
|
||||||
]);
|
|
||||||
$parsed = $this->splitStreetAndNumber($streetLine);
|
|
||||||
$postcode = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_postal_code'] ?? null,
|
|
||||||
$delivery['zip_code'] ?? null,
|
|
||||||
$customer['zip_code'] ?? null,
|
|
||||||
]);
|
|
||||||
$city = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_city'] ?? null,
|
|
||||||
$delivery['city'] ?? null,
|
|
||||||
$customer['city'] ?? null,
|
|
||||||
]);
|
|
||||||
$country = strtoupper($this->firstNonEmpty([
|
|
||||||
$formData['receiver_country_code'] ?? null,
|
|
||||||
$delivery['country'] ?? null,
|
|
||||||
$customer['country'] ?? null,
|
|
||||||
'PL',
|
|
||||||
]));
|
|
||||||
$phone = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_phone'] ?? null,
|
|
||||||
$delivery['phone'] ?? null,
|
|
||||||
$customer['phone'] ?? null,
|
|
||||||
]);
|
|
||||||
$email = $this->firstNonEmpty([
|
|
||||||
$formData['receiver_email'] ?? null,
|
|
||||||
$delivery['email'] ?? null,
|
|
||||||
$customer['email'] ?? null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($name === '' || $postcode === '' || $city === '') {
|
|
||||||
throw new ShipmentException('Brak wymaganych danych adresowych odbiorcy (imie/kod pocztowy/miasto).');
|
|
||||||
}
|
|
||||||
if ($receiverPointId === '' && $parsed['street'] === '') {
|
|
||||||
throw new ShipmentException('Brak ulicy odbiorcy (wymagana dla usług kurierskich).');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'company' => $company,
|
|
||||||
'person' => $name,
|
|
||||||
'street' => $parsed['street'],
|
|
||||||
'housenumber' => $parsed['house'],
|
|
||||||
'flatnumber' => $parsed['flat'],
|
|
||||||
'postcode' => $postcode,
|
|
||||||
'city' => $city,
|
|
||||||
'email' => $email,
|
|
||||||
'phone' => $this->normalizePhone($phone),
|
|
||||||
'country' => $country !== '' ? $country : 'PL',
|
|
||||||
'point_id' => $receiverPointId,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $formData
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function buildPickup(array $formData): array
|
|
||||||
{
|
|
||||||
$date = trim((string) ($formData['pickup_date'] ?? ''));
|
|
||||||
if ($date === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
|
|
||||||
$date = $this->nextBusinessDay();
|
|
||||||
}
|
|
||||||
$from = trim((string) ($formData['pickup_time_from'] ?? '10:00'));
|
|
||||||
$to = trim((string) ($formData['pickup_time_to'] ?? '16:00'));
|
|
||||||
$noCourierOrder = !empty($formData['no_courier_order']);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'pickupdate' => $date,
|
|
||||||
'pickuptimefrom' => $from,
|
|
||||||
'pickuptimeto' => $to,
|
|
||||||
'nocourierorder' => $noCourierOrder,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $formData
|
* @param array<string, mixed> $formData
|
||||||
* @return array{codtype: string, codamount: float, codbankaccount: string, return_cod: string}|null
|
* @return array{codtype: string, codamount: float, codbankaccount: string, return_cod: string}|null
|
||||||
*/
|
*/
|
||||||
private function buildCodPayload(array $formData): ?array
|
private function buildCodPayload(array $formData): ?array
|
||||||
{
|
{
|
||||||
$cod = max(0.0, (float) ($formData['cod_amount'] ?? 0));
|
return $this->payloadBuilder->buildCodPayload($formData);
|
||||||
if ($cod <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$companySettings = $this->companySettings->getSettings();
|
|
||||||
$bankAccount = preg_replace('/[^0-9]/', '', (string) ($companySettings['bank_account'] ?? '')) ?? '';
|
|
||||||
if ($bankAccount === '') {
|
|
||||||
throw new ShipmentException('Przesyłka COD wymaga numeru konta bankowego. Uzupelnij go w Ustawienia > Firma.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'codtype' => 'S',
|
|
||||||
'codamount' => round($cod, 2),
|
|
||||||
'codbankaccount' => $bankAccount,
|
|
||||||
'return_cod' => 'BA',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function nextBusinessDay(): string
|
|
||||||
{
|
|
||||||
$ts = time();
|
|
||||||
do {
|
|
||||||
$ts = strtotime('+1 day', $ts);
|
|
||||||
$dow = (int) date('N', $ts ?: time());
|
|
||||||
} while ($dow >= 6);
|
|
||||||
return date('Y-m-d', $ts ?: time());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{street: string, house: string, flat: string}
|
|
||||||
*/
|
|
||||||
private function splitStreetAndNumber(string $streetLine): array
|
|
||||||
{
|
|
||||||
$street = trim($streetLine);
|
|
||||||
if ($street === '') {
|
|
||||||
return ['street' => '', 'house' => '', 'flat' => ''];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wzorce: "Marszalkowska 10/5", "Marszalkowska 10 m. 5", "Marszalkowska 10A"
|
|
||||||
if (preg_match('/^(.*?)\s+(\d+[A-Za-z]?)(?:\s*[\/\-]\s*|\s*m\.?\s*)?(\d+[A-Za-z]?)?$/u', $street, $matches) === 1) {
|
|
||||||
return [
|
|
||||||
'street' => trim((string) $matches[1]),
|
|
||||||
'house' => trim((string) ($matches[2] ?? '')),
|
|
||||||
'flat' => trim((string) ($matches[3] ?? '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['street' => $street, 'house' => '', 'flat' => ''];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $address
|
|
||||||
*/
|
|
||||||
private function composeStreet(array $address): string
|
|
||||||
{
|
|
||||||
$street = trim((string) ($address['street_name'] ?? ''));
|
|
||||||
$number = trim((string) ($address['street_number'] ?? ''));
|
|
||||||
if ($street === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return $number !== '' ? trim($street . ' ' . $number) : $street;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<int, mixed> $candidates
|
|
||||||
*/
|
|
||||||
private function firstNonEmpty(array $candidates): string
|
|
||||||
{
|
|
||||||
foreach ($candidates as $candidate) {
|
|
||||||
$value = trim((string) $candidate);
|
|
||||||
if ($value !== '') {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizePhone(string $phone): string
|
|
||||||
{
|
|
||||||
$digits = preg_replace('/\D+/', '', $phone) ?? '';
|
|
||||||
if ($digits === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
// Polkurier akceptuje cyfry. Usuń prefiks 48 jezeli jest podwojny.
|
|
||||||
if (str_starts_with($digits, '48') && strlen($digits) === 11) {
|
|
||||||
$digits = substr($digits, 2);
|
|
||||||
}
|
|
||||||
return $digits;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $carrier
|
|
||||||
*/
|
|
||||||
private function detectPickupPointSupport(string $code, array $carrier): bool
|
|
||||||
{
|
|
||||||
$haystack = strtolower($code . ' ' . trim((string) ($carrier['name'] ?? '')));
|
|
||||||
return str_contains($haystack, 'paczkomat')
|
|
||||||
|| str_contains($haystack, 'parcel')
|
|
||||||
|| str_contains($haystack, 'inpost')
|
|
||||||
|| str_contains($haystack, 'orlen')
|
|
||||||
|| str_contains($haystack, 'pocztex')
|
|
||||||
|| str_contains($haystack, 'kurier48')
|
|
||||||
|| str_contains($haystack, 'punkt');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolvePointCourierKey(string $code): ?string
|
|
||||||
{
|
|
||||||
$lower = strtolower($code);
|
|
||||||
if (str_contains($lower, 'inpost') || str_contains($lower, 'paczkomat')) {
|
|
||||||
return 'inpost';
|
|
||||||
}
|
|
||||||
if (str_contains($lower, 'orlen')) {
|
|
||||||
return 'orlen';
|
|
||||||
}
|
|
||||||
if (str_contains($lower, 'pocztex')) {
|
|
||||||
return 'pocztex';
|
|
||||||
}
|
|
||||||
if (str_contains($lower, 'kurier48')) {
|
|
||||||
return 'kurier48';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* polkurier API wymaga lowercase z dozwolonego zbioru:
|
|
||||||
* [box, envelope, palette, small_parcel, parcel_size_20].
|
|
||||||
* Mapuje istniejące orderPRO wartości (PACKAGE/BOX/ENVELOPE/...) na format polkurier.
|
|
||||||
*/
|
|
||||||
private function normalizeShipmentType(string $input): string
|
|
||||||
{
|
|
||||||
$raw = strtolower(trim($input));
|
|
||||||
$allowed = ['box', 'envelope', 'palette', 'small_parcel', 'parcel_size_20'];
|
|
||||||
if (in_array($raw, $allowed, true)) {
|
|
||||||
return $raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
$aliases = [
|
|
||||||
'package' => 'box',
|
|
||||||
'parcel' => 'box',
|
|
||||||
'paczka' => 'box',
|
|
||||||
'koperta' => 'envelope',
|
|
||||||
'paleta' => 'palette',
|
|
||||||
'mala_paczka' => 'small_parcel',
|
|
||||||
'small' => 'small_parcel',
|
|
||||||
];
|
|
||||||
return $aliases[$raw] ?? 'box';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveCarrierLabel(string $courierCode): string
|
|
||||||
{
|
|
||||||
foreach ($this->getDeliveryServices() as $service) {
|
|
||||||
if (strcasecmp((string) ($service['id'] ?? ''), $courierCode) === 0) {
|
|
||||||
return (string) ($service['name'] ?? $courierCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $courierCode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user