From 176d74057828ec867a384152788b31cc7aed7754 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 28 Mar 2026 15:32:34 +0100 Subject: [PATCH] feat(50-allegro-shipment-waybill-push): push waybill to allegro checkout form Phase 50 complete: - add conditional waybill push for allegro orders only - add retry on ALLEGRO_HTTP_401 and non-critical failure handling - add unit tests and update architecture/changelog docs --- .paul/PROJECT.md | 6 +- .paul/ROADMAP.md | 15 +- .paul/STATE.md | 37 +- .../50-01-PLAN.md | 177 ++++++++++ .../50-01-SUMMARY.md | 36 ++ DOCS/ARCHITECTURE.md | 3 + DOCS/TECH_CHANGELOG.md | 12 + .../Shipments/AllegroShipmentService.php | 118 ++++++- tests/Unit/AllegroShipmentServiceTest.php | 322 ++++++++++++++++++ 9 files changed, 696 insertions(+), 30 deletions(-) create mode 100644 .paul/phases/50-allegro-shipment-waybill-push/50-01-PLAN.md create mode 100644 .paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md create mode 100644 tests/Unit/AllegroShipmentServiceTest.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 55475a7..594b3fe 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,7 +13,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| | Version | 1.0.0 | -| Status | v2.1 Complete | +| Status | v2.2 Complete | | Last Updated | 2026-03-28 | ## Requirements @@ -58,6 +58,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów - [x] Automatyzacja przesylek: natychmiastowy event `shipment.created` + akcja `update_shipment_status` - Phase 47 - [x] Szablony e-mail: zmienne `przesylka.numer` i `przesylka.link_sledzenia` z provider-aware linkiem sledzenia - Phase 48 - [x] Automatyzacja: tab Historia z filtrowaniem/paginacja + retencja 30 dni + akcja update_order_status - Phase 49 +- [x] Allegro: automatyczne przekazywanie numeru przesylki do checkout form po utworzeniu paczki (tylko source=allegro) - Phase 50 ### Active (In Progress) @@ -127,6 +128,7 @@ PHP (XAMPP/Laravel), integracje z API marketplace'ów (Allegro, Erli) oraz API | Zmienne e-mail przesylki bazuja na najnowszej paczce `shipment_packages` i `DeliveryStatus::trackingUrl` | Jeden spojny kontrakt dla numeru i linku sledzenia w szablonach | 2026-03-28 | Active | | Historia automatyzacji zapisywana per regula (success/failed) i czyszczona cronem po 30 dniach | Audyt wykonywania regul bez recznego utrzymania danych | 2026-03-28 | Active | | Akcja update_order_status korzysta z OrdersRepository::updateOrderStatus | Spojnosc z historia statusow i activity log bez duplikowania logiki | 2026-03-28 | Active | +| Push waybilla do Allegro checkout forms wykonywany tylko dla zamowien source=allegro i jest niekrytyczny dla lokalnego tworzenia paczki | Eliminacja recznego kroku po stronie Allegro bez ryzyka utraty lokalnie utworzonej przesylki przy bledzie API | 2026-03-28 | Active | ## Success Metrics @@ -158,6 +160,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-03-28 after Phase 49 completion (Automation History Tab)* +*Last updated: 2026-03-28 after Phase 50 completion (Allegro Shipment Waybill Push)* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 87998b2..a9c8b47 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -12,6 +12,19 @@ Next action: uruchom $paul-milestone (lub $paul-plan) dla kolejnego celu bizneso ## Completed Milestones +
+v2.2 Allegro Shipment Waybill Push - 2026-03-28 (1 phase, 1 plan) + +Automatyczne przekazywanie waybilla do Allegro checkout forms przy tworzeniu przesylki, ograniczone do zamowien `source=allegro` i odporne na bledy API Allegro. + +| Phase | Name | Plans | Completed | +|-------|------|-------|-----------| +| 50 | Allegro Shipment Waybill Push | 1/1 | 2026-03-28 | + +Archive: `.paul/phases/50-allegro-shipment-waybill-push/` + +
+
v2.1 Automation History & Observability - 2026-03-28 (1 phase, 1 plan) @@ -302,7 +315,7 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-03-28 - v2.1 completed (phase 49)* +*Last updated: 2026-03-28 - v2.2 completed (phase 50)* diff --git a/.paul/STATE.md b/.paul/STATE.md index ff92b50..dedf87f 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -1,23 +1,24 @@ -# Project State +# Project State ## Project Reference See: .paul/PROJECT.md (updated 2026-03-28) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** Milestone v2.1 completed; ready for next milestone planning +**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. +**Current focus:** Milestone v2.2 completed; ready for next PLAN / next milestone ## Current Position -Milestone: v2.1 Automation History & Observability - Complete -Phase: 1 of 1 (49 - Automation History Tab) - Complete -Plan: 49-01 complete -Status: Ready for next PLAN / next milestone -Last activity: 2026-03-28 14:47:06 - UNIFY closed for 49-01, SUMMARY created +Milestone: v2.2 Allegro Shipment Waybill Push - Complete +Phase: 1 of 1 (50 - Allegro Shipment Waybill Push) - Complete +Plan: 50-01 complete +Status: Loop complete - ready for next PLAN +Last activity: 2026-03-28 15:33:00 - UNIFY closed for 50-01, SUMMARY created Progress: - Milestone: [##########] 100% -- Phase 49: [##########] 100% +- Phase 50: [##########] 100% ## Loop Position @@ -29,25 +30,19 @@ PLAN --> APPLY --> UNIFY ## Session Continuity -Last session: 2026-03-28 14:47:06 -Stopped at: Phase 49 complete, milestone v2.1 complete -Next action: Uruchom `$paul-milestone` (lub `$paul-plan`) dla kolejnego celu -Resume file: .paul/ROADMAP.md +Last session: 2026-03-28 15:33:00 +Stopped at: Phase 50 complete, milestone v2.2 complete +Next action: Uruchom $paul-milestone (lub $paul-plan) dla kolejnego celu +Resume file: .paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md ## Accumulated Context ### Decisions | Date | Decision | Impact | |------|----------|--------| -| 2026-03-28 | Rozdzielenie `/settings/automation` na taby `Ustawienia` i `Historia` | Lepsza czytelnosc i oddzielenie konfiguracji od audytu wykonania | -| 2026-03-28 | Historia automatyzacji zapisywana per regula (success/failed) + filtry/paginacja | Szybsza diagnostyka triggerow i wynikow regul | -| 2026-03-28 | Retencja historii starszej niz 30 dni przez cron `automation_history_cleanup` | Kontrola rozmiaru danych i automatyczne porzadkowanie | -| 2026-03-28 | Akcja `update_order_status` przez `OrdersRepository::updateOrderStatus` | Spojnosc z historia statusow i activity logiem | - -### Skill Audit Carry-Over -| Expected | Invoked | Notes | -|----------|---------|-------| -| sonar-scanner | yes | Uruchomiony po APPLY, analiza zakonczona sukcesem | +| 2026-03-28 | Push waybilla do Allegro wykonywany tylko dla `orders.source='allegro'` i `source_order_id` | Brak falszywych pushy dla innych integracji | +| 2026-03-28 | Blad pushu waybilla do Allegro jest niekrytyczny dla tworzenia paczki | Lokalna przesylka nie ginie przy problemie API Allegro | +| 2026-03-28 | Retry pushu po `ALLEGRO_HTTP_401` przez ponowne `resolveToken()` | Wyzsza odpornosc na wygasle tokeny | ## Git State diff --git a/.paul/phases/50-allegro-shipment-waybill-push/50-01-PLAN.md b/.paul/phases/50-allegro-shipment-waybill-push/50-01-PLAN.md new file mode 100644 index 0000000..af899be --- /dev/null +++ b/.paul/phases/50-allegro-shipment-waybill-push/50-01-PLAN.md @@ -0,0 +1,177 @@ +--- +phase: 50-allegro-shipment-waybill-push +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Shipments/AllegroShipmentService.php + - tests/Unit/AllegroShipmentServiceTest.php + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +autonomous: true +--- + + +## Goal +Dodac automatyczne przekazywanie numeru przesylki do Allegro w momencie skutecznego wygenerowania przesylki, ale tylko dla zamowien zrodla `allegro`. + +## Purpose +Operator nie powinien recznie dopisywac numeru przesylki w Allegro po utworzeniu paczki w orderPRO, bo to spowalnia obsluge i zwieksza ryzyko pomylek. + +## Output +Rozszerzony flow `AllegroShipmentService` o push waybilla do endpointu checkout form shipments Allegro (z ochrona na przypadki spoza Allegro), testy jednostkowe nowej logiki oraz aktualizacja dokumentacji technicznej. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@DOCS/DB_SCHEMA.md +@DOCS/ARCHITECTURE.md + +## Prior Work (only if genuinely needed) +@.paul/phases/46-allegro-status-push/46-01-SUMMARY.md +@.paul/phases/47-shipment-created-automation/47-01-SUMMARY.md + +## Source Files +@src/Modules/Shipments/AllegroShipmentService.php +@src/Modules/Settings/AllegroApiClient.php +@src/Modules/Orders/OrdersRepository.php +@src/Modules/Shipments/ShipmentPackageRepository.php +@tests/Unit/AllegroStatusSyncServiceTest.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| `sonar-scanner` | required | Po APPLY, przed UNIFY | o | +| /feature-dev | optional | Przed wdrazaniem nowej funkcjonalnosci integracyjnej | o | +| /code-review | optional | Po APPLY, przed UNIFY | o | + +**BLOCKING:** Required skills MUST be loaded before APPLY proceeds. + +## Skill Invocation Checklist +- [ ] `sonar-scanner` uruchomiony po APPLY +- [ ] /feature-dev (opcjonalnie) +- [ ] /code-review (opcjonalnie) + + + + + +## AC-1: Numer przesylki jest wysylany do Allegro dla zamowienia Allegro +```gherkin +Given zamowienie ma `source = allegro` i posiada `source_order_id` +When przesylka Allegro WZA zostanie utworzona i ma tracking number +Then system wywola API Allegro `POST /order/checkout-forms/{id}/shipments` +And do Allegro trafi waybill oraz dane przewoznika +``` + +## AC-2: Zamowienia spoza Allegro nie wykonuja pushu do Allegro +```gherkin +Given zamowienie ma inne zrodlo niz `allegro` (np. shoppro, erli) +When przesylka zostanie wygenerowana +Then system nie wywola endpointu pushu shipment do Allegro +And pozostale elementy flow tworzenia przesylki dzialaja bez zmian +``` + +## AC-3: Blad pushu numeru do Allegro nie blokuje lokalnego procesu +```gherkin +Given przesylka lokalnie zostala utworzona i ma tracking number +When wywolanie endpointu Allegro zwroci blad lub wyjatek +Then rekord paczki w orderPRO pozostaje poprawnie zapisany +And uzytkownik nie traci utworzonej przesylki z powodu bledu pushu +``` + +## AC-4: Dokumentacja odzwierciedla nowy kontrakt +```gherkin +Given wdrozono push tracking number do Allegro checkout form shipments +When zespol czyta dokumentacje techniczna +Then `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md` opisuja nowy moment i warunki pushu +``` + + + + + + + Task 1: Rozszerz AllegroShipmentService o push waybill do checkout form shipments + src/Modules/Shipments/AllegroShipmentService.php + + Dodaj wewnetrzny flow, ktory po skutecznym utworzeniu przesylki i uzyskaniu tracking number wysyla numer do Allegro przez istniejace `AllegroApiClient::addShipmentToOrder(...)`. + Logike uruchamiaj tylko dla zamowien `source = allegro` oraz niepustego `source_order_id`. + Nie zmieniaj zachowania dla innych zrodel zamowien i providerow. + Blad pushu do Allegro traktuj jako niekrytyczny dla lokalnego lifecycle paczki (zapis paczki pozostaje sukcesem). + Utrzymaj retry po `ALLEGRO_HTTP_401` zgodnie z obecnym wzorcem token managera. + + rg -n "addShipmentToOrder|source_order_id|source = 'allegro'|ALLEGRO_HTTP_401" src/Modules/Shipments/AllegroShipmentService.php + AC-1 satisfied, AC-2 satisfied i AC-3 satisfied: push dziala tylko dla Allegro i nie psuje lokalnego flow przy bledzie API. + + + + Task 2: Dodaj testy jednostkowe dla scenariuszy pushu tracking number + tests/Unit/AllegroShipmentServiceTest.php + + Dodaj testy obejmujace co najmniej: + - wywolanie pushu dla zamowienia Allegro z tracking number, + - brak wywolania pushu dla zamowienia nie-Allegro, + - odporne zachowanie przy bledzie pushu (przesylka lokalnie pozostaje utworzona), + - retry po `ALLEGRO_HTTP_401`. + Mockuj zaleznosci zgodnie z obecnym stylem testow (`PHPUnit` + `dg/bypass-finals`). + + C:/xampp/php/php.exe vendor/bin/phpunit --filter AllegroShipmentServiceTest --testdox + AC-1 satisfied, AC-2 satisfied i AC-3 satisfied: testy potwierdzaja warunki i odpornosc implementacji. + + + + Task 3: Zaktualizuj dokumentacje techniczna + DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md + + Opisz nowy krok przeplywu tworzenia przesylki Allegro: automatyczne przekazanie waybilla do checkout form shipment po uzyskaniu tracking number. + Dodaj informacje o warunku ograniczajacym zakres do zamowien zrodla Allegro i o fallbacku na niekrytyczny blad pushu. + Potwierdz brak zmian w schemacie bazy. + + rg -n "checkout-forms/.*/shipments|waybill|source = allegro|tracking number" DOCS/ARCHITECTURE.md DOCS/TECH_CHANGELOG.md + AC-4 satisfied: dokumentacja odzwierciedla nowa logike i ograniczenia zakresu. + + + + + + +## DO NOT CHANGE +- `database/migrations/*` (brak zmian schematu DB) +- Flow tworzenia przesylek dla providerow innych niz `allegro_wza` +- Logika status sync orderPRO -> Allegro z fazy 46 (to osobny mechanizm) + +## SCOPE LIMITS +- Zakres obejmuje tylko push numeru przesylki do Allegro przy generowaniu przesylki. +- Bez zmian UI i bez nowych formularzy. +- Bez dodawania nowego cron joba. + + + + +Before declaring plan complete: +- [ ] `C:\xampp\php\php.exe -l src/Modules/Shipments/AllegroShipmentService.php` +- [ ] `C:\xampp\php\php.exe -l tests/Unit/AllegroShipmentServiceTest.php` +- [ ] `C:\xampp\php\php.exe vendor/bin/phpunit --filter AllegroShipmentServiceTest --testdox` +- [ ] Brak regresji lokalnego flow tworzenia paczki dla zamowien nie-Allegro +- [ ] Dokumentacja zaktualizowana (`DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`) +- [ ] All acceptance criteria met + + + +- Po wygenerowaniu przesylki dla zamowienia Allegro system automatycznie wysyla numer przesylki do Allegro. +- Dla zamowien spoza Allegro nie ma wywolan endpointu Allegro shipment push. +- Ewentualny blad pushu nie anuluje lokalnie utworzonej przesylki. +- Dokumentacja opisuje nowy kontrakt i brak zmian DB. + + + +After completion, create `.paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md` + diff --git a/.paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md b/.paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md new file mode 100644 index 0000000..4e2771e --- /dev/null +++ b/.paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md @@ -0,0 +1,36 @@ +--- +phase: 50-allegro-shipment-waybill-push +plan: 01 +status: completed +completed: 2026-03-28 +--- + +# Phase 50 Plan 01 Summary + +## Result +- Dodano automatyczny push numeru przesylki do Allegro (`POST /order/checkout-forms/{id}/shipments`) po uzyskaniu `tracking_number`. +- Push wykonuje sie tylko dla zamowien Allegro (`orders.source='allegro'`) z niepustym `source_order_id`. +- Blad pushu jest niekrytyczny - lokalna paczka pozostaje utworzona. +- Dodano retry pushu po `ALLEGRO_HTTP_401` z odswiezeniem tokenu. + +## Acceptance Criteria +- AC-1: Pass +- AC-2: Pass +- AC-3: Pass +- AC-4: Pass + +## Verification +- `C:\xampp\php\php.exe -l src/Modules/Shipments/AllegroShipmentService.php` PASS +- `C:\xampp\php\php.exe -l tests/Unit/AllegroShipmentServiceTest.php` PASS +- `C:\xampp\php\php.exe vendor/bin/phpunit --filter AllegroShipmentServiceTest --testdox` PASS (4 tests, 54 assertions) +- `sonar-scanner` PASS (analysis successful): https://sonar.project-pro.pl/dashboard?id=orderPRO + +## Files +- `src/Modules/Shipments/AllegroShipmentService.php` +- `tests/Unit/AllegroShipmentServiceTest.php` +- `DOCS/ARCHITECTURE.md` +- `DOCS/TECH_CHANGELOG.md` + +## Notes +- Brak zmian schematu DB. +- Brak checkpointow manualnych (plan autonomiczny). diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index bf9a5f2..794e864 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -341,6 +341,9 @@ - `GET /orders/{id}/shipment/{packageId}/status`: - `ShipmentController::checkStatus(Request): Response`, - wybiera providera po `shipment_packages.provider` i deleguje `checkCreationStatus(...)`. + - dla providera `allegro_wza` po uzyskaniu `tracking_number` serwis probuje przekazac waybill do Allegro przez `POST /order/checkout-forms/{id}/shipments` (`AllegroApiClient::addShipmentToOrder(...)`), + - push waybilla jest wykonywany tylko dla zamowien `orders.source='allegro'` i niepustego `orders.source_order_id`, + - blad pushu waybilla do Allegro nie przerywa lokalnego flow tworzenia paczki (fallback niekrytyczny, paczka zostaje zapisana w orderPRO). - `POST /orders/{id}/shipment/{packageId}/label`: - `ShipmentController::label(Request): Response`, - wybiera providera po `shipment_packages.provider` i deleguje `downloadLabel(...)`, diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index 3cb4fe6..9e7aa86 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,17 @@ # Tech Changelog +## 2026-03-28 (Phase 50 - Allegro Shipment Waybill Push, Plan 01) +- `AllegroShipmentService`: + - po sukcesie `checkCreationStatus(...)` (gdy jest `tracking_number`) probuje dopiac przesylke do checkout form Allegro, + - wykorzystuje `AllegroApiClient::addShipmentToOrder(...)` (`POST /order/checkout-forms/{id}/shipments`), + - push wykonywany tylko dla zamowien `orders.source='allegro'` i niepustego `source_order_id`, + - retry pushu po `ALLEGRO_HTTP_401` z ponownym `tokenManager->resolveToken()`, + - bledy pushu traktowane jako niekrytyczne (lokalna paczka pozostaje utworzona). +- `AllegroShipmentService::downloadLabel(...)`: + - przy fallbackowym dopelnieniu trackingu (gdy brak numeru w rekordzie paczki) wykonuje ten sam warunkowy push waybilla do Allegro. +- Testy: + - dodano `tests/Unit/AllegroShipmentServiceTest.php` (scenariusze: push dla Allegro, brak pushu dla nie-Allegro, fallback przy bledzie API, retry po 401). + ## 2026-03-28 (Public HTTPS cron endpoint) - Dodano publiczny endpoint triggera crona: - `GET /cron?token=` diff --git a/src/Modules/Shipments/AllegroShipmentService.php b/src/Modules/Shipments/AllegroShipmentService.php index a573f7c..c0c5d1a 100644 --- a/src/Modules/Shipments/AllegroShipmentService.php +++ b/src/Modules/Shipments/AllegroShipmentService.php @@ -3,13 +3,13 @@ declare(strict_types=1); namespace App\Modules\Shipments; +use App\Core\Exceptions\IntegrationConfigException; +use App\Core\Exceptions\ShipmentException; use App\Modules\Orders\OrdersRepository; use App\Modules\Settings\AllegroApiClient; use App\Modules\Settings\AllegroTokenManager; use App\Modules\Settings\CompanySettingsRepository; use RuntimeException; -use App\Core\Exceptions\IntegrationConfigException; -use App\Core\Exceptions\ShipmentException; use Throwable; final class AllegroShipmentService implements ShipmentProviderInterface @@ -35,6 +35,7 @@ final class AllegroShipmentService implements ShipmentProviderInterface { [$accessToken, $env] = $this->tokenManager->resolveToken(); $response = $this->apiClient->getDeliveryServices($env, $accessToken); + return is_array($response['services'] ?? null) ? $response['services'] : []; } @@ -107,7 +108,7 @@ final class AllegroShipmentService implements ShipmentProviderInterface $codAmount = (float) ($formData['cod_amount'] ?? 0); if ($codAmount > 0) { - // Allegro WZA manages COD funds internally – iban/ownerName are not accepted + // Allegro WZA manages COD funds internally - iban/ownerName are not accepted. $apiPayload['input']['cashOnDelivery'] = [ 'amount' => number_format($codAmount, 2, '.', ''), 'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))), @@ -196,14 +197,27 @@ final class AllegroShipmentService implements ShipmentProviderInterface if ($status === 'SUCCESS' && $shipmentId !== '') { $details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId); $trackingNumber = trim((string) ($details['waybill'] ?? '')); + $carrierId = trim((string) ($package['carrier_id'] ?? '')); + if ($carrierId === '') { + $carrierId = trim((string) ($details['carrierId'] ?? '')); + } $this->packages->update($packageId, [ 'status' => 'created', 'shipment_id' => $shipmentId, 'tracking_number' => $trackingNumber !== '' ? $trackingNumber : null, + 'carrier_id' => $carrierId !== '' ? $carrierId : ($package['carrier_id'] ?? null), 'payload_json' => $details, ]); + $this->pushWaybillToAllegroCheckoutForm( + (int) ($package['order_id'] ?? 0), + $trackingNumber, + $carrierId, + $accessToken, + $env + ); + return [ 'status' => 'created', 'shipment_id' => $shipmentId, @@ -268,16 +282,32 @@ final class AllegroShipmentService implements ShipmentProviderInterface 'label_path' => 'labels/' . $filename, ]; - // Refresh tracking number if not yet saved (may not have been available at creation time) + // Refresh tracking number if not yet saved (may not have been available at creation time). if (trim((string) ($package['tracking_number'] ?? '')) === '') { try { $details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId); $trackingNumber = trim((string) ($details['waybill'] ?? '')); + $carrierId = trim((string) ($package['carrier_id'] ?? '')); + if ($carrierId === '') { + $carrierId = trim((string) ($details['carrierId'] ?? '')); + } + if ($trackingNumber !== '') { $updateFields['tracking_number'] = $trackingNumber; + if ($carrierId !== '') { + $updateFields['carrier_id'] = $carrierId; + } + + $this->pushWaybillToAllegroCheckoutForm( + (int) ($package['order_id'] ?? 0), + $trackingNumber, + $carrierId, + $accessToken, + $env + ); } } catch (Throwable) { - // non-critical – label is still saved + // non-critical - label is still saved } } @@ -359,11 +389,87 @@ final class AllegroShipmentService implements ShipmentProviderInterface } } + private function pushWaybillToAllegroCheckoutForm( + int $orderId, + string $trackingNumber, + string $carrierId, + string $accessToken, + string $environment + ): void { + if ($orderId <= 0) { + return; + } + + $waybill = trim($trackingNumber); + if ($waybill === '') { + return; + } + + $orderDetails = $this->ordersRepository->findDetails($orderId); + if ($orderDetails === null) { + return; + } + + $order = is_array($orderDetails['order'] ?? null) ? $orderDetails['order'] : []; + $source = strtolower(trim((string) ($order['source'] ?? ''))); + if ($source !== 'allegro') { + return; + } + + $checkoutFormId = trim((string) ($order['source_order_id'] ?? '')); + if ($checkoutFormId === '') { + return; + } + + $normalizedCarrierId = trim($carrierId); + if ($normalizedCarrierId === '') { + return; + } + + $carrierName = (string) ($this->packages->resolveCarrierName('allegro_wza', $normalizedCarrierId) ?? ''); + $carrierName = trim($carrierName); + if ($carrierName === '') { + $carrierName = $normalizedCarrierId; + } + + try { + $this->apiClient->addShipmentToOrder( + $environment, + $accessToken, + $checkoutFormId, + $waybill, + $normalizedCarrierId, + $carrierName + ); + } catch (RuntimeException $exception) { + if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') { + return; + } + + try { + [$refreshedToken, $refreshedEnvironment] = $this->tokenManager->resolveToken(); + $this->apiClient->addShipmentToOrder( + $refreshedEnvironment, + $refreshedToken, + $checkoutFormId, + $waybill, + $normalizedCarrierId, + $carrierName + ); + } catch (Throwable) { + // non-critical - local shipment remains created + } + } catch (Throwable) { + // non-critical - local shipment remains created + } + } + private function generateUuid(): string { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } -} +} \ No newline at end of file diff --git a/tests/Unit/AllegroShipmentServiceTest.php b/tests/Unit/AllegroShipmentServiceTest.php new file mode 100644 index 0000000..0554595 --- /dev/null +++ b/tests/Unit/AllegroShipmentServiceTest.php @@ -0,0 +1,322 @@ +tokenManager = $this->createMock(AllegroTokenManager::class); + $this->apiClient = $this->createMock(AllegroApiClient::class); + $this->packages = $this->createMock(ShipmentPackageRepository::class); + $this->companySettings = $this->createMock(CompanySettingsRepository::class); + $this->ordersRepository = $this->createMock(OrdersRepository::class); + } + + public function testCheckCreationStatusPushesWaybillForAllegroOrder(): void + { + $this->tokenManager + ->method('resolveToken') + ->willReturn(['token-1', 'sandbox']); + + $this->packages + ->method('findById') + ->with(11) + ->willReturn([ + 'id' => 11, + 'order_id' => 200, + 'command_id' => 'cmd-11', + 'carrier_id' => 'INPOST', + ]); + + $this->packages + ->expects($this->once()) + ->method('update') + ->with( + 11, + $this->callback(static function (array $payload): bool { + return ($payload['tracking_number'] ?? null) === 'TRK-001' + && ($payload['carrier_id'] ?? null) === 'INPOST'; + }) + ); + + $this->packages + ->expects($this->once()) + ->method('resolveCarrierName') + ->with('allegro_wza', 'INPOST') + ->willReturn('InPost'); + + $this->ordersRepository + ->method('findDetails') + ->with(200) + ->willReturn([ + 'order' => [ + 'source' => 'allegro', + 'source_order_id' => 'CHECKOUT-200', + ], + ]); + + $this->apiClient + ->method('getShipmentCreationStatus') + ->with('sandbox', 'token-1', 'cmd-11') + ->willReturn([ + 'status' => 'SUCCESS', + 'shipmentId' => 'SHIP-11', + ]); + + $this->apiClient + ->method('getShipmentDetails') + ->with('sandbox', 'token-1', 'SHIP-11') + ->willReturn([ + 'waybill' => 'TRK-001', + ]); + + $this->apiClient + ->expects($this->once()) + ->method('addShipmentToOrder') + ->with( + 'sandbox', + 'token-1', + 'CHECKOUT-200', + 'TRK-001', + 'INPOST', + 'InPost' + ) + ->willReturn([]); + + $service = $this->createService(); + $result = $service->checkCreationStatus(11); + + $this->assertSame('created', $result['status']); + $this->assertSame('TRK-001', $result['tracking_number']); + } + + public function testCheckCreationStatusSkipsPushForNonAllegroOrder(): void + { + $this->tokenManager + ->method('resolveToken') + ->willReturn(['token-1', 'sandbox']); + + $this->packages + ->method('findById') + ->with(12) + ->willReturn([ + 'id' => 12, + 'order_id' => 201, + 'command_id' => 'cmd-12', + 'carrier_id' => 'DPD', + ]); + + $this->packages + ->expects($this->once()) + ->method('update'); + + $this->ordersRepository + ->method('findDetails') + ->with(201) + ->willReturn([ + 'order' => [ + 'source' => 'shoppro', + 'source_order_id' => 'SP-ORDER-1', + ], + ]); + + $this->apiClient + ->method('getShipmentCreationStatus') + ->willReturn([ + 'status' => 'SUCCESS', + 'shipmentId' => 'SHIP-12', + ]); + + $this->apiClient + ->method('getShipmentDetails') + ->willReturn([ + 'waybill' => 'TRK-002', + ]); + + $this->apiClient + ->expects($this->never()) + ->method('addShipmentToOrder'); + + $service = $this->createService(); + $result = $service->checkCreationStatus(12); + + $this->assertSame('created', $result['status']); + $this->assertSame('TRK-002', $result['tracking_number']); + } + + public function testCheckCreationStatusDoesNotFailWhenPushReturnsError(): void + { + $this->tokenManager + ->method('resolveToken') + ->willReturn(['token-1', 'sandbox']); + + $this->packages + ->method('findById') + ->with(13) + ->willReturn([ + 'id' => 13, + 'order_id' => 202, + 'command_id' => 'cmd-13', + 'carrier_id' => 'DHL', + ]); + + $this->packages + ->expects($this->once()) + ->method('update'); + + $this->packages + ->method('resolveCarrierName') + ->willReturn('DHL'); + + $this->ordersRepository + ->method('findDetails') + ->with(202) + ->willReturn([ + 'order' => [ + 'source' => 'allegro', + 'source_order_id' => 'CHECKOUT-202', + ], + ]); + + $this->apiClient + ->method('getShipmentCreationStatus') + ->willReturn([ + 'status' => 'SUCCESS', + 'shipmentId' => 'SHIP-13', + ]); + + $this->apiClient + ->method('getShipmentDetails') + ->willReturn([ + 'waybill' => 'TRK-003', + ]); + + $this->apiClient + ->expects($this->once()) + ->method('addShipmentToOrder') + ->willThrowException(new RuntimeException('API Allegro HTTP 422')); + + $service = $this->createService(); + $result = $service->checkCreationStatus(13); + + $this->assertSame('created', $result['status']); + $this->assertSame('TRK-003', $result['tracking_number']); + } + + public function testCheckCreationStatusRetriesPushAfter401(): void + { + $this->tokenManager + ->method('resolveToken') + ->willReturnOnConsecutiveCalls( + ['token-old', 'sandbox'], + ['token-new', 'production'] + ); + + $this->packages + ->method('findById') + ->with(14) + ->willReturn([ + 'id' => 14, + 'order_id' => 203, + 'command_id' => 'cmd-14', + 'carrier_id' => 'INPOST', + ]); + + $this->packages + ->expects($this->once()) + ->method('update'); + + $this->packages + ->method('resolveCarrierName') + ->willReturn('InPost'); + + $this->ordersRepository + ->method('findDetails') + ->with(203) + ->willReturn([ + 'order' => [ + 'source' => 'allegro', + 'source_order_id' => 'CHECKOUT-203', + ], + ]); + + $this->apiClient + ->method('getShipmentCreationStatus') + ->with('sandbox', 'token-old', 'cmd-14') + ->willReturn([ + 'status' => 'SUCCESS', + 'shipmentId' => 'SHIP-14', + ]); + + $this->apiClient + ->method('getShipmentDetails') + ->with('sandbox', 'token-old', 'SHIP-14') + ->willReturn([ + 'waybill' => 'TRK-004', + ]); + + $this->apiClient + ->expects($this->exactly(2)) + ->method('addShipmentToOrder') + ->willReturnCallback(static function ( + string $environment, + string $token, + string $checkoutId, + string $waybill, + string $carrierId, + string $carrierName + ): array { + static $calls = 0; + $calls++; + + if ($calls === 1) { + TestCase::assertSame('sandbox', $environment); + TestCase::assertSame('token-old', $token); + TestCase::assertSame('CHECKOUT-203', $checkoutId); + TestCase::assertSame('TRK-004', $waybill); + TestCase::assertSame('INPOST', $carrierId); + TestCase::assertSame('InPost', $carrierName); + throw new RuntimeException('ALLEGRO_HTTP_401'); + } + + TestCase::assertSame('production', $environment); + TestCase::assertSame('token-new', $token); + return []; + }); + + $service = $this->createService(); + $result = $service->checkCreationStatus(14); + + $this->assertSame('created', $result['status']); + $this->assertSame('TRK-004', $result['tracking_number']); + } + + private function createService(): AllegroShipmentService + { + return new AllegroShipmentService( + $this->tokenManager, + $this->apiClient, + $this->packages, + $this->companySettings, + $this->ordersRepository + ); + } +}