Phase 112 / Plan 112-01 complete (v3.6): - OrderImportRepository::upsertOrderAggregate split into create vs re-import paths - replaceAddresses/Items/Notes/Shipments/StatusHistory invoked only on first import - new updateOrderDelta() narrows UPDATE to status_code (cond.), payment_status, total_paid, is_canceled_by_buyer, source_updated_at, payload_json, fetched_at - source-side cancellation override (is_canceled_by_buyer=1 OR pull status_code='anulowane') - identical-payload no-op guard via normalizePayloadJson() - fixes case #882: order_items.id stable, project_generated (Phase 97) preserved - Phase 111 payment.status_changed emit retained without regression Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
| phase | plan | type | wave | depends_on | files_modified | autonomous | delegation | |||
|---|---|---|---|---|---|---|---|---|---|---|
| 112-reimport-data-protection | 01 | execute | 1 |
|
true | off |
Purpose
Bug case #882: po re-imporcie produkty wyswietlaja "Brak projektu" mimo wczesniej wygenerowanych projektow. Przyczyna: replaceItems() robi DELETE+INSERT, czego skutkiem jest reset order_items.project_generated/project_generated_at i zmiana order_items.id (lamie referencje skryptu generowania, ktory updatuje po id). Re-import jest aktualnie inicjowany m.in. przez nowy event payment.status_changed z Phase 111, wiec problem stal sie regularny.
Output
- Zmodyfikowany
OrderImportRepository::upsertOrderAggregate():replaceAddresses,replaceItems,replaceNoteswywolywane tylko przycreated=true. - Nowa wewnetrzna metoda
updateOrderDelta()(lub zaweznieupdateOrder()) aktualizujaca tylko:status_code(warunkowo),payment_status,total_paid,is_canceled_by_buyer,source_updated_at,payload_json,fetched_at,updated_at. - Logika przepuszczenia anulowania: gdy zamowienie zostalo anulowane w zrodle (
is_canceled_by_buyer=1LUB zmapowany pullstatus_codeze zrodla =anulowane) ->orders.status_codeustawiony naanulowaneniezaleznie odstatusOverwriteAllowed. - Guard na identyczny payload: jezeli znormalizowany
payload_jsonze zrodla jest identyczny zorders.payload_jsonw DB, re-import calkowicie pomija UPDATE (no-op). - Aktualizacja
.paul/codebase/architecture.md(sekcja "Re-import") oraz wpis do.paul/codebase/tech_changelog.md.
Project Context
@.paul/PROJECT.md @.paul/STATE.md @.paul/codebase/architecture.md
Prior Work
@.paul/phases/111-payment-transition-event/111-01-SUMMARY.md
Phase 111 dodal payment_transition flag w upsertOrderAggregate() i emit eventu payment.status_changed z AllegroOrderImportService / ShopproOrdersSyncService. Logika ta MUSI dalej dzialac — tylko zaweza sie zakres operacji wykonywanych przy re-imporcie.
@.paul/phases/97-project-generation/97-01-SUMMARY.md
Phase 97 dodala kolumny order_items.project_generated, project_generated_at. Skrypt batch (tools/generowanie/_batch_run.sh) updatuje te kolumny po id. Stabilnosc order_items.id jest warunkiem poprawnego dzialania flow.
Source Files
@src/Modules/Orders/OrderImportRepository.php @src/Modules/Settings/AllegroOrderImportService.php @src/Modules/Settings/ShopproOrdersSyncService.php @resources/views/orders/show.php @tools/generowanie/_batch_run.sh
<acceptance_criteria>
AC-1: Re-import nie kasuje pozycji
Given zamowienie istnieje w DB i ma order_items z project_generated=1, project_generated_at != NULL
When OrderImportRepository::upsertOrderAggregate() jest wywolane dla tego zamowienia (created=false)
Then liczba wierszy w order_items dla tego zamowienia jest niezmieniona
And order_items.id pozostaja te same
And order_items.project_generated oraz project_generated_at pozostaja niezmienione
AC-2: Re-import nie kasuje adresow ani notatek
Given zamowienie istnieje w DB z wierszami w order_addresses i order_notes
When OrderImportRepository::upsertOrderAggregate() jest wywolane dla tego zamowienia (created=false)
Then liczba i tresc wierszy w order_addresses dla tego order_id pozostaje bez zmian
And liczba i tresc wierszy w order_notes dla tego order_id pozostaje bez zmian
AC-3: updateOrder zawezony do listy delta-only
Given zamowienie istnieje w DB z status_code='w_realizacji' (recznie zmienionym po imporcie) i payment_status=2
When re-import dostarcza payload z payment_status=2 i status_code='nieoplacone' ze zrodla (statusOverwriteAllowed=false, paymentTransition=false)
Then orders.status_code dla zamowienia pozostaje 'w_realizacji' (logika preservacji status_code z Phase 62 dziala)
And orders.payment_status, total_paid, source_updated_at, payload_json, fetched_at, updated_at sa zaktualizowane
And orders.delivery_price, send_date_min, send_date_max, ordered_at, customer_login NIE sa nadpisywane przez re-import
AC-4: Pierwszy import (created=true) bez regresji
Given zamowienie nie istnieje w DB (findOrderIdBySource zwraca null)
When OrderImportRepository::upsertOrderAggregate() jest wywolane
Then INSERT INTO orders + replaceAddresses + replaceItems + replaceNotes + replacePayments + replaceShipments + replaceStatusHistory wykonuja sie
And wszystkie pola zamowienia zapisuja sie tak jak przed planem 112-01
AC-5: Re-import emituje payment.status_changed bez regresji
Given zamowienie istnieje z payment_status=0 lub 1
When re-import dostarcza payload z payment_status=2
Then upsertOrderAggregate() zwraca payment_transition=true
And AllegroOrderImportService / ShopproOrdersSyncService emituja event 'payment.status_changed'
And chain reguly automatyzacji #7 zmienia status_code na 'w_realizacji' (logika z Phase 111 dziala)
AC-6: Re-import propaguje anulowanie ze zrodla
Given zamowienie istnieje z status_code='w_realizacji' i is_canceled_by_buyer=0
When re-import dostarcza payload, w ktorym is_canceled_by_buyer=1 LUB zmapowany pull status_code = 'anulowane'
Then orders.status_code zostaje ustawiony na 'anulowane' (override niezalezny od statusOverwriteAllowed)
And jezeli is_canceled_by_buyer=1 ze zrodla -> orders.is_canceled_by_buyer = 1
AC-7: Identyczny payload jest no-op
Given zamowienie istnieje w DB z payload_json = X
When re-import dostarcza identyczny znormalizowany payload (X)
Then upsertOrderAggregate() nie wykonuje UPDATE na orders ani na zadnej powiazanej tabeli
And zwraca payment_transition=false, created=false
And orders.updated_at oraz fetched_at NIE zmieniaja sie
</acceptance_criteria>
Task 1: Rozdzielic ścieżkę create vs update w upsertOrderAggregate() src/Modules/Orders/OrderImportRepository.php W metodzie `upsertOrderAggregate()` (linie ~25-87) przeniesc wywolania `replaceAddresses`, `replaceItems`, `replaceNotes` pod galaz `if ($created)` razem z istniejacymi `replacePayments`, `replaceShipments`, `replaceStatusHistory`.Po zmianie struktura ma byc:
- `if ($created)` -> insertOrder + replaceAddresses + replaceItems + replaceNotes + replacePayments + replaceShipments + replaceStatusHistory
- `else` -> updateOrderDelta() (nowa metoda, patrz Task 2). replacePayments wykonywane jak dzis tylko przy `$paymentTransition || $statusOverwriteAllowed`.
Zachowac dotychczasowy:
- blok wyznaczania `$paymentTransition` i `$statusOverwriteAllowed` (linie 44-56)
- logika preservacji `status_code` przy `!$statusOverwriteAllowed` (linia 53-55)
- return shape `['order_id' => ..., 'created' => ..., 'payment_transition' => ...]`
- obsluga transakcji (beginTransaction/commit/rollBack)
Pliku NIE wolno przerabiac na inne wzorce (medoo wrapper, ORM) — zostaje czysty PDO + prepared statements zgodnie z CLAUDE.md.
Wizualna inspekcja `git diff src/Modules/Orders/OrderImportRepository.php` — `replaceAddresses/replaceItems/replaceNotes` widoczne tylko w galezi `if ($created)`.
Test recznie: w lokalnym MySQL dla zamowienia z istniejacymi pozycjami uruchomic re-import (np. ponowne pobranie shopPRO/Allegro), nastepnie sprawdzic `SELECT COUNT(*), MIN(id), MAX(id) FROM order_items WHERE order_id=:id` przed i po — wartosci niezmienione.
AC-1, AC-2, AC-4 spelnione
Task 2: Dodac updateOrderDelta() i propagacje anulowania
src/Modules/Orders/OrderImportRepository.php
Dodac prywatna metode `updateOrderDelta(int $orderId, array $orderData): int` aktualizujaca tylko nastepujace kolumny w tabeli `orders`:
- `status_code` (przekazany juz po preservacji z linii 53-55, wiec respektuje istniejaca logike)
- `payment_status`
- `total_paid`
- `is_canceled_by_buyer`
- `source_updated_at`
- `payload_json`
- `fetched_at`
- `updated_at = NOW()`
Wszystkie inne kolumny (`integration_id`, `source`, `external_*`, `customer_login`, `currency`, `total_without_tax`, `total_with_tax`, `delivery_price`, `send_date_*`, `ordered_at`, `source_created_at`, `preferences_json`, `is_invoice`, `is_encrypted`, `external_carrier_*`, `external_payment_type_id`) NIE sa aktualizowane przy re-imporcie.
Logika anulowania (przed wywolaniem updateOrderDelta, w upsertOrderAggregate ramach galezi else):
- Wykryj anulowanie ze zrodla: warunek `!empty($orderData['is_canceled_by_buyer']) || (string)($orderData['status_code'] ?? '') === 'anulowane'`.
- Pull mapper statusow Allegro/shopPRO juz przeklada zewnetrzny status na `orderData['status_code']`, wiec wartosc 'anulowane' jest dostepna na tym etapie (Phase 75/83 — pull mapping).
- Gdy warunek spelniony -> wymus `$orderData['status_code'] = 'anulowane'` (override niezalezny od `statusOverwriteAllowed`).
- Override musi byc nalozony PO bloku preservacji status_code (linia 53-55), tak aby anulowanie mialo pierwszenstwo nad preservacja.
Guard na identyczny payload (na samym poczatku galezi else, przed wszystkimi UPDATE):
- Pobierz aktualny `orders.payload_json` z DB jednym SELECT (mozna rozszerzyc `getCurrentStatusAndPaymentStatus` lub osobne zapytanie).
- Znormalizuj oba payloady przez `json_decode` -> `json_encode` (z `JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES`) i porownaj stringi (deterministyczna serializacja po reszerializacji).
- Jezeli identyczne -> wykonaj `commit()` i zwroc `['order_id' => $existingOrderId, 'created' => false, 'payment_transition' => false]` bez wywolywania `updateOrderDelta()`.
- W ten sposob `fetched_at`, `updated_at` oraz wszystkie powiazane tabele pozostaja nietkniete.
Zostawic istniejaca metode `updateOrder()` jako dead code do usuniecia w tym samym commicie (lub usunac od razu — jest uzywana tylko w jednym miejscu).
Uzywac prepared statements + medoo style array binding zgodnie z reszta klasy.
1. PHP syntax: `php -l src/Modules/Orders/OrderImportRepository.php` -> "No syntax errors detected".
2. Recznie: zaimportowac zamowienie, w UI orderPRO zmienic `status_code` na `w_realizacji`, wymusic re-import (np. cron Allegro) z payload zawierajacym pierwotny status. Sprawdzic w DB ze `orders.status_code` pozostal `w_realizacji`.
3. Recznie: dla zamowienia testowego w sandbox/dev podstawic payload z `is_canceled_by_buyer=1` i wymusic re-import. Sprawdzic ze `orders.status_code='anulowane'` i `is_canceled_by_buyer=1`.
4. Recznie: dla zamowienia testowego, w ktorym pull mapping zewnetrznego statusu daje 'anulowane' (bez `is_canceled_by_buyer=1`), wymusic re-import. Sprawdzic ze `orders.status_code='anulowane'`.
5. Recznie: re-import zamowienia z identycznym payloadem (np. dwa razy pod rzad bez zmian po stronie zrodla) — `orders.fetched_at` i `orders.updated_at` nie zmieniaja sie.
AC-3, AC-5, AC-6, AC-7 spelnione
Task 3: Aktualizacja dokumentacji architektury i changelog
.paul/codebase/architecture.md, .paul/codebase/tech_changelog.md
1. W `.paul/codebase/architecture.md` w sekcji "Order Lifecycle" zaktualizowac punkt 2 ("Re-import (Phase 111)") o nowy zakres delta-only:
- replaceAddresses/replaceItems/replaceNotes wykonywane tylko przy pierwszym imporcie (`created=true`)
- re-import istniejacego zamowienia aktualizuje wylacznie `status_code` (warunkowo, z propagacja anulowania), `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`
- logika `payment_transition` (Phase 111) i preservacji status_code (Phase 62) pozostaje rozdzielona i dziala jak wczesniej
- referencja do tego planu (Phase 112-01)
2. W `.paul/codebase/tech_changelog.md` dodac wpis (na poczatku, w stylu istniejacych wpisow):
- data: 2026-05-07
- tytul: "Phase 112-01 — Re-import data protection"
- krotki opis: skip replace dla items/addresses/notes przy re-imporcie, zawezony updateOrderDelta(), propagacja anulowania ze zrodla
- bug context: case #882 — projekty znikaly z UI po re-imporcie wymuszanym przez payment.status_changed event
NIE zmieniac db_schema.md (brak zmian schematu).
Wizualna inspekcja diff obu plikow — wpisy spojne z reszta dokumentacji (jezyk, formatowanie, daty).
Dokumentacja odzwierciedla nowe zachowanie re-importu
DO NOT CHANGE
database/migrations/**— brak zmian schematu w tym planie.src/Modules/Settings/AllegroOrderImportService.php,src/Modules/Settings/ShopproOrdersSyncService.php— emit eventupayment.status_changed(Phase 111) pozostaje bez zmian. Plan 112-01 dziala "pod" tymi serwisami w warstwie repository.tools/generowanie/_batch_run.sh— skrypt batch dziala poorder_items.id, plan zapewnia stabilnosc id.replacePayments,replaceShipments,replaceStatusHistory— istniejaca logika wywolywania (tylko przycreatedlubpaymentTransition/statusOverwriteAlloweddla payments) bez zmian.- Logika
findOrderIdBySource,getCurrentStatusAndPaymentStatus,insertOrder, blok preservacji status_code z Phase 62, blok payment_transition z Phase 111.
SCOPE LIMITS
- Brak backfillu zamowienia #882 ani innych — robi to operator recznie po wdrozeniu (decyzja uzytkownika).
- Brak zmian w UI (
resources/views/orders/show.phpzostaje). - Brak zmian w
OrdersRepository,AutomationService,OrdersController. - Brak diff/UPSERT po kluczu naturalnym (
source_item_id/external_item_id) — wariant odrzucony, jesli kiedys zrodlo bedzie modyfikowac pozycje, to osobny plan. - Plan nie obejmuje
update_atnaorder_items/order_addresses/order_notes— przy delta-only te wiersze sa nietkniete, wiec ichupdated_atz definicji nie zmieni sie. - Brak nowych testow PHPUnit w tym planie (projekt ma testy unit dla AllegroOrderImportService; ten plan jest zmiana w warstwie repository i weryfikacja jest manualna na bazie).
replacePaymentsprzypaymentTransition || statusOverwriteAllowedzostaje bez zmian (DELETE+INSERT). Ryzyko nadpisania recznych platnosci dodanych w UI (Phase 56) — odlozone jako deferred issue, do oceny po wdrozeniu 112-01 i obserwacji.
<success_criteria>
- Wszystkie 3 taski wykonane.
- AC-1 do AC-7 spelnione.
- Po re-imporcie zamowienia z
project_generated=1flaga "Projekt" w/orders/{id}pozostaje widoczna (po recznym backfillu zamowienia 882 — poza zakresem planu). - Brak regresji w pierwszym imporcie (Allegro + shopPRO).
- Brak regresji w mechanizmie
payment_transitionz Phase 111 (case #864 nadal naprawiony). </success_criteria>