2026-07-17 — XP-140 apply-price-change loop → set-based UPDATE
What shipped
Section titled “What shipped”packages/service/src/pricing/apply-price-change.ts— the D3 APPEND branch ofapplyPerSettlementAdjustmentlooped one UPDATE per settlement row per side (2N write round-trips inside the tx). Replaced both per-row loops (student + tutor) with one set-basedUPDATE … WHERE lesson_session_id IN (sesiIds)per side, arithmetic done in-SQL:adjustment_amount_idr = adjustment_amount_idr + delta, net recomputed perdomain-billing-period.md. APPEND path is now 1 guard SELECT + 2 UPDATEs regardless of N.- New assert in
apply-price-change-batch.integration.test.tson the isolated SD_6 axis: 2 delivered sesi → 2 student + 2 tutor settlements → one batch bumps all four cells; every row’s adjustment + net checked exactly.
Key decisions
Section titled “Key decisions”deltais constant across affected rows and nothing (lock/audit) interleaves per-row — audit fires once in the caller — so the loop was pure round-trip waste, safe to collapse. Row set provably identical to the loop’s per-roweq(id, s.id)within the same tx-scoped SELECT.- LOCKED-immutability guard unchanged (still throws → whole-tx rollback); the guard SELECT was narrowed to only
lesson_session_id+billing_period_statussince the DB now does the arithmetic.
Gotchas / lessons
Section titled “Gotchas / lessons”- Postgres evaluates every SET RHS against the OLD row, so referencing
adjustment_amount_idrinside the net expression yields the pre-increment value:net = gross + (old_adj + delta) = gross + new_adj. An inline comment documents this so a future reader won’t “fix” it into a self-referential read. Proven by the new test’s non-degenerate tutor cell (transport 5k, pph 1k → net 59_000), not just by reasoning.
Reference changes
Section titled “Reference changes”None. No canonical doc (plans/ARCHITECTURE.md, plans/scope/*.md, .claude/rules/*.md) changed.