--- phase: 112-reimport-data-protection plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/Modules/Orders/OrderImportRepository.php - .paul/codebase/architecture.md - .paul/codebase/tech_changelog.md autonomous: true delegation: off --- ## Goal Re-import istniejacego zamowienia (Allegro + shopPRO) ma byc delta-only: nie kasuje pozycji/adresow/notatek i nie nadpisuje pol zamowienia, ktore nie zmieniaja sie ze zrodla. Aktualizujemy wylacznie stan oplaty, anulowanie i znaczniki synchronizacji. Pierwszy import (`created=true`) zostaje bez zmian. ## 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`, `replaceNotes` wywolywane tylko przy `created=true`. - Nowa wewnetrzna metoda `updateOrderDelta()` (lub zaweznie `updateOrder()`) 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=1` LUB zmapowany pull `status_code` ze zrodla = `anulowane`) -> `orders.status_code` ustawiony na `anulowane` niezaleznie od `statusOverwriteAllowed`. - Guard na identyczny payload: jezeli znormalizowany `payload_json` ze zrodla jest identyczny z `orders.payload_json` w DB, re-import calkowicie pomija UPDATE (no-op). - Aktualizacja `.paul/codebase/architecture.md` (sekcja "Re-import") oraz wpis do `.paul/codebase/tech_changelog.md`. - **[Zakres skip]** — Co dokladnie pomijac w re-imporcie istniejacego zamowienia (DELETE+INSERT zostaje tylko przy pierwszym imporcie)? → Odpowiedz: order_items, order_addresses, order_notes (wszystkie trzy) - **[updateOrder]** — Co z `updateOrder`? Obecnie aktualizuje wszystkie kolumny zamowienia przy re-imporcie. → Odpowiedz: Zawezic do listy delta-only (payment_status, total_paid, status_code warunkowo, is_canceled_by_buyer, source_updated_at, payload_json, fetched_at) - **[Anulowanie]** — Anulowanie zamowienia ze zrodla (np. Allegro is_canceled_by_buyer=1) — czy ten plan ma rozszerzyc logike re-importu o przepuszczenie anulowania? → Odpowiedz: TAK — objac w tym planie (re-import anulowanego zamowienia ustawia status_code=anulowane niezaleznie od statusOverwriteAllowed) - **[Backfill 882]** — Backfill zamowienia #882 — gdzie? → Odpowiedz: Pominac — uzytkownik zrobi sam (poza zakresem planu) - **[Anulowanie scope]** — Czy bazowac tylko na `is_canceled_by_buyer`, czy szerzej? → Odpowiedz: Szerzej — jezeli zamowienie zostalo anulowane w zrodle (rownoczesnie sprawdzamy zmapowany pull `status_code` ze zrodla; jesli wskazuje `anulowane`, propagujemy) - **[Payments przy paymentTransition]** — Czy zostawic obecny `replacePayments` (DELETE+INSERT) czy zmienic na append-only? → Odpowiedz: Niezdecydowane — w tym planie zostawiamy bez zmian (existing behavior z Phase 111), ryzyko nadpisania recznie dodanych platnosci (Phase 56) odnotowane jako deferred issue. - **[Identyczny payload]** — Czy pomijac update gdy payload identyczny? → Odpowiedz: TAK — guard porownujacy payload_json przed wykonaniem updateOrderDelta() ## 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 ## AC-1: Re-import nie kasuje pozycji ```gherkin 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 ```gherkin 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 ```gherkin 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 ```gherkin 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 ```gherkin 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 ```gherkin 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 ```gherkin 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 ``` 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 eventu `payment.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 po `order_items.id`, plan zapewnia stabilnosc id. - `replacePayments`, `replaceShipments`, `replaceStatusHistory` — istniejaca logika wywolywania (tylko przy `created` lub `paymentTransition`/`statusOverwriteAllowed` dla 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.php` zostaje). - 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_at` na `order_items`/`order_addresses`/`order_notes` — przy delta-only te wiersze sa nietkniete, wiec ich `updated_at` z 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). - `replacePayments` przy `paymentTransition || statusOverwriteAllowed` zostaje 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. Przed declaracja planu jako kompletnego: - [ ] `php -l src/Modules/Orders/OrderImportRepository.php` -> brak bledow - [ ] Recznie zweryfikowane AC-1 (order_items.id stable + project_generated zachowane po re-imporcie) - [ ] Recznie zweryfikowane AC-3 (status_code preservowany przy re-imporcie bez statusOverwriteAllowed) - [ ] Recznie zweryfikowane AC-5 (payment.status_changed event nadal sie emituje przy 0/1->2) - [ ] Recznie zweryfikowane AC-6 (is_canceled_by_buyer=1 LUB pull status_code='anulowane' ze zrodla -> orders.status_code='anulowane') - [ ] Recznie zweryfikowane AC-7 (identyczny payload -> brak UPDATE, fetched_at/updated_at niezmienione) - [ ] Architecture.md i tech_changelog.md zaktualizowane - [ ] Brak nowych natywnych alert()/confirm() w widokach (nie ruszamy widokow w tym planie, ale check dla pewnosci) - Wszystkie 3 taski wykonane. - AC-1 do AC-7 spelnione. - Po re-imporcie zamowienia z `project_generated=1` flaga "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_transition` z Phase 111 (case #864 nadal naprawiony). Po zakonczeniu utworzyc `.paul/phases/112-reimport-data-protection/112-01-SUMMARY.md` zawierajace: - co zostalo zmienione (lista plikow + 1-zdaniowe podsumowanie kazdej zmiany) - jak zweryfikowano AC-1..AC-6 - ewentualne deferred issues - link do tego PLAN.md