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.
+
+
+
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
+ );
+ }
+}