Skip to content

ADR 026 Payment Cash-Receipt Handoff Snapshot

Legacy KCP forms carried cash-receipt flags separately from payment-product, merchant-of-record, collection-channel, and receipt-issuer facts. Direct caregiver service balance and Sanmopia-collected customer share could therefore reach similar UI code despite different tax-evidence authority. Existing modern eligibility query was advisory and payment pre-registration could bypass it.

PreRegisterReservationPaymentCommand requires one explicit PaymentProviderCashReceiptIssueRequest. Public input contains payment id, request state, purpose, and vault identity reference only. Browser cannot submit reservation, amount, payment method, product plan, merchant, collection channel, receipt issuer, eligibility, or provider routing.

Backend resolves one effective PaymentProductPlan, verifies obligation coverage, quotes provider fee, authorizes actor, and creates ReservationPaymentCashReceiptEligibilitySnapshot. Requested but ineligible cash receipt stops before provider pre-registration. Exact replay requires same actor, idempotency key, and cash-receipt request.

Reservation payment provider boundary receives one typed ReservationPaymentProviderHandoffIntent containing frozen actor, route-fee, and cash-receipt snapshots. Legacy (payment_id, amount) pre-registration stays only for payment-change compatibility; reservation checkout cannot silently drop cash-receipt policy before Kill Bill handoff.

Supabase RPC sanmopia_pre_register_payment_handoff calls existing actor, fee, and cash-receipt pre-registration inside the same transaction, independently validates the cash decision against the effective product catalog, validates the backend-owned provider handoff session against frozen payment facts, then inserts immutable eligibility and handoff snapshots. Product-policy or handoff forgery rolls back all writes. Existing modern actor snapshots backfill as not_requested; raw registration identity is never stored.

Reservation checkout receives a PaymentProviderHandoffSession descriptor whose amount, selected method, allowed method set, provider route, return path, expected payment revision, expiry, idempotency key, and fingerprint come from backend facts. The browser never supplies provider payload fields or recomputes the handoff from visible amount, product labels, or catalog state.

Backend focused gate passed 54 tests plus Ruff and Tach dependency, interface, and external-import gates. Contract layout, ESLint, TypeScript build, and 40 focused tests passed. Supabase stage applied migrations 20260712161500 and 20260712162500; schema lint, generated types, product policy smoke, table lookup, and RPC lookup passed.

Transactional stage proof pre-registered payment stage-payment-continuation-20260708084031, atomically returned actor snapshot 33333333-3333-5333-8333-333333333333 and cash snapshot 44444444-4444-5444-8444-444444444444, observed accepted provider issuance, then rolled back. Separate proof submitted forged caregiver merchant policy for same Sanmopia plan; DB rejected cash receipt snapshot contradicts payment product policy and retained no writes.

Callback ingestion parses provider cash fields once as ProviderCashReceiptEventFacts. Cash-receipt evidence moves to reconciliation_required without a frozen accepted decision, on purpose mismatch, identity-reference mismatch, or partial approval identity. Accepted payment workflow still continues; cash-evidence failure cannot trigger a full-payment refund. Provider callback may supply approval, transaction, order, and source-code evidence; stored request purpose and vault identity come only from frozen pre-registration. Focused callback/handoff gate passed 98 tests plus Ruff and Tach. Stage schema lint, type generation, and transactional reconciliation-status proof passed.

Successful refund callback now reads latest approved cash-receipt evidence through sanmopia_latest_approved_cash_receipt_evidence, then appends provider_refund_reversal evidence only when provider supplies an explicit cash reversal reference. Reversal row links original request/approval, refund intent, payment-ledger revision, and immutable eligibility snapshot id. Missing source, snapshot, reader, store, or reversal reference becomes reconciliation_required; code never treats payment refund transaction id as proof that tax evidence was reversed. Production composition now injects the Supabase cash-evidence store and reader into Kill Bill ingestion; this binding was previously absent.

Reversal-focused gate passed 58 tests, Ruff, Tach, main import, Supabase lint, generated types, and transactional stage proof for original-evidence lookup plus append-only reversal lineage. Stage data rolled back.

Receipt artifact and settlement tax-export projection now consume a backend-owned cash-receipt evidence timeline. PaymentCashReceiptTaxEvidenceTimeline sorts all append-only evidence for one payment by ledger revision and occurrence time, selects the current customer-visible evidence for printable receipt artifacts, and supplies all exportable approval/reversal rows for settlement tax datasets. Supabase now exposes a payment-scoped evidence list reader, so UI/API callers no longer choose a single evidence row and cannot expose a stale approval after a refund reversal. Focused projection and adapter gates prove stale approval replacement with provider_refund_reversal evidence and approval-plus-reversal lineage in the settlement dataset.

Hosted customer receipt tax-evidence lookup now has an authenticated FastAPI boundary: GET /customer-receipt-tax-evidence-artifacts/{reservationBookingId}. The request supplies only paymentId and templateVersion; backend checks Supabase Auth, rejects withdrawn sessions, checks SpiceDB view_receipt on the reservation booking, loads all cash-receipt evidence through the Supabase payment-scoped reader, and returns the current customer-safe artifact payload. The response excludes provider payment/order/transaction references and raw identity fields.

Hosted settlement tax-evidence dataset lookup now has an authenticated FastAPI boundary: GET /settlement-tax-evidence-dataset-revisions/{datasetRevisionId}. The request supplies branch or headquarters scope, payment id, and payment ledger revision; backend checks Supabase Auth, rejects withdrawn sessions, checks SpiceDB settle on the branch or review_hq_settlement on the headquarters settlement data room before reading evidence, then returns backend-selected approval and reversal lines with checksum and source evidence keys. The export route does not allow UI row selection or provider reference exposure.

Stage runtime proof now exercises that hosted boundary with a real booking: pnpm supabase:smoke:stage:settlement-tax-evidence-route created reservation booking f6931f0c-1eaf-4cb8-a45a-7485c47934a4, seeded provider-neutral payment stage-tax-evidence-payment-20260712122749-8c31df28, projected dataset stage-tax-evidence-dataset-20260712122749-8c31df28, verified branch and headquarters access each returned two approval/reversal lines, and reaped the stage reservation batch with SpiceDB cleanup verification.

Stage customer receipt runtime proof now exercises the customer-safe hosted boundary: pnpm supabase:smoke:stage:customer-receipt-tax-evidence-route created reservation booking ba16063f-0b9c-4357-9fc4-b21a91399872, seeded provider-neutral payment stage-customer-receipt-payment-20260712123232-51185685, verified the mother actor could read the backend-selected reversal artifact customer_receipt_tax_evidence:stage-customer-receipt-payment-20260712123232-51185685:ledger-4:stage-cash-receipt-reversal-20260712123232-51185685, and reaped the stage reservation batch with SpiceDB cleanup verification.

Stage reservation checkout handoff proof now exercises the public pre-registration route and persisted handoff row: pnpm supabase:smoke:stage:reservation-payment-handoff-session created reservation booking 113d0182-a79d-471d-a66b-d7183b053298, pre-registered payment 113d0182a79d471da66bd7183b053298, verified the response included backend-owned handoff reservation-payment-handoff:113d0182-a79d-471d-a66b-d7183b053298:113d0182a79d471da66bd7183b053298:0, verified exact replay kept the same handoff key and fingerprint, verified the Supabase row persisted with provider_key=killbill, state=prepared, and expected_payment_revision=0, and reaped the stage reservation batch with SpiceDB cleanup verification.

Stage Kill Bill callback proof now exercises the hosted callback boundary with the official ExtBus payment payload shape: pnpm supabase:smoke:stage:killbill-payment-event-callback created reservation booking 06bb3c50-aca5-4ab4-97e2-fd8a955729a6, pre-registered payment 06bb3c50aca54ab497e2fd8a955729a6, posted a signed PAYMENT_SUCCESS callback, verified inbox event killbill-event:sha256:3af8a2681cbb1d12985bf4eda20e0ede5e162efa50e9930b5e9d66d52165d08f, verified exactly one sanmopia_payment_webhook_processing message 7, verified replay returned duplicate without another queue message, dispatched the internal payment_webhook_processing worker, verified the worker archived message 7, verified Restate payment workflow killbill:PAYMENT_SUCCESS:06bb3c50aca54ab497e2fd8a955729a6:stage-killbill-tx-20260712131444-9f9a82b5 completed, verified the payment record moved to approved with deposited_at, and reaped the stage reservation batch with SpiceDB cleanup verification.

Stage callback-to-settlement proof now uses the callback-approved payment as the same payment workflow for downstream settlement: pnpm supabase:smoke:stage:killbill-callback-to-settlement-completion created reservation booking b681a97a-3bb9-4e00-887d-c50d3799ef23, approved payment b681a97a3bb94e00887dc50d3799ef23 through Kill Bill callback event killbill-event:sha256:1aef72c067a736ae2bbbd3fcdb13e0efe2bdb1c35d3234093f4f397da22d001a, continued the same payment workflow killbill:PAYMENT_SUCCESS:b681a97a3bb94e00887dc50d3799ef23:stage-killbill-tx-20260712141353-8b4ed89b, recorded service delivery, issued caregiver payout instruction ec4f5565-8a01-57bf-b830-35e1e0a193b6, recorded branch payout deposit, prepared branch and headquarters settlement, completed settlement, verified customer account-disclosure readiness not_required, verified branch settlement board line items, and reaped the stage reservation batch with SpiceDB cleanup verification.

Cash-receipt policy and provider checkout handoff become historical payment evidence, not UI or callback state. Catalog changes cannot rewrite completed payment presentation. Remaining proof: trusted Traefik/provider-originated Kill Bill callback deployment and live provider-originated reversal.