Skip to content

ADR 025 Payment-Change Secure Handoff

Legacy payment-change UI posted reservation id and amount, while public API accepted mother identity, payment id, and provider name. Ownership was checked after mutable unpaid rows were replaced. Existing atomic Supabase handoff logic was unreachable from HTTP and reused pre-reservation authorization terminology.

Public intent contains only selected payment method plus Idempotency-Key. Supabase Auth derives payer. Backend reads latest adjustment and booking context, then derives frozen amount, mother, reservation state, branch, product plan, Kill Bill route, payment id, revisions, return route, expiry, and evidence.

PAY_PAYMENT_CHANGE is distinct from PAY_PRE_RESERVATION. Reservation stage policy decides when mother may use payment-change handoff; SpiceDB verifies the reservation pay_payment_change permission. Supabase RPC atomically persists payment record, handoff session, adjustment revision, and authorization audit.

Exact replay reconstructs original expected revision and issue time from frozen pre-registration, skips a second provider call, and still reaches RPC replay validation. Changed payment or idempotency facts conflict without another provider call.

Replacement of an unpaid adjustment uses one service-role Supabase RPC under a booking advisory lock. It verifies any prior provider payment remains waiting, marks old handoff session superseded, cancels old adjustment, inserts new adjustment, and appends PaymentChangeSupersessionFact in one transaction. Paid, failed, or unresolved provider facts reject replacement and move to the provider reconciliation/compensation workflow.

Backend stage run created booking 9c19e1c3-9b64-41b3-bb2b-67517e08920a, adjustment 665ff856-a041-4b51-be40-b4c12165ffac, and backend payment id payment-change:665ff856-a041-4b51-be40-b4c12165ffac. It proved forged legacy fields rejected, backend amount/provider/payment identity, exact replay, non-mother denial, Supabase/SpiceDB authorization, and registered-data cleanup.

Follow-up stage run created booking bcf1db59-9c44-47c1-aa6e-6aacafd79cba, superseded adjustment c5d89751-1544-4b32-900f-86e112dedfe2, and replacement e13e961e-6ab4-4245-8e1b-cf2fe8c25483. It proved atomic old-cancel/new-active states, append-only supersession fact, old handoff superseded, retained waiting payment evidence, and registered DB/SpiceDB cleanup.

Backend affected gate passed 138 tests, Ruff, Tach, Supabase lint, generated types, and live schema bootstrap. Contract gate passed 396 tests.

Browser cannot redirect provider routing, impersonate mother, change amount, or choose payment identity. Payment-change state remains independently extensible from pre-reservation payment. Provider calls are not duplicated on HTTP replay.