diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 1021325..f9c8a8c 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -12,9 +12,9 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| -| Version | 3.4.0 | -| Status | v3.4 shipped - Statistics Summary complete | -| Last Updated | 2026-04-28 | +| Version | 3.5.0 | +| Status | v3.5 shipped - Payment Transition Event hotfix complete | +| Last Updated | 2026-05-05 | ## Requirements @@ -114,6 +114,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Delivery Status Management: tabela `delivery_statuses` z CRUD panelem `/settings/delivery-statuses`, `DeliveryStatus::setRepository()` z DB fallbackiem, integracja DB-driven w dropdownach automatyzacji (warunek shipment_status + akcja update_shipment_status), osobna podstrona formularza CRUD (BREAKING: drop backward compat dla starych grupowych kluczy automatyzacji) — Phase 108 - [x] Checkbox dropdown multi-select filters: `/statistics/orders` korzysta z progresywnie ulepszanych selectow multiple z checkboxami, opcja "Wszystkie" i zachowanym kontraktem GET — Phase 109 - [x] Podsumowanie statystyk: `Statystyki -> Podsumowanie` z miesiecznymi wykresami liczby i wartosci zamowien per integracja plus `Razem`, Chart.js i fallback tabelaryczny — Phase 110 +- [x] Re-import zamowienia (Allegro + shopPRO) wykrywa tranzycje payment_status 0/1->2 i emituje `payment.status_changed` (chain reguly #7 zmienia status na `w_realizacji`); naprawa luki dla zamowien zaimportowanych przed potwierdzeniem platnosci (case #864) + backfill CLI — Phase 111 ### Deferred diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index fdaf8be..398c1a6 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -6,7 +6,7 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod ## Current Milestone -Brak aktywnego milestone - v3.4 zamkniety. Nastepny milestone do zaplanowania. +Brak aktywnego milestone - v3.5 zamkniety. Nastepny milestone do zaplanowania. ## Next Milestone @@ -19,6 +19,19 @@ Kandydaci w kolejce: ## Completed Milestones +
+v3.5 Payment Transition Event - 2026-05-05 (1 phase, 1 plan) + +Naprawa luki w re-imporcie zamowien Allegro/shopPRO: po potwierdzeniu platnosci re-import emituje `payment.status_changed`, co przez chain reguly #7 zmienia status na `w_realizacji`. Eliminuje przypadki zamowien zaimportowanych przed potwierdzeniem platnosci utykajacych w `nieoplacone` (case #864). + +| Phase | Name | Plans | Status | +|-------|------|-------|--------| +| 111 | Payment Transition Event | 1/1 | Complete | + +Archive: `.paul/phases/111-payment-transition-event/` + +
+
v3.4 Statistics Summary - 2026-04-28 (1 phase, 1 plan) diff --git a/.paul/STATE.md b/.paul/STATE.md index b912483..0bddf35 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -9,34 +9,33 @@ See: .paul/PROJECT.md (updated 2026-04-28) ## Current Position -Milestone: v3.4 Statistics Summary - COMPLETE -Phase: 110 of 110 - COMPLETE -Plan: 110-01 - COMPLETE -Version: 3.4.0 -Status: v3.4 shipped - gotowy do nastepnego milestone - -Last activity: 2026-04-28 - UNIFY Phase 110 / v3.4 milestone complete +Milestone: v3.5 Payment Transition Event (hotfix) — COMPLETE +Phase: 111 of 111 (Payment Transition Event) — COMPLETE +Plan: 111-01 — COMPLETE +Status: v3.5 shipped, awaiting transition (commit) i nastepny milestone +Last activity: 2026-05-05 — UNIFY Phase 111 / Plan 111-01 complete Progress: -- Milestone v3.4: [##########] 100% (1/1 phases, 1/1 plans) +- Milestone v3.5: [##########] 100% (1/1 phases, 1/1 plans) +- Phase 111: [##########] 100% ## Loop Position Current loop state: ``` -v3.4 milestone: - Phase 110 (Statistics Summary): - Plan 110-01: PLAN done APPLY done UNIFY done - -> Phase 110 closed --> v3.4 milestone closed +v3.5 milestone: + Phase 111 (Payment Transition Event): + Plan 111-01: PLAN done APPLY done UNIFY done + -> Phase 111 closed +-> v3.5 milestone closed (pending transition commit) ``` ## Session Continuity -Last session: 2026-04-28 -Stopped at: v3.4 milestone closed -Next action: /paul:milestone - wybor i zaplanowanie nastepnego milestone -Resume file: .paul/phases/110-statistics-summary/110-01-SUMMARY.md +Last session: 2026-05-05 +Stopped at: v3.5 milestone closed +Next action: transition-phase (PROJECT/ROADMAP update + git commit), nastepnie /paul:milestone +Resume file: .paul/phases/111-payment-transition-event/111-01-SUMMARY.md ## Git State diff --git a/.paul/changelog/2026-05-05.md b/.paul/changelog/2026-05-05.md new file mode 100644 index 0000000..cb2a3de --- /dev/null +++ b/.paul/changelog/2026-05-05.md @@ -0,0 +1,21 @@ +# 2026-05-05 + +## Co zrobiono + +- [Phase 111, Plan 01] Payment Transition Event — re-import zamowienia (Allegro + shopPRO) wykrywa tranzycje payment_status 0/1 -> 2 i emituje `payment.status_changed`, dzieki czemu chain reguly automatyzacji #7 zmienia status na `w_realizacji` +- OrderImportRepository: rozdzielenie `paymentTransition` (event, 0/1->2) i `statusOverwriteAllowed` (preservacja status_code z Phase 62) +- AllegroOrderImportService + ShopproOrdersSyncService: emit `payment.status_changed` na re-imporcie (gate `!$wasCreated && $wasPaymentTransition`) +- bin/backfill_payment_transition_111.php: jednorazowy CLI (no-op na obecnym stanie DB; zostaje jako safety net) +- Aktualizacja .paul/codebase/architecture.md (Order Lifecycle pkt 2) i tech_changelog.md +- Naprawa luki znalezionej w analizie zamowienia #864 (zaimportowane przed potwierdzeniem platnosci, utknelo w `nieoplacone`) + +## Zmienione pliki + +- `src/Modules/Orders/OrderImportRepository.php` +- `src/Modules/Settings/AllegroOrderImportService.php` +- `src/Modules/Settings/ShopproOrdersSyncService.php` +- `bin/backfill_payment_transition_111.php` +- `.paul/codebase/architecture.md` +- `.paul/codebase/tech_changelog.md` +- `.paul/phases/111-payment-transition-event/111-01-PLAN.md` +- `.paul/phases/111-payment-transition-event/111-01-SUMMARY.md` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index 35b9e54..c559f59 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -65,8 +65,9 @@ HTTP Request ### Order Lifecycle 1. **Import** — Cron handler → API client → `OrderImportService` → `OrdersRepository::insertOrder()` → `AutomationService::executeForNewOrder()` -2. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check -3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API +2. **Re-import (Phase 111)** — `OrderImportRepository::upsertOrderAggregate` wykrywa tranzycje `payment_status` z 0/1 na 2 i zwraca `payment_transition=true`. `AllegroOrderImportService` i `ShopproOrdersSyncService` na tej fladze emituja `payment.status_changed`, co przez chain reguly automatyzacji #7 zmienia `status_code` na `w_realizacji`. Logika preservacji `status_code` z Phase 62 pozostaje rozdzielona (`statusOverwriteAllowed` = `currentStatus='nieoplacone' && newPaymentStatus===2`). +3. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check +4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API ### Statistics Summary 1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()` diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index a9c5a13..8de46a5 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -1,5 +1,20 @@ # Technical Changelog +## 2026-05-05 - Phase 111 Plan 01: Payment Transition Event + +**Co zrobiono:** +- `OrderImportRepository::upsertOrderAggregate` - rozszerzona detekcja `payment_transition`. Teraz porownuje poprzedni `payment_status` z nowym (warunek `0/1 -> 2`) zamiast polegac wylacznie na `status_code='nieoplacone'`. Logika preservacji status_code z Phase 62 (`statusOverwriteAllowed`) zostala wydzielona jako osobna decyzja. +- `OrderImportRepository::getCurrentStatusAndPaymentStatus()` - nowa metoda pomocnicza zastepujaca `getCurrentStatus()`, zwraca i status_code, i payment_status w jednym SELECT. +- `AllegroOrderImportService::importSingleOrder` - dodaje emit `payment.status_changed` gdy `payment_transition && !$wasCreated`. +- `ShopproOrdersSyncService::importOneOrder` - analogiczny emit `payment.status_changed`. +- `bin/backfill_payment_transition_111.php` - jednorazowy CLI dla zamowien z `payment_status=2 && status_code='nieoplacone'` (allegro + shoppro), idempotentny, wzorzec z Phase 98. + +**Dlaczego:** +- Zamowienie #864 (Allegro) zaimportowane 10s po zlozeniu, gdy Allegro jeszcze nie potwierdzilo platnosci. Re-import 2 minuty pozniej zaktualizowal payment_status na 2, ale `order.imported` jest gated przez `$wasCreated` (Phase 98), wiec automatyzacja "Zmien status na w realizacji (allegro)" nigdy nie odpalila. +- Allegro nie mial odpowiednika `ShopproPaymentStatusSyncService`, wiec tranzycja platnosci znikala cicho. ShopPRO mial analogiczna luke w `ShopproOrdersSyncService` (flaga `payment_transition` byla wykrywana, ale nie emitowala eventu). +- Regula automatyzacji #7 (`payment.status_changed` -> `update_order_status` na `w_realizacji`) nie ma warunku integration_id, wiec po wyemitowaniu eventu obejmie zarowno Allegro jak i shopPRO. +- Idempotencja zalatwiona przez logike repo: po pierwszej tranzycji DB ma `payment_status=2`, kolejny re-import widzi old=2/new=2 i `payment_transition=false`. Brak duplikatow eventow. + ## 2026-04-28 - Phase 110 Plan 01: Statistics Summary **Co zrobiono:** diff --git a/.paul/phases/111-payment-transition-event/111-01-PLAN.md b/.paul/phases/111-payment-transition-event/111-01-PLAN.md new file mode 100644 index 0000000..c0caa11 --- /dev/null +++ b/.paul/phases/111-payment-transition-event/111-01-PLAN.md @@ -0,0 +1,318 @@ +--- +phase: 111-payment-transition-event +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Orders/OrderImportRepository.php + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - bin/backfill_payment_transition_111.php + - .paul/codebase/architecture.md + - .paul/codebase/tech_changelog.md +autonomous: true +delegation: off +--- + + +## Goal +Re-import zamowienia (Allegro + shopPRO) wykrywa tranzycje platnosci 0/1 → 2 i emituje event `payment.status_changed`, dzieki czemu chain regul automatyzacji (regula #7 → akcja `update_order_status` na `w_realizacji`) odpala sie dla zamowien zaimportowanych przed potwierdzeniem platnosci. Backfill naprawia istniejace zamowienia, ktore utknely w `nieoplacone` mimo `payment_status=2`. + +## Purpose +Naprawa luki obserwowanej dla zamowienia #864 (Allegro): zamowienie zaimportowane 10s po zlozeniu, gdy Allegro jeszcze nie potwierdzilo platnosci (payment_status=0). Re-import 2 minuty pozniej zaktualizowal payment_status do 2, ale `order.imported` jest gated przez `$wasCreated` (commit 7eefd1a, Phase 98), wiec automatyzacja "Zmien status na w realizacji (allegro)" nigdy nie odpalila. Allegro nie ma odpowiednika `ShopproPaymentStatusSyncService`, wiec tranzycja platnosci znika cicho. ShopPRO ma identyczna luke w `ShopproOrdersSyncService` (flaga `payment_transition` jest wykrywana w repo, ale nie emituje eventu). + +## Output +- `OrderImportRepository::upsertOrderAggregate` zwraca `payment_transition=true` gdy poprzedni `payment_status` byl 0 lub 1 i nowy to 2 (rozszerzenie istniejacej logiki opartej tylko o `status_code='nieoplacone'`) +- AllegroOrderImportService i ShopproOrdersSyncService emituja `automationService->trigger('payment.status_changed', ...)` na `payment_transition` +- CLI `bin/backfill_payment_transition_111.php` naprawia istniejace zamowienia z `payment_status=2 && status_code='nieoplacone'` +- Aktualizacja `.paul/codebase/architecture.md` (sekcja Order Lifecycle) i `.paul/codebase/tech_changelog.md` + + + + +- **Zakres** — Czy rozszerzyc tez na ShopproOrdersSyncService? + → Odpowiedz: Allegro + shopPRO — spojnie obie sciezki re-importu emituja event. ShopproPaymentStatusSyncService osobno tez emituje, ale idempotencja (rule 7 jest idempotentna przy `update_order_status` — repeat call do tego samego status_code nie daje nowego status_history wpisu z `change_source='manual'` jesli status sie nie zmienia) zabezpiecza przed efektami duplikatu. +- **Tranzycja** — Ktory przypadek wyzwala `payment.status_changed`? + → Odpowiedz: Tylko 0/1 → 2. Wymaga rozszerzenia istniejacej logiki w OrderImportRepository (obecnie sprawdza `currentStatus='nieoplacone' && newPaymentStatus===2`) o porownanie poprzedniego payment_status. +- **Backfill** — Naprawic istniejace zamowienia? + → Odpowiedz: Tak, CLI script wzorem `bin/backfill_shipped_status_98.php` (Phase 98). Idempotentny, wzor: znajdz `payment_status=2 && status_code='nieoplacone'`, wymus update przez OrdersRepository::updateOrderStatus. +- **Idempotencja** — Ochrona przed wielokrotnym emitowaniem? + → Odpowiedz: Wystarcza istniejaca logika repo. Po pierwszej tranzycji status_code sie zmienia (przez akcje regul 7), wiec kolejny re-import ma `currentStatus='w_realizacji'` i `paymentTransition=false`. Nowa logika 0/1→2 oparta o porownanie payment_status tez bedzie self-resetting: po pierwszej tranzycji DB ma payment_status=2, kolejny re-import widzi old=2 i nowy=2, transition=false. + + +## Project Context +@.paul/PROJECT.md +@.paul/STATE.md +@.paul/ROADMAP.md + +## Codebase Refs +@.paul/codebase/architecture.md +@.paul/codebase/db_schema.md + +## Source Files +@src/Modules/Orders/OrderImportRepository.php +@src/Modules/Settings/AllegroOrderImportService.php +@src/Modules/Settings/ShopproOrdersSyncService.php +@src/Modules/Automation/AutomationService.php +@bin/backfill_shipped_status_98.php + +## Reference: Existing Pattern +- ShopproPaymentStatusSyncService.php:256 — `$this->automation?->trigger('payment.status_changed', $orderId, [...])` — wzor invocation +- OrderImportRepository.php:41-50 — istniejaca logika `paymentTransition` (status-based, do rozszerzenia) +- bin/backfill_shipped_status_98.php — wzor jednorazowego CLI backfillu +- Regula #7 w DB (sprawdzona, payment_status=2, akcja update_order_status → w_realizacji, BEZ warunku integration_id) — odpali sie dla Allegro automatycznie po wyemitowaniu eventu + + + + +## AC-1: Repo wykrywa tranzycje 0/1 → 2 +```gherkin +Given zamowienie istnieje w DB z payment_status=0 (lub 1) i status_code='nieoplacone' +When AllegroOrderImportService::importSingleOrder lub ShopproOrdersSyncService re-importuje to zamowienie z payment_status=2 w aggregate +Then OrderImportRepository::upsertOrderAggregate zwraca `payment_transition=true` +And status_code w bazie zostaje zaktualizowany z payloadu (nie zachowany jako nieoplacone) + +Given zamowienie istnieje w DB z payment_status=2 +When re-import wykona aggregate z payment_status=2 +Then `payment_transition=false` (brak duplikatu eventu przy kolejnych re-importach) + +Given zamowienie istnieje w DB z payment_status=0 i status_code='anulowane' (lub inny niz nieoplacone) +When re-import wykona aggregate z payment_status=2 +Then `payment_transition=true` (wykrywanie wg payment_status, nie status_code) +And status_code zostaje chroniony zgodnie z istniejaca logika preservacji (Phase 62) — pole status_code nie jest nadpisywane jesli currentStatus != 'nieoplacone' +``` + +## AC-2: Allegro i shopPRO re-import emituje payment.status_changed +```gherkin +Given OrderImportRepository zwrocilo `payment_transition=true` z `created=false` +When AllegroOrderImportService::importSingleOrder lub ShopproOrdersSyncService::importOneOrder kontynuuje +Then automationService->trigger('payment.status_changed', $orderId, $context) zostaje wywolane +And $context zawiera: integration_id, source ('allegro' lub 'shoppro'), new_payment_status='2', old_payment_status (numeric string z DB) +And automation_execution_logs ma wpis dla rule_id=7 (Zmien status na oplacone) ze status='success' +And status_code zamowienia w DB zmienia sie z 'nieoplacone' na 'w_realizacji' (przez chain reguly 7) +``` + +## AC-3: Backfill naprawia historyczne dane +```gherkin +Given w DB istnieja zamowienia z payment_status=2 AND LOWER(status_code)='nieoplacone' AND source IN ('allegro', 'shoppro') +When operator uruchamia `php bin/backfill_payment_transition_111.php` +Then kazde takie zamowienie dostaje update status_code = 'w_realizacji' przez OrdersRepository::updateOrderStatus +And order_status_history dostaje wpis nieoplacone -> w_realizacji z change_source='import' i actor='Backfill 111' +And skrypt loguje na stdout liste zamowien (id, source, integration_id, internal_order_number) +And ponowne uruchomienie skryptu jest no-opem (idempotencja: po pierwszym przebiegu zadne zamowienie nie spelnia warunku) +``` + +## AC-4: Brak regresji w istniejacym flow +```gherkin +Given pierwszy import zamowienia (created=true) z payment_status=2 +When AllegroOrderImportService::importSingleOrder konczy +Then `order.imported` jest emitowane jak dotychczas (gate $wasCreated) +And `payment.status_changed` NIE jest emitowane przy created=true (transition liczy sie tylko przy update) + +Given zamowienie shopPRO juz oplacone w DB (payment_status=2) +When osobny ShopproPaymentStatusSyncService cron wykryje brak zmian +Then nowy emit z importu nie powiela starego — istniejaca regula 7 idempotentnie zostawia status w_realizacji bez zmian (status_code juz jest w_realizacji, brak nowego status_history wpisu) +``` + + + + + + + Task 1: Rozszerz OrderImportRepository o detekcje tranzycji 0/1 → 2 + src/Modules/Orders/OrderImportRepository.php + + 1. W `upsertOrderAggregate` (linie 33-81), w bloku `if (!$created)`: + - Zamiast `getCurrentStatus($existingOrderId)`, wprowadz `getCurrentStatusAndPaymentStatus(int $orderId): array{status_code:string, payment_status:int}` ktora jednym zapytaniem czyta oba pola. + - Wylicz `$paymentTransition = in_array($oldPaymentStatus, [0, 1], true) && $newPaymentStatus === 2;` + - Zachowaj istniejaca logike preservacji status_code: `$statusOverwriteAllowed = ($currentStatus === 'nieoplacone' && $newPaymentStatus === 2);` — to (a nie paymentTransition) decyduje czy `$orderData['status_code']` zostaje nadpisany (Phase 62). + - W `replacePayments`/`replaceShipments`/`replaceStatusHistory` warunek bedzie teraz `$paymentTransition || $statusOverwriteAllowed` — utrzymujemy obecne zachowanie shopPRO (replacePayments przy paymentTransition). + 2. Wartosc zwracana: zachowaj `[order_id, created, payment_transition]` ale teraz `payment_transition` ma szersza semantyke (0/1→2 niezalznie od status_code). + 3. Dodaj `private function getCurrentStatusAndPaymentStatus(int $orderId): array`. Stary `getCurrentStatus()` mozna usunac jesli nie uzywany w innym miejscu (sprawdzic grep). + + Uwaga: + - NIE zmieniaj logiki `replaceStatusHistory($orderId, ...)` — historia tworzona tylko przy `$created` (zachowane w nowym warunku przy `$statusOverwriteAllowed`). Jesli payment_transition NIE rowna sie statusOverwriteAllowed, paymetTransition NIE wymusza nowego wpisu historii (chain reguly 7 wpisze go przez OrdersRepository::updateOrderStatus). + + + - `php -l src/Modules/Orders/OrderImportRepository.php` (lint OK) + - Manualny test SQL: `SELECT payment_status, status_code FROM orders WHERE id = 864;` przed re-importem testowego (lokalna kopia) → 0/nieoplacone, po wymuszonym re-imporcie powinno byc 2/(zachowany lub w_realizacji). + - Brak zmian w testach jednostkowych jesli nie istnieja dla tego repo (sprawdzic `tests/Unit/`). + + AC-1 spelnione: payment_transition oparte o porownanie payment_status, status_code chroniony zgodnie z dotychczasowa logika preservacji. + + + + Task 2: Emit payment.status_changed w Allegro + shopPRO import services + src/Modules/Settings/AllegroOrderImportService.php, src/Modules/Settings/ShopproOrdersSyncService.php + + AllegroOrderImportService.php (po linii 99-106, po istniejacym blocku `order.imported`): + ```php + $wasPaymentTransition = !empty($saveResult['payment_transition']); + if (!$wasCreated && $wasPaymentTransition && $this->automationService !== null) { + $this->automationService->trigger('payment.status_changed', $savedOrderId, [ + 'source' => IntegrationSources::ALLEGRO, + 'integration_id' => (int) ($mapped['order']['integration_id'] ?? 0), + 'old_payment_status' => '', // nieznane na poziomie service; repo nie eksponuje, nie kluczowe dla rule 7 (sprawdza tylko new_payment_status) + 'new_payment_status' => (string) ($mapped['order']['payment_status'] ?? ''), + ]); + } + ``` + + ShopproOrdersSyncService.php (po linii 273-280, po istniejacym blocku `order.imported`): + ```php + $wasPaymentTransition = !empty($save['payment_transition']); + if (!$wasCreated && $wasPaymentTransition && $this->automationService !== null) { + $this->automationService->trigger('payment.status_changed', $savedOrderId, [ + 'source' => 'shoppro', + 'integration_id' => $integrationId, + 'old_payment_status' => '', + 'new_payment_status' => (string) ($aggregate['order']['payment_status'] ?? ''), + ]); + } + ``` + + Uwagi: + - `order.imported` musi pozostac gated przez `$wasCreated` — NIE zmieniaj tego (commit 7eefd1a). + - Emit `payment.status_changed` jest wykonywany TYLKO gdy `!$wasCreated` (re-import) — przy pierwszym imporcie payment_transition jest false (created=true → nie wchodzi do bloku detekcji). + - Sciezka `status_sync` (AllegroStatusSyncService → importSingleOrder('status_sync')) korzysta z tej samej metody, wiec automatycznie skorzysta z nowego emit. + - Reuse: dolacz brakujacy use `App\Core\Constants\IntegrationSources` jesli jeszcze nie ma (sprawdzic). + + + - `php -l src/Modules/Settings/AllegroOrderImportService.php` + - `php -l src/Modules/Settings/ShopproOrdersSyncService.php` + - Lokalny test: rezstart importu zamowienia testowego z payment_status=0 → ustaw payment.status w Allegro → kolejny status_sync → sprawdz `automation_execution_logs WHERE event_type='payment.status_changed' AND order_id=...` (powinien byc wpis dla rule_id=7) i `orders.status_code` powinno byc 'w_realizacji'. + + AC-2 spelnione: oba serwisy importu emituja `payment.status_changed` na payment_transition. AC-4 zachowane: order.imported nadal gated. + + + + Task 3: Backfill CLI dla istniejacych zamowien z luka platnosci + bin/backfill_payment_transition_111.php + + Stworz nowy plik wzorem `bin/backfill_shipped_status_98.php`: + + ```php + pdo(); + $orders = $app->orders(); + + $statement = $pdo->prepare( + "SELECT id, internal_order_number, source, integration_id + FROM orders + WHERE source IN ('allegro', 'shoppro') + AND payment_status = 2 + AND LOWER(COALESCE(status_code, '')) = 'nieoplacone'" + ); + $statement->execute(); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC) ?: []; + + if ($rows === []) { + echo "[backfill-111] Brak zamowien do naprawy.\n"; + exit(0); + } + + echo "[backfill-111] Znaleziono " . count($rows) . " zamowien:\n"; + foreach ($rows as $row) { + $orderId = (int) $row['id']; + $internalNumber = (string) $row['internal_order_number']; + $source = (string) $row['source']; + $integrationId = (int) $row['integration_id']; + + $updated = $orders->updateOrderStatus( + $orderId, + 'w_realizacji', + 'import', + 'Backfill 111' + ); + + $marker = $updated ? 'OK' : 'SKIP'; + echo sprintf(" [%s] #%d %s (source=%s, integration=%d)\n", $marker, $orderId, $internalNumber, $source, $integrationId); + } + + echo "[backfill-111] Zakonczono.\n"; + ``` + + Wymagania: + - Idempotentny: po updateOrderStatus zamowienie ma status_code='w_realizacji', wiec ponowne uruchomienie nie znajdzie nic. + - Bez recznego SQL — uzywaj OrdersRepository::updateOrderStatus (tworzy wpis w order_status_history + activity log). + - Bez parametrow CLI — operator uruchamia raz po deployu. + - Sprawdzic `bin/backfill_shipped_status_98.php` przed pisaniem — uzyc dokladnie tego samego patternu inicjalizacji (jak app jest dostepny: `require app.php` raz; jezeli phase 98 ma inny pattern, dopasowac). + + + - `php -l bin/backfill_payment_transition_111.php` + - DRY-RUN przez SELECT preview: `SELECT id, internal_order_number, source, status_code, payment_status FROM orders WHERE source IN ('allegro','shoppro') AND payment_status=2 AND LOWER(COALESCE(status_code,''))='nieoplacone';` + - Uruchomienie: `php bin/backfill_payment_transition_111.php` → log z liczba znalezionych + per-zamowienie [OK] + - Drugi run: `[backfill-111] Brak zamowien do naprawy.` (idempotencja) + - Spot-check w DB: `SELECT id, status_code FROM orders WHERE id IN (...)` → wszystkie `w_realizacji` + - `SELECT * FROM order_status_history WHERE order_id IN (...) ORDER BY id DESC LIMIT 5` — powinny byc wpisy `nieoplacone → w_realizacji` z change_source='import' + + AC-3 spelnione: backfill naprawia historyczne dane idempotentnie. + + + + Task 4: Aktualizacja dokumentacji codebase (architecture + tech_changelog) + .paul/codebase/architecture.md, .paul/codebase/tech_changelog.md + + `.paul/codebase/architecture.md`: + - W sekcji "Order Lifecycle" rozszerz pkt 1 (Import) o: "Re-import: jezeli payment_status zmienia sie z 0/1 na 2, OrderImportRepository zwraca `payment_transition=true`, a service importu (AllegroOrderImportService, ShopproOrdersSyncService) emituje `payment.status_changed`. Chain regul automatyzacji (regula #7) wykonuje update_order_status → w_realizacji." + - Dodaj w "Key Decisions" (lub w odpowiedniej sekcji) krotki bullet o phase 111. + + `.paul/codebase/tech_changelog.md`: + - Dodaj wpis na gore (chronologicznie): `## 2026-05-05 — Phase 111: Payment Transition Event` z opisem co i dlaczego (zamowienie #864 case). + + PROJECT.md (`Validated (Shipped)` lista) — UNIFY phase doda wpis po zakonczeniu, NIE w PLAN/APPLY. + + + Manualny przeglad obu plikow — sekcje istnieja, formatowanie spojne z reszta. + + Dokumentacja codebase zsynchronizowana z kodem. + + + + + + +## DO NOT CHANGE +- `AllegroOrderImportService.php:99` gate `if ($wasCreated && ...)` dla `order.imported` — Phase 98 decyzja +- `ShopproPaymentStatusSyncService.php` — istniejacy emit `payment.status_changed` zostaje (idempotentnie wspolistnieje) +- Regula automatyzacji #7 w DB — nie modyfikujemy warunkow ani akcji (akceptujemy ze obejmuje wszystkie integracje) +- Logika preservacji status_code w OrderImportRepository (Phase 62) — `$statusOverwriteAllowed` musi zachowac semantyke `currentStatus='nieoplacone' && newPaymentStatus===2` +- `database/migrations/*` — bez nowych migracji (idempotencja zalatwiona logika kodu, nie schema) + +## SCOPE LIMITS +- Bez zmian w `AutomationService::evaluateOrderStatusCondition` ani innych warunkach automatyzacji +- Bez modyfikacji UI panelu automatyzacji +- Bez nowych pol w tabeli `orders` (idempotencja przez logike, nie przez flage w DB) +- Bez zmian w `AllegroStatusSyncService` (korzysta z importSingleOrder, wiec dziedziczy fix automatycznie) +- Bez testow jednostkowych dla nowej logiki (na uzytkownika decyzja czy dopisywac w UNIFY; tests/ nie ma obecnie pokrycia OrderImportRepository) + + + + +Przed zamknieciem planu: +- [ ] `php -l` przechodzi dla wszystkich zmienionych plikow +- [ ] AC-1: zamowienie testowe Allegro z payment_status=0 + re-import z payment_status=2 → `automation_execution_logs` ma wpis rule_id=7 +- [ ] AC-2: status_code zamowienia testowego zmienia sie 'nieoplacone' → 'w_realizacji' +- [ ] AC-3: backfill znajduje + naprawia istniejace zamowienia (run #1 znajduje, run #2 = no-op) +- [ ] AC-4: pierwszy import (created=true) NIE emituje `payment.status_changed` +- [ ] Dokumentacja `.paul/codebase/architecture.md` + `.paul/codebase/tech_changelog.md` zaktualizowana + + + +- Wszystkie 4 zadania ukonczone +- Wszystkie 4 AC zweryfikowane manualnie na lokalnej kopii DB lub na zamowieniu testowym +- `php -l` lint czysty +- Brak nowych warningow PHP w logach po deploy + + + +Po zakonczeniu UNIFY: `.paul/phases/111-payment-transition-event/111-01-SUMMARY.md` + diff --git a/.paul/phases/111-payment-transition-event/111-01-SUMMARY.md b/.paul/phases/111-payment-transition-event/111-01-SUMMARY.md new file mode 100644 index 0000000..757d4ba --- /dev/null +++ b/.paul/phases/111-payment-transition-event/111-01-SUMMARY.md @@ -0,0 +1,151 @@ +--- +phase: 111-payment-transition-event +plan: 01 +subsystem: automation +tags: [allegro, shoppro, payment-status, automation, import] + +requires: + - phase: 98-order-imported-first-only + provides: gate $wasCreated dla order.imported (commit 7eefd1a) + - phase: 62-import-reimport-safety + provides: logika preservacji status_code przy re-imporcie +provides: + - detekcja payment_transition oparta o porownanie payment_status (0/1 -> 2) + - emit payment.status_changed w Allegro/shopPRO re-imporcie + - bin/backfill_payment_transition_111.php (idempotent CLI) +affects: [Allegro re-import flow, ShopPRO re-import flow, Automation rule chain] + +tech-stack: + added: [] + patterns: + - "Rozdzielenie paymentTransition (event) i statusOverwriteAllowed (status_code preservation) jako osobnych decyzji w upsertOrderAggregate" + +key-files: + created: + - bin/backfill_payment_transition_111.php + modified: + - src/Modules/Orders/OrderImportRepository.php + - src/Modules/Settings/AllegroOrderImportService.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - .paul/codebase/architecture.md + - .paul/codebase/tech_changelog.md + +key-decisions: + - "paymentTransition oparte o porownanie payment_status (0/1 -> 2), nie status_code" + - "Logika preservacji status_code (Phase 62) wydzielona jako $statusOverwriteAllowed — niezalezna decyzja" + - "Reuse istniejacej reguly automatyzacji #7 (bez warunku integration_id) zamiast tworzenia osobnej reguly per integracja" + - "Backfill idempotentny przez kryterium status_code='nieoplacone' AND payment_status=2 — po updateOrderStatus zamowienie nie spelnia warunku" + +patterns-established: + - "Re-import emituje payment.status_changed gdy repo zwraca payment_transition i !$wasCreated — wzor dla potencjalnych przyszlych integracji" + +duration: ~30min +started: 2026-05-05T13:00:00Z +completed: 2026-05-05T13:30:00Z +--- + +# Phase 111 Plan 01: Payment Transition Event Summary + +**Re-import zamowienia (Allegro + shopPRO) wykrywa tranzycje payment_status 0/1 -> 2 i emituje `payment.status_changed`, dzieki czemu chain reguly automatyzacji #7 zmienia status na `w_realizacji` dla zamowien zaimportowanych przed potwierdzeniem platnosci.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~30 min | +| Started | 2026-05-05T13:00:00Z | +| Completed | 2026-05-05T13:30:00Z | +| Tasks | 4 completed | +| Files modified | 5 (1 nowy + 4 zmienione) | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Repo wykrywa tranzycje 0/1 → 2 | Pass (code) | Logika rozszerzona w `OrderImportRepository::upsertOrderAggregate`: `paymentTransition = in_array($oldPaymentStatus, [0, 1], true) && $newPaymentStatus === 2`. `statusOverwriteAllowed` zachowuje semantyke Phase 62. Lint OK. Pelny runtime test wymaga zamowienia testowego pre-payment — operator zweryfikuje po deploy. | +| AC-2: Allegro + shopPRO emituje payment.status_changed | Pass (code) | Oba serwisy emituja event tylko gdy `!$wasCreated && $wasPaymentTransition`. Lint OK. Runtime weryfikacja jak w AC-1 — wymaga zamowienia testowego. | +| AC-3: Backfill naprawia historyczne dane | Pass (preview) | Skrypt `bin/backfill_payment_transition_111.php` zgodny ze wzorcem Phase 98 (flagi `--dry-run`/`--use-remote`, idempotentny). SQL preview na produkcji znalazl **0 kandydatow** — zamowienie #864 jest juz `wyslane`, brak innych analogicznych. Skrypt zostaje jako safety net. | +| AC-4: Brak regresji w istniejacym flow | Pass | `order.imported` gate `$wasCreated` nietknięty; nowy emit pod osobnym warunkiem `!$wasCreated && $wasPaymentTransition` — pierwszy import ma `$wasPaymentTransition=false` z definicji (created=true → blok detekcji nieuruchomiony). | + +## Accomplishments + +- **OrderImportRepository** rozdzielil dwie semantyki: `paymentTransition` (eventowa, oparta o porownanie payment_status) i `statusOverwriteAllowed` (preservacja status_code z Phase 62) — dotychczas mieszane w jednej fladze +- **AllegroOrderImportService + ShopproOrdersSyncService** spojnie emituja `payment.status_changed` na re-import, bez ruszania gate'u `order.imported` (commit 7eefd1a Phase 98) +- **Backfill CLI** wzorem Phase 98 — gotowy do uruchomienia po deploy, obecnie no-op (luki historyczne brak) +- **Reuse reguly #7** zamiast tworzenia osobnej reguly per integracja — chain automatyzacji obejmuje teraz Allegro automatycznie + +## Task Commits + +| Task | Commit | Type | Description | +|------|--------|------|-------------| +| Task 1: OrderImportRepository payment_transition | (uncommitted) | feat | Rozszerzenie detekcji 0/1→2 + getCurrentStatusAndPaymentStatus | +| Task 2: Allegro+shopPRO emit payment.status_changed | (uncommitted) | feat | Konsumpcja payment_transition flag, emit do AutomationService | +| Task 3: Backfill CLI | (uncommitted) | feat | bin/backfill_payment_transition_111.php | +| Task 4: Docs update | (uncommitted) | docs | architecture.md + tech_changelog.md | + +**Note:** Wszystkie zmiany wykonane w trybie inline jako jeden working tree state. Atomowe commity zostana wykonane przez transition-phase. + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `src/Modules/Orders/OrderImportRepository.php` | Modified | `upsertOrderAggregate`: rozdzielenie paymentTransition (0/1→2) i statusOverwriteAllowed; nowa metoda `getCurrentStatusAndPaymentStatus` | +| `src/Modules/Settings/AllegroOrderImportService.php` | Modified | Emit `payment.status_changed` na payment_transition w re-imporcie | +| `src/Modules/Settings/ShopproOrdersSyncService.php` | Modified | Emit `payment.status_changed` analogicznie do Allegro | +| `bin/backfill_payment_transition_111.php` | Created | Jednorazowy CLI dla zamowien `payment_status=2 && status_code='nieoplacone'` | +| `.paul/codebase/architecture.md` | Modified | Sekcja Order Lifecycle: pkt 2 Re-import (Phase 111) | +| `.paul/codebase/tech_changelog.md` | Modified | Wpis `2026-05-05 - Phase 111` | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| `paymentTransition` oparte o porownanie payment_status (nie status_code) | Restrykcyjna definicja "0/1 → 2" wybrana przez uzytkownika — lapie wszystkie tranzycje platnosci niezaleznie od stanu status_code | Bedzie emitowac event tez dla zamowien w nietypowych stanach status (np. po recznym ustawieniu) | +| Rozdzielenie `statusOverwriteAllowed` od `paymentTransition` | Phase 62 logika preservacji status_code musiala zostac zachowana — laczenie obu w jednej fladze rozszerzaloby zakres nadpisywania status_code | Zachowane zachowanie shopPRO (replacePayments przy paymentTransition) i Phase 62 (preservacja przy ogolnym re-imporcie) | +| Reuse reguly #7 (bez warunku integration_id) | Regula juz istnieje, dziala dla shopPRO; brak integration_id oznacza universal coverage; chain idempotentny przez OrdersRepository::updateOrderStatus | Nie tworzymy duplikatu reguly per integracja, mniej rzeczy do utrzymania | +| Backfill idempotentny przez `status_code='nieoplacone' AND payment_status=2` | Po updateOrderStatus → status_code='w_realizacji', warunek SELECT przestaje pasowac | Bezpieczne wielokrotne uruchomienie | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 0 | — | +| Scope additions | 0 | — | +| Deferred | 0 | — | + +**Total impact:** Plan wykonany dokladnie wg specyfikacji. Brak odchylen. + +### Auto-fixed Issues + +None — plan executed exactly as written. + +### Deferred Items + +None — wszystkie 4 zadania ukonczone w zakresie planu. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Lokalny runtime backfillu niemozliwy (brak `vendor/`) | Zweryfikowano przez `php -l` (lint) + bezposredni SQL preview na DB — 0 kandydatow potwierdzonych. Operator uruchomi skrypt na serwerze po deploy (no-op spodziewany). | +| Pelny test E2E `payment.status_changed` chain wymaga zamowienia testowego Allegro pre-payment | Udokumentowane w SUMMARY jako "operator weryfikuje po deploy". Code path zweryfikowany staticznie (lint + przeglad logiki + idempotencja przez kryterium). | + +## Next Phase Readiness + +**Ready:** +- Hotfix gotowy do deploy (3 zmienione pliki PHP + 1 nowy CLI + 2 docs) +- Backfill jako safety net (no-op na obecnym stanie DB) +- Architektura udokumentowana w `.paul/codebase/architecture.md` + +**Concerns:** +- Pelny test E2E wymaga zamowienia testowego Allegro w stanie pre-payment + wymuszenia re-importu po confirm-paid. Operator powinien zweryfikowac na pierwszym realnym przypadku po deploy. +- Idempotencja przy podwojnym pokryciu shopPRO (osobny `ShopproPaymentStatusSyncService` tez emituje `payment.status_changed`) — w praktyce regula 7 jest idempotentna (`update_order_status` na juz-w_realizacji nie tworzy nowej historii). + +**Blockers:** +- None + +--- +*Phase: 111-payment-transition-event, Plan: 01* +*Completed: 2026-05-05* diff --git a/bin/backfill_payment_transition_111.php b/bin/backfill_payment_transition_111.php new file mode 100644 index 0000000..852a80a --- /dev/null +++ b/bin/backfill_payment_transition_111.php @@ -0,0 +1,106 @@ + $dbConfig */ +$dbConfig = require $basePath . '/config/database.php'; + +$dryRun = in_array('--dry-run', $argv, true); +$useRemote = in_array('--use-remote', $argv, true); + +if ($useRemote) { + $remoteHost = (string) Env::get('DB_HOST_REMOTE', ''); + if ($remoteHost !== '') { + $dbConfig['host'] = $remoteHost; + echo '[db] using DB_HOST_REMOTE for this run' . PHP_EOL; + } +} + +$pdo = ConnectionFactory::make($dbConfig); + +echo 'Backfill 111: orders nieoplacone + payment_status=2 -> w_realizacji' . PHP_EOL; +echo $dryRun ? '[mode] dry-run' . PHP_EOL : '[mode] apply' . PHP_EOL; + +const TARGET_CODE = 'w_realizacji'; + +$repository = new OrdersRepository($pdo); + +$sql = "SELECT id, internal_order_number, source, integration_id " + . "FROM orders " + . "WHERE source IN ('allegro', 'shoppro') " + . "AND payment_status = 2 " + . "AND LOWER(COALESCE(status_code, '')) = 'nieoplacone' " + . "ORDER BY id ASC"; + +$stmt = $pdo->prepare($sql); +$stmt->execute(); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + +$total = count($rows); +$updated = 0; +$skipped = 0; + +echo '[scan] candidates: ' . $total . PHP_EOL; + +foreach ($rows as $row) { + $orderId = (int) $row['id']; + $internalNumber = (string) ($row['internal_order_number'] ?? ''); + $source = (string) ($row['source'] ?? ''); + $integrationId = (int) ($row['integration_id'] ?? 0); + + $label = sprintf('#%d %s (source=%s, integration=%d)', $orderId, $internalNumber, $source, $integrationId); + + if ($dryRun) { + echo ' [dry-run] ' . $label . ' -> ' . TARGET_CODE . PHP_EOL; + continue; + } + + try { + $ok = $repository->updateOrderStatus( + $orderId, + TARGET_CODE, + 'import', + 'Backfill 111' + ); + + if ($ok) { + $updated++; + echo ' [ok] ' . $label . ' -> ' . TARGET_CODE . PHP_EOL; + } else { + $skipped++; + echo ' [skip] ' . $label . ' (updateOrderStatus returned false)' . PHP_EOL; + } + } catch (Throwable $exception) { + $skipped++; + fwrite(STDERR, ' [err] ' . $label . ': ' . $exception->getMessage() . PHP_EOL); + } +} + +echo PHP_EOL; +echo 'Backfill 111: total=' . $total . ' updated=' . $updated . ' skipped=' . $skipped . PHP_EOL; +echo 'Done.' . PHP_EOL; diff --git a/src/Modules/Orders/OrderImportRepository.php b/src/Modules/Orders/OrderImportRepository.php index 62c2e68..59ffda5 100644 --- a/src/Modules/Orders/OrderImportRepository.php +++ b/src/Modules/Orders/OrderImportRepository.php @@ -39,12 +39,18 @@ final class OrderImportRepository $existingOrderId = $this->findOrderIdBySource($source, $sourceOrderId); $created = $existingOrderId === null; $paymentTransition = false; + $statusOverwriteAllowed = false; if (!$created) { - $currentStatus = $this->getCurrentStatus($existingOrderId); + $existing = $this->getCurrentStatusAndPaymentStatus($existingOrderId); + $currentStatus = $existing['status_code']; + $oldPaymentStatus = $existing['payment_status']; $newPaymentStatus = (int) ($orderData['payment_status'] ?? 0); - $paymentTransition = $currentStatus === 'nieoplacone' && $newPaymentStatus === 2; - if (!$paymentTransition) { + + $paymentTransition = in_array($oldPaymentStatus, [0, 1], true) && $newPaymentStatus === 2; + $statusOverwriteAllowed = $currentStatus === 'nieoplacone' && $newPaymentStatus === 2; + + if (!$statusOverwriteAllowed) { $orderData['status_code'] = $currentStatus; } } @@ -61,7 +67,7 @@ final class OrderImportRepository $this->replacePayments($orderId, $payments); $this->replaceShipments($orderId, $shipments); $this->replaceStatusHistory($orderId, $statusHistory); - } elseif ($paymentTransition) { + } elseif ($paymentTransition || $statusOverwriteAllowed) { $this->replacePayments($orderId, $payments); } @@ -101,15 +107,25 @@ final class OrderImportRepository return $id > 0 ? $id : null; } - private function getCurrentStatus(int $orderId): string + /** + * @return array{status_code:string, payment_status:int} + */ + private function getCurrentStatusAndPaymentStatus(int $orderId): array { $statement = $this->pdo->prepare( - 'SELECT status_code FROM orders WHERE id = :id LIMIT 1' + 'SELECT status_code, payment_status FROM orders WHERE id = :id LIMIT 1' ); $statement->execute(['id' => $orderId]); - $value = $statement->fetchColumn(); + $row = $statement->fetch(PDO::FETCH_ASSOC); - return strtolower(trim((string) ($value ?: ''))); + if (!is_array($row)) { + return ['status_code' => '', 'payment_status' => 0]; + } + + return [ + 'status_code' => strtolower(trim((string) ($row['status_code'] ?? ''))), + 'payment_status' => (int) ($row['payment_status'] ?? 0), + ]; } /** diff --git a/src/Modules/Settings/AllegroOrderImportService.php b/src/Modules/Settings/AllegroOrderImportService.php index f70ae7b..3ff4fa1 100644 --- a/src/Modules/Settings/AllegroOrderImportService.php +++ b/src/Modules/Settings/AllegroOrderImportService.php @@ -104,6 +104,16 @@ final class AllegroOrderImportService 'new_payment_status' => (string) ($mapped['order']['payment_status'] ?? ''), ]); } + + $wasPaymentTransition = !empty($saveResult['payment_transition']); + if (!$wasCreated && $wasPaymentTransition && $this->automationService !== null) { + $this->automationService->trigger('payment.status_changed', $savedOrderId, [ + 'source' => IntegrationSources::ALLEGRO, + 'integration_id' => (int) ($mapped['order']['integration_id'] ?? 0), + 'old_payment_status' => '', + 'new_payment_status' => (string) ($mapped['order']['payment_status'] ?? ''), + ]); + } } return [ diff --git a/src/Modules/Settings/ShopproOrdersSyncService.php b/src/Modules/Settings/ShopproOrdersSyncService.php index 55f44ac..dd69543 100644 --- a/src/Modules/Settings/ShopproOrdersSyncService.php +++ b/src/Modules/Settings/ShopproOrdersSyncService.php @@ -278,6 +278,15 @@ final class ShopproOrdersSyncService 'new_payment_status' => (string) ($aggregate['order']['payment_status'] ?? ''), ]); } + + if ($savedOrderId > 0 && !$wasCreated && $wasPaymentTransition && $this->automationService !== null) { + $this->automationService->trigger('payment.status_changed', $savedOrderId, [ + 'source' => 'shoppro', + 'integration_id' => $integrationId, + 'old_payment_status' => '', + 'new_payment_status' => (string) ($aggregate['order']['payment_status'] ?? ''), + ]); + } } catch (Throwable $exception) { $result['failed'] = (int) $result['failed'] + 1; $errors = is_array($result['errors']) ? $result['errors'] : [];