feat(119): protect total_paid from re-import overwrite

OrderImportRepository::updateOrderDelta() przechodzi na dynamic SET builder.
total_paid jest dolaczane do UPDATE tylko gdy payment_status realnie sie
zmienia; is_canceled_by_buyer analogicznie, ale z override przez
cancelledBySource (cancel ze zrodla nadal propaguje sie do bazy).

Chroni reczne korekty operatora (zwroty czesciowe) przed cichym
nadpisaniem z payloadu zrodla przy kolejnym sync. Incydent #976:
operator zwrocil klientowi 28,00 PLN obnizajac total_paid 119->91,
co bez tej zmiany byloby cofniete przez kolejny re-import shoppro.

Boundaries: identical-payload guard, paymentTransition, statusOverwriteAllowed,
cancel propagation (status_code='anulowane') - bez zmian.

Tests: tests/Unit/OrderImportRepositoryTest.php - 3 scenariusze
(preserve / transition / cancel propagation) via Reflection + sqlite
in-memory. PHPUnit run odroczony (vendor/ gitignored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 14:57:04 +02:00
parent bcbb35bc6b
commit 3a2c419c25
4 changed files with 578 additions and 18 deletions

View File

@@ -0,0 +1,143 @@
---
phase: 119-reimport-total-paid-protection
plan: 01
subsystem: orders
tags: [reimport, idempotency, payment, refund, delta-update]
requires:
- phase: 112-reimport-data-protection
provides: delta-only updateOrderDelta + identical-payload guard
- phase: 111-payment-transition-event
provides: paymentTransition detection (0/1 -> 2)
provides:
- total_paid protection when payment_status unchanged
- is_canceled_by_buyer cancel-propagation override at field level
- dynamic SQL SET builder in updateOrderDelta
affects: [orders re-import, manual payment corrections, refund flow, accounting]
tech-stack:
added: []
patterns:
- "Dynamic SET fragment builder for partial column updates (PHP array + implode)"
- "Boolean-flag-driven conditional column inclusion in UPDATE"
key-files:
created:
- tests/Unit/OrderImportRepositoryTest.php
modified:
- src/Modules/Orders/OrderImportRepository.php
- .paul/codebase/architecture.md
- .paul/codebase/tech_changelog.md
key-decisions:
- "total_paid chronione gdy oldPaymentStatus === newPaymentStatus (any value, not only 2->2)"
- "is_canceled_by_buyer chronione analogicznie, ale cancelledBySource override wymusza wpis"
- "Brak backfillu - fix forward (order #976 poprawiony recznie)"
- "Test flat path tests/Unit/ zamiast nested - match konwencji"
patterns-established:
- "Dynamic SET builder: $setFragments[] + implode(', ', ...) zamiast statycznego SQL z NULL-bindem (NULL nadpisuje, NIE pomija)"
- "Conditional column inclusion poprzez flagi boolean wyliczane w upsertOrderAggregate i przekazywane do delta method"
duration: ~20min
started: 2026-05-12T14:00:00Z
completed: 2026-05-12T14:20:00Z
---
# Phase 119 Plan 01: Re-import total_paid Protection Summary
**`updateOrderDelta()` chroni `total_paid` (i pomocniczo `is_canceled_by_buyer`) przed nadpisaniem z payloadu zrodla gdy `payment_status` nie ulega zmianie - reczne korekty operatora (zwroty czesciowe) przezywaja re-import.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~20 min |
| Started | 2026-05-12T14:00:00Z |
| Completed | 2026-05-12T14:20:00Z |
| Tasks | 3 completed |
| Files modified | 4 (1 source, 1 test, 2 docs) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: total_paid preserved when payment_status unchanged | Pass (code + test) | SQL builder pomija `total_paid` gdy `$paymentStatusUnchanged=true`. Test napisany. |
| AC-2: total_paid updated on payment transition | Pass (code + test) | Gdy `payment_status` rozni sie (np. 0->2), fragment SET dolaczany. Test napisany. |
| AC-3: is_canceled_by_buyer propagated on source cancel | Pass (code + test) | Drugi warunek (`!$paymentStatusUnchanged || $cancelledBySource`) wymusza wpis flagi przy cancel ze zrodla nawet przy stabilnym `payment_status`. |
| AC-4: Test coverage | Partial | 3 testy napisane, syntax-checked (`php -l`). PHPUnit run odroczony - `vendor/` nieobecny, composer poza PATH. |
## Accomplishments
- Order #976 (i przyszle przypadki recznych korekt `total_paid`) chronione przed nadpisaniem przy kolejnym sync shoppro.
- `updateOrderDelta()` przeszedl ze static SQL na czytelny dynamic builder - latwo dodawac kolejne warunkowe kolumny w przyszlosci.
- Cancel propagation ze zrodla pozostaje silnym kontraktem - flaga `is_canceled_by_buyer` zawsze trafia do bazy gdy zrodlo anuluje, niezaleznie od ruchu `payment_status`.
## Task Commits
Atomic commits TBD (commit po UNIFY zgodnie z transition workflow).
| Task | Type | Description |
|------|------|-------------|
| Task 1: Dynamic SET builder | feat | `updateOrderDelta()` warunkowe `total_paid` + `is_canceled_by_buyer` |
| Task 2: Test PHPUnit | test | 3 testy via Reflection + sqlite in-memory |
| Task 3: Docs update | docs | `architecture.md` + `tech_changelog.md` Phase 119-01 |
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `src/Modules/Orders/OrderImportRepository.php` | Modified | `upsertOrderAggregate` wylicza `$paymentStatusUnchanged`; `updateOrderDelta()` dynamic SET builder z warunkowym `total_paid` i `is_canceled_by_buyer` |
| `tests/Unit/OrderImportRepositoryTest.php` | Created | 3 testy PHPUnit pokrywajace AC-1/2/3 (sqlite + ReflectionMethod) |
| `.paul/codebase/architecture.md` | Modified | Sekcja Re-import rozszerzona o akapit Phase 119-01 |
| `.paul/codebase/tech_changelog.md` | Modified | Nowy wpis 2026-05-12 Phase 119 Plan 01 |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| `paymentStatusUnchanged` definiowany jako `oldPaymentStatus === newPaymentStatus` (any value) | Konserwatywne ograniczenie do `2 == 2` byloby surprising; "any value" jest spojne z intencja "nie ruszaj gdy nic sie nie zmienilo" | Operator moze recznie ustawic `total_paid` przy dowolnym `payment_status` (np. czesciowa platnosc 1) i recznie ja korygowac |
| `is_canceled_by_buyer` chronione **ale** override przez `cancelledBySource` | Bez override blokowalibysmy propagacje anulowania ze zrodla - to bylby regres wzgledem Phase 112-01 | Cancel ze zrodla nadal flaguje zamowienie i ustawia `status_code='anulowane'` |
| Brak CLI backfillu | Order #976 poprawiony recznie; inne przypadki ad-hoc; plan maly i skupiony | Mozliwe stare zamowienia z "uszkodzonym" `total_paid` - dopuszczalne ryzyko |
| Test flat path (`tests/Unit/`) zamiast nested (`tests/Unit/Modules/Orders/`) | Match istniejacej konwencji projektu (wszystkie testy plasko) | Spojnosc struktury testow |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | - |
| Scope additions | 0 | - |
| Deferred | 1 | PHPUnit run odroczony do srodowiska z `composer install` |
**Total impact:** Brak scope creep. Jedna deviation srodowiskowa (vendor/ gitignored).
### Deferred Items
- **PHPUnit run** - `vendor/` nieobecne, composer poza PATH. Test napisany i syntax-checked (`php -l`). Manual command po `composer install`: `vendor/bin/phpunit tests/Unit/OrderImportRepositoryTest.php`. Analogiczna sytuacja do gapow w Phase 116/117 (sonar-scanner).
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| PHPUnit nieuruchamialny (brak vendor/) | Code + test napisany, syntax check przez `php -l`. Run odroczony. Pattern z Phase 117 (manual verification deferred). |
| Plan zakladal nested test path | Wybrano flat path - match konwencji projektu (wszystkie istniejace testy w `tests/Unit/` plasko, namespace `Tests\Unit`) |
## Next Phase Readiness
**Ready:**
- `OrderImportRepository::updateOrderDelta()` chroni reczne korekty `total_paid` przy kolejnych syncach.
- Pattern dynamic SET buildera dostepny dla przyszlych warunkowych UPDATE-ow.
**Concerns:**
- Brak realnego potwierdzenia testem zielonym (vendor unavailable). Operator powinien uruchomic `composer install` + phpunit przed produkcyjnym push.
- Brak manualnego smoke testu na zywym shoppro - re-sync order #976 powinien potwierdzic, ze `total_paid=91.00` przetrwa.
**Blockers:** None.
---
*Phase: 119-reimport-total-paid-protection, Plan: 01*
*Completed: 2026-05-12*