feat(111): payment transition event for Allegro+shopPRO re-import

Re-import zamowienia 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 dla zamowien
  payment_status=2 && status_code='nieoplacone' (Allegro + shopPRO)
- Naprawa luki znalezionej w analizie zamowienia #864

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 23:35:14 +02:00
parent 0da0241ebf
commit 5cf531d718
12 changed files with 691 additions and 31 deletions

View File

@@ -12,9 +12,9 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
| Attribute | Value | | Attribute | Value |
|-----------|-------| |-----------|-------|
| Version | 3.4.0 | | Version | 3.5.0 |
| Status | v3.4 shipped - Statistics Summary complete | | Status | v3.5 shipped - Payment Transition Event hotfix complete |
| Last Updated | 2026-04-28 | | Last Updated | 2026-05-05 |
## Requirements ## 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] 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] 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] 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 ### Deferred

View File

@@ -6,7 +6,7 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
## Current Milestone ## Current Milestone
Brak aktywnego milestone - v3.4 zamkniety. Nastepny milestone do zaplanowania. Brak aktywnego milestone - v3.5 zamkniety. Nastepny milestone do zaplanowania.
## Next Milestone ## Next Milestone
@@ -19,6 +19,19 @@ Kandydaci w kolejce:
## Completed Milestones ## Completed Milestones
<details>
<summary>v3.5 Payment Transition Event - 2026-05-05 (1 phase, 1 plan)</summary>
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/`
</details>
<details> <details>
<summary>v3.4 Statistics Summary - 2026-04-28 (1 phase, 1 plan)</summary> <summary>v3.4 Statistics Summary - 2026-04-28 (1 phase, 1 plan)</summary>

View File

@@ -9,34 +9,33 @@ See: .paul/PROJECT.md (updated 2026-04-28)
## Current Position ## Current Position
Milestone: v3.4 Statistics Summary - COMPLETE Milestone: v3.5 Payment Transition Event (hotfix) — COMPLETE
Phase: 110 of 110 - COMPLETE Phase: 111 of 111 (Payment Transition Event) — COMPLETE
Plan: 110-01 - COMPLETE Plan: 111-01 COMPLETE
Version: 3.4.0 Status: v3.5 shipped, awaiting transition (commit) i nastepny milestone
Status: v3.4 shipped - gotowy do nastepnego milestone Last activity: 2026-05-05 — UNIFY Phase 111 / Plan 111-01 complete
Last activity: 2026-04-28 - UNIFY Phase 110 / v3.4 milestone complete
Progress: 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 ## Loop Position
Current loop state: Current loop state:
``` ```
v3.4 milestone: v3.5 milestone:
Phase 110 (Statistics Summary): Phase 111 (Payment Transition Event):
Plan 110-01: PLAN done APPLY done UNIFY done Plan 111-01: PLAN done APPLY done UNIFY done
-> Phase 110 closed -> Phase 111 closed
-> v3.4 milestone closed -> v3.5 milestone closed (pending transition commit)
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-04-28 Last session: 2026-05-05
Stopped at: v3.4 milestone closed Stopped at: v3.5 milestone closed
Next action: /paul:milestone - wybor i zaplanowanie nastepnego milestone Next action: transition-phase (PROJECT/ROADMAP update + git commit), nastepnie /paul:milestone
Resume file: .paul/phases/110-statistics-summary/110-01-SUMMARY.md Resume file: .paul/phases/111-payment-transition-event/111-01-SUMMARY.md
## Git State ## Git State

View File

@@ -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`

View File

@@ -65,8 +65,9 @@ HTTP Request
### Order Lifecycle ### Order Lifecycle
1. **Import** — Cron handler → API client → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()` 1. **Import** — Cron handler → API client → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()`
2. **Status update**`OrdersController::updateStatus()` `OrdersRepository::updateStatus()` → automation check 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 sync**Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API 3. **Status update**`OrdersController::updateStatus()` `OrdersRepository::updateStatus()` → automation check
4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
### Statistics Summary ### Statistics Summary
1. **Request**`/statistics/summary``OrdersStatisticsController::summary()` 1. **Request**`/statistics/summary``OrdersStatisticsController::summary()`

View File

@@ -1,5 +1,20 @@
# Technical Changelog # 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 ## 2026-04-28 - Phase 110 Plan 01: Statistics Summary
**Co zrobiono:** **Co zrobiono:**

View File

@@ -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
---
<objective>
## 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`
</objective>
<context>
<clarifications>
- **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.
</clarifications>
## 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
</context>
<acceptance_criteria>
## 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)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Rozszerz OrderImportRepository o detekcje tranzycji 0/1 → 2</name>
<files>src/Modules/Orders/OrderImportRepository.php</files>
<action>
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).
</action>
<verify>
- `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/`).
</verify>
<done>AC-1 spelnione: payment_transition oparte o porownanie payment_status, status_code chroniony zgodnie z dotychczasowa logika preservacji.</done>
</task>
<task type="auto">
<name>Task 2: Emit payment.status_changed w Allegro + shopPRO import services</name>
<files>src/Modules/Settings/AllegroOrderImportService.php, src/Modules/Settings/ShopproOrdersSyncService.php</files>
<action>
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).
</action>
<verify>
- `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'.
</verify>
<done>AC-2 spelnione: oba serwisy importu emituja `payment.status_changed` na payment_transition. AC-4 zachowane: order.imported nadal gated.</done>
</task>
<task type="auto">
<name>Task 3: Backfill CLI dla istniejacych zamowien z luka platnosci</name>
<files>bin/backfill_payment_transition_111.php</files>
<action>
Stworz nowy plik wzorem `bin/backfill_shipped_status_98.php`:
```php
<?php
declare(strict_types=1);
require __DIR__ . '/../bootstrap/app.php';
/** @var App\Core\Application $app */
$app = require __DIR__ . '/../bootstrap/app.php';
$pdo = $app->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).
</action>
<verify>
- `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'
</verify>
<done>AC-3 spelnione: backfill naprawia historyczne dane idempotentnie.</done>
</task>
<task type="auto">
<name>Task 4: Aktualizacja dokumentacji codebase (architecture + tech_changelog)</name>
<files>.paul/codebase/architecture.md, .paul/codebase/tech_changelog.md</files>
<action>
`.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.
</action>
<verify>
Manualny przeglad obu plikow — sekcje istnieja, formatowanie spojne z reszta.
</verify>
<done>Dokumentacja codebase zsynchronizowana z kodem.</done>
</task>
</tasks>
<boundaries>
## 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)
</boundaries>
<verification>
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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
Po zakonczeniu UNIFY: `.paul/phases/111-payment-transition-event/111-01-SUMMARY.md`
</output>

View File

@@ -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*

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/**
* Backfill (Phase 111): zamowienia z payment_status=2 i status_code='nieoplacone'
* (source=allegro lub shoppro) zostaja przestawione na status 'w_realizacji'.
* Zmiana idzie przez OrdersRepository::updateOrderStatus, dzieki czemu powstaje
* wpis w order_status_history i order_activity_log.
*
* Naprawia luke z Phase 98 (gate $wasCreated dla order.imported) — zamowienia
* zaimportowane przed potwierdzeniem platnosci, ktore utknely w 'nieoplacone'
* mimo opłacenia.
*
* Idempotentny: drugie uruchomienie na czystej bazie nie zrobi nic.
*
* Flagi:
* --dry-run pokaz tylko liste kandydatow, bez zmian
* --use-remote uzyj DB_HOST_REMOTE zamiast DB_HOST (dla operacji recznych agenta)
*/
use App\Core\Database\ConnectionFactory;
use App\Core\Support\Env;
use App\Modules\Orders\OrdersRepository;
$basePath = dirname(__DIR__);
require $basePath . '/vendor/autoload.php';
Env::load($basePath . '/.env');
/** @var array<string, mixed> $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;

View File

@@ -39,12 +39,18 @@ final class OrderImportRepository
$existingOrderId = $this->findOrderIdBySource($source, $sourceOrderId); $existingOrderId = $this->findOrderIdBySource($source, $sourceOrderId);
$created = $existingOrderId === null; $created = $existingOrderId === null;
$paymentTransition = false; $paymentTransition = false;
$statusOverwriteAllowed = false;
if (!$created) { if (!$created) {
$currentStatus = $this->getCurrentStatus($existingOrderId); $existing = $this->getCurrentStatusAndPaymentStatus($existingOrderId);
$currentStatus = $existing['status_code'];
$oldPaymentStatus = $existing['payment_status'];
$newPaymentStatus = (int) ($orderData['payment_status'] ?? 0); $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; $orderData['status_code'] = $currentStatus;
} }
} }
@@ -61,7 +67,7 @@ final class OrderImportRepository
$this->replacePayments($orderId, $payments); $this->replacePayments($orderId, $payments);
$this->replaceShipments($orderId, $shipments); $this->replaceShipments($orderId, $shipments);
$this->replaceStatusHistory($orderId, $statusHistory); $this->replaceStatusHistory($orderId, $statusHistory);
} elseif ($paymentTransition) { } elseif ($paymentTransition || $statusOverwriteAllowed) {
$this->replacePayments($orderId, $payments); $this->replacePayments($orderId, $payments);
} }
@@ -101,15 +107,25 @@ final class OrderImportRepository
return $id > 0 ? $id : null; 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( $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]); $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),
];
} }
/** /**

View File

@@ -104,6 +104,16 @@ final class AllegroOrderImportService
'new_payment_status' => (string) ($mapped['order']['payment_status'] ?? ''), '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 [ return [

View File

@@ -278,6 +278,15 @@ final class ShopproOrdersSyncService
'new_payment_status' => (string) ($aggregate['order']['payment_status'] ?? ''), '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) { } catch (Throwable $exception) {
$result['failed'] = (int) $result['failed'] + 1; $result['failed'] = (int) $result['failed'] + 1;
$errors = is_array($result['errors']) ? $result['errors'] : []; $errors = is_array($result['errors']) ? $result['errors'] : [];