ADR 027 Payment Refund Settlement Reversal
Context
Section titled “Context”Legacy KCP refund paths treated provider response containers as success, changed local paid rows directly, and left settlement/reporting screens to infer refund impact from mutable payment rows. A provider refund could therefore mark payment state, settlement output, receipt visibility, or operator dashboards out of order.
Decision
Section titled “Decision”Provider refund success is an append-only provider result fact. Public/operator input cannot supply refund amount, paid-row revision, remaining paid amount, or settlement reversal values.
Backend derives refund amount from the immutable refund ledger and derives the
current paid-row revision from Supabase before any mutation. The paid-row
transition is recorded through one Supabase RPC that inserts the transition
decision and advances sanmopia_payment_records under expected-revision guard.
Rejected, stale, already-refunded, or mismatched results stay as auditable
decision facts and do not rewrite paid rows.
Applied refund transitions produce settlement reversal lines and provider-refund reporting invalidations from the same provider result event. Reporting refresh requests target sales dashboard, branch/HQ settlement statements, settlement export artifacts, and branch/HQ operator boards. UI surfaces may render only backend projections and must not recompute refund revenue or settlement reversal from payment-row status.
Evidence
Section titled “Evidence”2026-07-12 stage proof ran
pnpm supabase:smoke:stage:reservation-to-settlement-refund-impact. It first
settled positive-balance reservation booking
6a619c99-5fcf-446b-8f30-8fd03b835636, including service-balance amount
25000, disclosure readiness ready, branch settlement 100000, HQ settlement
50000, financial stage settled, and registered-batch cleanup with SpiceDB
cleanup verification.
The same run then recorded provider refund event
stage-provider-refund-result-20260712121644-1a6e3fa1 for payment
stage-provider-refund-payment-20260712121644-1a6e3fa1. It proved
/internal/provider-refund-results-from-ledger, ledger-derived refund amount,
paid-row status transition to partial_refunded, payment fact revision 1,
settlement reversal line persistence, six reporting invalidation rows, refresh
request queue state, and registered refund-batch cleanup.
2026-07-12 hosted callback proof ran
pnpm supabase:smoke:stage:killbill-refund-callback-to-settlement-reversal.
It seeded one paid customer-share balance payment
stage-killbill-refund-payment-20260712132758-6e829503, one immutable refund
ledger intent stage-killbill-refund-intent-20260712132758-6e829503, posted an
official-shape Kill Bill PAYMENT_SUCCESS / REFUND callback, verified atomic
inbox event
killbill-event:sha256:d0eae42c9d148f6d292ef50de640cb71f631bea830fb7a961271ac823adfed2b,
verified canonical duplicate replay did not requeue work, dispatched
sanmopia_payment_webhook_processing, archived queue message 10, recorded
provider refund result
killbill:PAYMENT_SUCCESS:stage-killbill-refund-payment-20260712132758-6e829503:stage-killbill-refund-tx-20260712132758-6e829503,
mutated the paid row to partial_refunded, persisted the settlement reversal
line, requested all six reporting refresh targets, and reaped the registered
stage batch.
Focused wrapper gate bash -n passed for
scripts/supabase_stage_provider_refund_paid_row_transition_smoke.sh and
scripts/supabase_stage_reservation_to_settlement_refund_impact_smoke.sh.
Focused callback gate bash -n passed for
scripts/supabase_stage_killbill_refund_callback_to_settlement_reversal_smoke.sh.
Backend unit proof on 2026-07-12 added multi-refund-safe correlation:
ProviderRefundResultFromLedgerHandler resolves a refund by explicit
payment_refund_intent_id or provider-enriched correlation key matching the
ledger refund intent/idempotency key. If no correlation key is present and the
ledger has multiple refund intents, the provider result remains failed/repairable
instead of choosing by amount or mutable paid-row state.
2026-07-12 callback enrichment added a provider-neutral
ProviderRefundCorrelationLookup application port. Kill Bill callback ingestion
uses it only after provider-enriched payload fields are absent. The Kill Bill
HTTP adapter resolves the callback transaction through /payments/{paymentId},
matches the callback paymentTransactionId to a REFUND transaction, and
returns its transactionExternalKey as the refund correlation key. Focused
tests cover callback fallback, adapter extraction, and rejection of same-id
non-refund transactions.
Known Gaps
Section titled “Known Gaps”Live Kill Bill-originated webhook E2E through the restricted Traefik admission route remains open. Sales dashboard fact recomputation/read APIs, artifact grant revocation persistence, replacement request/reissue projection, live Kill Bill transaction lookup stage proof for provider-originated callbacks, and operator-visible repair result history remain partial.
Consequences
Section titled “Consequences”Refund impact becomes backend-ledger authority, not UI or provider-envelope truth. Settlement and reporting invalidation share one provider result event, so later screens cannot show paid revenue after an accepted refund without an explicit stale-projection bug. Smoke scripts must reap both reservation and provider-refund batches after proof.
Revision History
Section titled “Revision History”- 2026-07-12: accepted ledger-derived provider refund transition and settlement reversal authority.
- 2026-07-12: added hosted Kill Bill refund callback to settlement reversal proof.
- 2026-07-12: added provider lookup fallback for refund callback correlation.