Skip to content

ADR 027 Payment Refund Settlement Reversal

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.

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.

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.

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.

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.

  • 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.