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>
319 lines
19 KiB
Markdown
319 lines
19 KiB
Markdown
---
|
|
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>
|