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:
@@ -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*
|
||||
Reference in New Issue
Block a user