# Payment aggregate integration plan ## Goal Replace the current payment callback records with one durable payment system, built from the strongest parts of two branches, delivered to `main` as pull requests that each leave a complete, smoke-testable system. This file is the working plan: status, binding rules, decided behavior, milestones, and the fault ledger. ## Sources - `origin/base/payment-aggregate` (#1962) is the operational reference: storage, claims, provider reads, reconciliation, booking completion, refunds, maintenance, migration, redaction, and owner pages. It is never merged or copied as-is — its own TODO records unresolved faults and its coverage gate is incomplete. - `origin/claude/great-fermi-l2n29f` (#1973) is the reference for pure payment rules: money, provider observations, conflicts, stored records, and owner decisions. Its best idea is one `outcomeOf` diagnosis shared by live resolution and validation of stored evidence. It has no production callers and never merges as a standalone layer. - Neither branch contains the other (merge base `15e48fac7`), and neither merges into `main`. Work lands as fresh milestone pull requests. ## Where we are | Milestone | Status | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | M1 safety behavior (was PR 1) | Merged as #2020. Also landed the M2 pure modules: `src/shared/payment/money.ts`, `resource-id.ts`, `refund-state.ts`, and `validated-session.ts`. | | M2 money/resource vocabulary (was PR 2) | Core modules merged inside #2020. Any provider parsing still off those schemas rides with M3 or M4. | | M3 provider ownership (was PR 3) | Complete: #2048 (payment processing core), #2050 (bounded registration delivery), #2060 (observation boundary + SumUp callback wiring; F2 closed). | | M4 settled-money diagnosis (was PR 4) | Part A complete for the current path: #2065 (one durable `payment_charges` authority, one provider-send permit, one owner-recovery lifecycle), #2079 (resumable placeholder completion; each machine declared as a checked spec feeding the `/admin/schema` map), #2084 (the declared seam between the machines, witnessed in every crash test, with a live stored-data check on the map), and #2109 (the SumUp checkout-recovery task on the current path). Whole-checkout diagnosis, stable booking obligations, allocation, and durable Refund All jobs remain later milestones. | | M5 cases (was PR 5) | The current-row review/acknowledgement slice landed in M4 Part A. Remaining aggregate case kinds ship with their M6–M8 producers and actions, not as a dormant layer. | | M6–M11 atomic aggregate cutover | Not started as a production cutover. #2056 started its read-only verifier early; that pull request is still open and unmerged. Creation, reads, jobs, completion, delivery, site effects, and bounded history migration activate together; no intermediate layer may merge as a live parallel path. | | M12–M13 | Not started. | Budgets below count `src/` lines only. Observed totals run 4–15x the `src/` figure once tests, stories, and catalog copy are included (#2020: 714 src lines, 10,519 total), so this plan no longer publishes total-diff estimates — they were consistently wrong and governed nothing. ## Delivery rules 1. **Every merged PR stands alone indefinitely.** Each merge leaves one complete production system the owner can smoke test, with no dormant foundations, no release trains, and no code activated only by a later PR. Every new production export has a production caller in the same PR. A durable case or due-work kind ships with every owner action and scheduled recovery path it needs — or links to a still-live admin tool that resolves it. Each PR must improve the system that is live when it merges; a test, schema, repository, or helper needed only by a later PR does not qualify. 2. **One production path per behavior.** Migrate every caller and delete the displaced implementation in the same PR. No aliases or compatibility wrappers between the branches' names, no dual writes, and no runtime read-through over the displaced store. A bounded migration may decode an old record only inside its explicit owner-authenticated ceremony. It must finish and verify before the canonical runtime is enabled; the same release then deletes every old runtime caller. M4 follows this rule by cutting every live refund sender over to the canonical authority at once. 3. **Size.** Keep each PR under 800 changed `src/` lines (insertions plus deletions, recounted after formatting). One exception: an atomic cutover that would otherwise need a throwaway compatibility layer may exceed the cap — a bigger honest PR beats building a bridge in one PR and demolishing it in the next. Say so in the description. docs/pr-workflow.md's "repository source-line limit" is this rule, exception included. Tests, fixtures, and documentation do not count against the cap; never weaken them to shrink a diff. #2065 is the approved current-path exception: its `14881` gross changed `src/` code lines cut every refund caller, provider adapter, persistence writer, and recovery surface over together. Imports, comments, and blank lines are excluded from that audited total. Splitting it at a live seam would have required the legacy and canonical authorities to run in parallel, which this plan forbids. 4. **Gates.** `devenv shell deno task precommit` passes before review. Run targeted mutation on the payment modules the PR changed and list the runs in the description. The branch-level `devenv shell deno task precommit:mutation` gate runs before merge as AGENTS.md requires — no per-PR waivers. If an exceptional cutover genuinely cannot run the branch gate, the owner first changes the policy in AGENTS.md; this plan cannot loosen a repository rule. 5. **Every bug fix ships a regression test** reproducing the bug. 6. **Deployments are forward-only and fleet-wide**, on the `release` tier only. No old-version support, no code rollback, no mixed-version reads or writes. Bunny's roughly two-second script handoff is operational overlap, not a compatibility window — it must not cause a runtime branch, schema adapter, or legacy replay path. The aggregate cutover release first fences old writes, completes its bounded owner-authenticated migration, and verifies every source disposition. Only then does one epoch switch enable canonical runtime reads and writes. No request can see a partly migrated store. 7. **The aggregate tables are already shipped.** M4 Part A activated the previously unwritten `payment_charges` table as the sole durable authority for provider refunds. Its migration refuses to reinterpret a non-empty table. The still-dormant `payment_sessions`, `payment_completion_effects`, `payment_completion_deliveries`, `payment_cases`, and `payment_case_decisions` tables are defined in `src/shared/db/migrations/schema/payments/`; do not add drop-and-recreate churn against them. Each must hold a complete production role when the atomic M6–M11 cutover activates or be dropped in that same release. Their existence is not permission to land unused repositories, codecs, indexes, or exports. Their currently dormant buyer-bearing columns accept DB-key `enc:1` ciphertext. No production writer may use that shape: before activation, rebuild every buyer-bearing or raw-reference field to require owner-key `hyb:1` ciphertext, or drop the table if it has no complete production role. 8. **Port the source branch's tests** when adapting one of its modules; adapt existing tests rather than authoring from scratch. Never copy great-fermi's test-only-export exemptions. 9. **PR descriptions state**: the immediate current-system value and the exact production route, worker, page, or write path that receives it; the summed `src/` line count; the database and provider call budget whenever the slice touches providers or adds queries (Bunny's hard limit is 50 subrequests per request); the fault-ledger rows closed; the tests and mutation commands run; and the old path deleted. The full field list is in steps 2, 5, and 6 of `docs/pr-workflow.md`. Review pure schemas, transactions, provider parsing, and orchestration as distinct commits where that helps. 10. **Machines, seams, and witnesses ship with the first slice.** Any new stateful lifecycle — M5's cases, the M6–M8 aggregate states, M9–M10's delivery and site work — lands with its declared machine spec, its seam declarations against every machine it shares reality with, witness assertions in its crash manufactures, an enumerated flows × crash-windows table in the PR description (each window closed by an idempotent-redelivery proof or a fault-injected test), and its section on the `/admin/schema` map. Retrofitting exactly this onto the refund machines took three large PRs (#2065, #2079, #2084); declaring it at design time is the cheap path. The method and its reference files are AGENTS.md's "Checked forwards and backwards" section. Size check: ```bash git diff --numstat ...HEAD -- src/ ``` ## Decided behavior These binding product decisions are requirements. Each attaches to the milestone that ships the behavior: - A failed checkout that shows captured money stops automatic work and creates an owner case. The owner must complete the booking or refund it. - A completed refund counts immediately even when the provider's cumulative total lags. New overlapping refunds stay blocked while reconciliation reads the provider again. - Every captured charge is stored, but more than one captured charge on one payment requires owner review. If several coherent captures sum exactly to the signed total, the booking may proceed with that review and no automatic refund. If the multi-capture reading also fails currency, total, parentage, or another validation rule, it parks: no booking and no automatic refund. A coherent single-charge mismatch keeps the automatic-refund remedy. The review is a durable case. Its best-effort ntfy alert contains only a finite case code — no buyer/provider identifiers or amounts — and points the owner to the owner-protected record where the evidence and required action live. - A provider-partially-refunded charge observed before booking parks even when its captured and expected totals otherwise agree: no paid booking and no automatic refund. The buyer is retained for owner review. On an existing booking, Refund and Refresh preserve the exact partial-return work rather than treating it as zero or complete. - Contradictory provider parentage is an action-level no-refund gate regardless of which conflict label wins. Conflict ordering may choose one headline, but must evaluate refusal-shaped conditions before proceed-shaped review and must retain the complete observed charge evidence. - Money already proved fully returned never creates a paid booking. A new booking takes the stable refused-and-refunded result; a balance remains unpaid and records the captured-and-returned cash round trip against the existing attendee rather than creating a placeholder. - If a reviewed or conflicted payment later fails ordinary booking work, that downstream failure cannot route it into an automatic one-charge refund. The buyer record and exact owner-review evidence remain authoritative. - A legacy provider charge may fund one or more booking obligations. Before an automated refund or ledger reversal can use it, the owner either records an exact allocation of its captured Money across those obligations or rejects the automated action. Every part is positive, uses the charge currency, and the parts sum exactly to the captured amount. The system never invents an equal, proportional, first-attendee, or current-row split. - Returning cash and cancelling a booking obligation are separate effects. A confirmed provider refund records only the cash that charge returned. An obligation cancellation reverses its sale, modifier, and fee facts exactly once under the stable obligation identity. Neither effect implies the other. - When one payment under an obligation returns while another remains captured, the owner makes a required, revision-fenced choice with no default: keep the booking and make the returned amount due, return all remaining cash and then cancel, or cancel now while keeping the retained cash as visible refund work owed to the buyer. - A queued owner email uses the current business address at send time. Its body and buyer facts remain the stored payment snapshot. - An incomplete or contradictory retained payment record is copied without invented facts, marked for owner review, and does not stop the rest of the aggregate migration. An attendee payment id that survives only inside old PII is outside that promise: current in-app refunds deliberately leave it unsupported rather than decrypting attendee history to recover it. - A buyer whose paid booking needs review sees that payment was received, that the booking needs review, and that they must not pay again. The approved copy is: “We received your payment. Your booking needs a manual check. Do not pay again — we will contact you.” Reloading shows the same stable result. - Post-terminal provider evidence is merged, never used to replay an irreversible effect. Unchanged evidence is a replay; the application's own later-visible refund confirms its recorded attempt. An unambiguous external full refund of the single known charge remains eligible for Refresh to record the returned cash in Money without implying booking cancellation. Partial, sibling-capture, or contradictory evidence blocks instead. Provider array order and mutable attributes are not resource identity. - No aggregate checkout path becomes authoritative until the owner can view its evidence and perform every supported required action. Until a case kind's own page action ships (M7/M8), a supported action may be a link to a still-live admin tool that genuinely resolves that case kind. - Retry and replay rules apply only to duplicate callbacks or interrupted work handled by the current version. The system never emulates an older version's payment behavior. - A refused checkout's terminal completion records its existing refund authority under the same durable session identity, so a later delivery of that session — a webhook redelivery, or the buyer opening the success page — short-circuits through the same engine and cannot make a second ticket for money already returned. No second processed-payment refund marker is added beside the canonical identity. - A buyer whose paid checkout was refused gets a truthful answer for every exit: money taken with no usable reference says so and asks them to get in touch; a return that awaits the provider or a required owner choice says which; money reported paid but not proved returned says that. A proved return says that the money was returned and that no booking was created. Reloads and redeliveries replay that same answer without another effect. Unverifiable or foreign price proof keeps the generic not-found answer, because it may belong to another site's buyer. The copy comes from the whole-checkout engine's exhaustive buyer outcome, never from a second parallel rejection-response state. ## Target architecture ### Pure rules (adapt from great-fermi) All pure payment rules live in `src/shared/payment/`; do not open a second directory. The common vocabulary is `money.ts`, `resource-id.ts`, `refund-state.ts`, and `validated-session.ts`. M4 Part A added the refund-only vocabulary in `resources.ts`, `conflict.ts`, `diagnose.ts`, `refund.ts`, and `admit-refund.ts`: `ChargeMoney` is the complete fact one charge can prove, `refundOutcomeOf` is its sole judge, and the pure `PaymentConflict` union contains only the three disagreements that judge can produce. Those names are not a second stored review schema: a durable provider disagreement retains its exact Money in the canonical authority. Exact zero or full-return evidence can become `needs_owner_choice/provider_conflict`; partial or inconclusive evidence becomes `needs_provider_check/provider_conflict` and admits no owner money answer. `provider-read.ts`, `provider-failures.ts`, `refund-attempt.ts`, and `refund-network.ts` make provider reads, attempts, failures, and bounded reconciliation total and exhaustive. `row-state.ts`, `claim.ts`, `review.ts`, and `admit-move.ts` declare the live row lifecycle and derive its operator and writer rules. The whole-checkout observation, ownership proofs, charge legs, expected-money comparisons, and whole-payment `outcomeOf` are deliberately NOT present. They arrive only with the M6 reader that can supply their complete evidence. Build them against that reader, widen stored unions only when a real reading can produce the new case, and do not recreate a second refund judge. ### Persistence and runtime (keep and harden from payment-aggregate) M4 Part A supplies the live runtime discipline: provider-aware indexed references, revision-fenced attendee claims, typed provider reads and attempts, one durable `payment_charges` authority shared by admin and buyer callbacks, one admin single/bulk/refresh orchestration path, exact ledger findings, structured confirmations, and declared lifecycles for provider work, local row work, owner choice, and terminal outcome. The aggregate still needs payment, allocation, case, decision, effect, and delivery repositories; persist-before-provider checkout creation; durable booking and refund jobs; scheduled recovery; bounded migration; backup, restore, and redaction. New raw provider references in `processed_payments` and `payment_charges` are encrypted to the owner's public key. Equality uses a DB-keyed one-way index, and SQL-only consumers see plain state words, never the reference. With a modern v2 password-wrapped owner key, a holder of the database and `DB_ENCRYPTION_KEY` alone cannot open those new raw references or attendee PII. A dormant legacy v1 owner wrap is the explicit exception: its KEK is derivable from the stored password hash plus `DB_ENCRYPTION_KEY`, so that holder can unwrap the data key and site private key until the next successful login upgrades the wrap to v2. This is owner-key-protected at-rest storage, not an authorization claim that only owner requests possess the site private key: manager requests share that key, while owner-only refund detail and decisions are enforced at the route boundary. This claim is deliberately about data stored in this database: provider credentials are DB-key encrypted settings, so somebody holding the database and environment key may be able to use those credentials to query data held by the provider. `processed_payments.failure_data` has a deliberately different boundary: it is DB-key encrypted, so that same holder can open its lifecycle metadata. Keep it limited to attendee and command ids, times, finite review/claim/outcome reasons, and terminal display text (which may include a listing name); raw provider references and buyer PII must never enter it. Every current writer uses the one `PaymentRowState` stored schema. Live readers reject historical bare terminal failure and review shapes instead of normalizing them into a parallel runtime. Historical plaintext `processed_payments.payment_reference` values are not readable by live refund code. Saving or merging that attendee cannot turn a PII payment id into refund authority: neither action can prove its provider, and neither writes a payment row. Old DB-key-encrypted refund-warning notes remain display history until the owner deletes them or the retention/redaction ceremony removes them. They are never refund-reference migration input. No interactive request performs a population decrypt or rewrite. Do not copy `payment-runtime/legacy-replay.ts`, `legacy-sumup.ts`, `operator-legacy-read.ts`, or any equivalent runtime branch selected by record age or origin. Migration code may decode an old retained payment-row format only to write a canonical current payment when that row already proves the identity, or a terminal accounting case when it does not. It may not search attendee PII or notes for another reference. After that write, only the current engine reads, reconciles, refunds, completes, or displays it. Provenance and unknown facts may preserve evidence; they must not dispatch to different runtime behavior. Every write validates the complete prospective record with the pure rules, writes with revision or lease fencing, and validates the returned row. There must not be parallel raw-row and decoded-domain implementations of the same rule. ## Data laws Eight laws govern how every part of this program behaves around data. Each milestone contract instantiates them for the data it touches and says which law admits each new state, consumer, or fact — so a review finding of one of these shapes is answered by the law, and a design that satisfies them up front rules the shape out as a class. M4's first production instantiations are `PaymentRowState` plus the exhaustive `PAYMENT_ROW_LIFECYCLE` declaration, `RefundAuthorityState` plus its exhaustive refund lifecycle, the exact claim and revision transactions, and `ATTENDEE_DATA_RULES`; M5's cases, M6's aggregate rows, M7's refund jobs, M8's completions, and M11's migration copies are all data these laws bind. 1. **One authority per fact.** Each fact has one canonical representation and one place that computes it; every other appearance derives from that copy and is regenerated, never restated. Two definitions of one value — a second judge, a second serialization, a second total — are a defect even while they agree. 2. **Facts carry provenance: signed, stored, or observed — never ambient.** A decision consumes only facts the datum carries, evidence a read proved, or values signed at creation. Live settings may order a search; they never decide a fact. A fact nothing carries is explicitly not evaluable, or an honest failure naming what is missing — never filled from today's configuration. 3. **Identity is immutable; everything else is an attribute.** Compare, merge, deduplicate, and index by the smallest immutable identity. A changed attribute updates its datum in place and is itself a recordable event; it never mints a second datum. 4. **Stored state is a declared machine with a total lifecycle.** Every state names what creates it, every consumer that must recognize it — readers and writers alike, including cleanup, merge, delete, restore, and replay — what retires it, and how it ages out under retention. A consumer that cannot read the authoritative record routes on a mirror written in the same statement, never on a proxy. A blocking state also names a real operator route and a rendered control that can reach it, with a visitor journey proving the state can end. A state representing unresolved money requires an explicit choice; acknowledgement alone never clears it. A state and all its consumers ship in one slice. 5. **Decisions bind to complete, versioned evidence.** A path that acts reads the full declared evidence shape for its source — no path decides on less than the source's declared observation — and every consequential write is fenced on the exact evidence it judged: changed evidence forces a re-judge, and recorded evidence only grows, by merge, never replacement. 6. **Data never moves to weaker protection.** A fact under a stronger key or boundary is never copied under a weaker one. Cross-boundary comparison uses one-way codes; a plaintext mirror carries a state word, never contents. A migration that cannot preserve this rule leaves the old record with reduced functionality rather than decrypting a population or copying its contents under the database key. 7. **External parties are capability records.** What a provider guarantees — an idempotency key, a cumulative total, an event authority — is declared once, and every behavior derives from the declared capability, never a per-party arm, so a new party inherits the whole discipline by declaration. 8. **Atomicity before compensation.** Facts that must agree and live in this database change in ONE atomic write — the same statement, or one libsql batch or interactive transaction, which commit whole and roll back whole, with a conditional write's affected-rows count deciding a winner (the helpers in AGENTS.md's Transactions and Batches section). Never write one half and defend the gap with a check, a retry, or a repair pass. Guards, fences, staleness rules, and re-judges are reserved for the one gap atomicity cannot close — an external call or a concurrent request in the middle — and each one names the gap it spans. When a review offers "add a guard, or make the write atomic", atomic wins. ## Milestones Milestones are behavior units, not normally a PR count: one milestone may land as several standalone PRs (M3 already has), and each PR still satisfies every delivery rule on its own. The M6–M11 aggregate replacement is the deliberate exception: its work packages may be developed and reviewed as commits or unmerged branches, but they activate in one PR and one fleet-wide cutover because any merged intermediate would require a live compatibility path. Provider cutovers move Stripe, Square, and SumUp together behind exhaustive records keyed by provider, so adding or omitting a provider is a compile error. The approved work has one current-path stack and one atomic aggregate cutover. The current-path stack now has M4 Part A's exhaustive provider outcomes, provider-tagged reference admission with explicit refusal for older untagged references, exact admin claims, one callback/admin provider authority and send permit, bounded selected-page execution, exact ledger repair, and owner-visible recovery. The aggregate cutover turns whole-checkout problems into durable cases without duplicating that lifecycle, makes attendee merge atomic, moves checkout, reads, completion, delivery, and site effects to stable booking obligations with exact allocations, migrates retained history under the owner key, then removes every old runtime reader and writer before activation. Its internal work packages are individually testable, but are not live release layers. ### Stack A — finish the current path (M3–M4; M5 behavior follows its producers) #### M3: Check provider ownership on the current path (was PR 3, complete) Src target: remainder of ~800. - Adapt the strict provider observation boundary. Allow an unrecorded child only when it is a charge under the same pending checkout. - Wire SumUp's current callback path first; block unknown unsigned callbacks from causing unbounded provider reads. Standalone value: current SumUp callbacks cannot attach unrelated resources or be used to amplify outbound requests. #### M4: One diagnosis for settled money — fail-closed cutovers only (was PR 4) Status: Part A is built. The code named here and its mirrored tests are the authority for this live slice. It hardens admin single refunds, Refund All, Payment Refresh, buyer-callback refunds, payment-row merge/delete/retention, and the provider and checkout boundaries they use. It activates `payment_charges` as the one provider-refund authority. Its machines are declared as checked specs, the seam between them is a declared illegal-combinations table witnessed by every crash test, and the `/admin/schema` map renders both and checks the stored data against them — the reference for delivery rule 10. It does not build a whole-checkout observation, allocate one charge across stable booking obligations, or write the other dormant aggregate tables. The former `PR4_PLAN.md` has been retired; this as-built map and the fault ledger preserve the useful decisions without leaving a second planning document to drift from the code. As-built module map: | Contract | Exported entry points | Focused authority tests | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Charge evidence and judgment | `chargeMoneyRead`; `refundMoneyReturned`; `refundMoneyAccountedFor`; `refundMoneyMatchesCapture`; `refundOutcomeOf`; `resolveRefund`; `admitRefund`; `admitObservedRefund`; `admissionReason` | `test/shared/payment/{resources,diagnose,refund,admit-refund}.test.ts` | | Provider reads and attempts | `ProviderRead`; `providerFailure`; `RefundAttemptResult`; `RefundRequest`; `refundOutcomeAfterReread`; `refundWithOneReread`; `SquarePaymentStatus`; `isSquarePaymentStatus` | `test/shared/payment/{provider-failures,refund-attempt}.test.ts`; `test/shared/stripe-provider/{outcomes,refund-outcomes}.test.ts`; `test/shared/square/payment-outcomes/refund.test.ts`; `test/shared/sumup/{transaction,provider-money}.test.ts`; `test/shared/square-provider/{metadata,webhook-fields}.test.ts` | | Durable provider state | `RefundAuthorityState`; `RefundAuthorityStateName`; `readRefundAuthorityState`; `writeRefundAuthorityState`; `validateRefundAuthorityState`; `refundStateMirror`; `refundLocalMirror`; `refundNextActionAt`; `readyRefund`; `armRefundSend`; `markRefundObservationDue`; `rearmKeyedRefund`; `returnRefundToReady`; `markRefundCompleted`; `markRefundLocalRecorded` | `test/shared/payment/refund-authority-state.test.ts`; `test/shared/payment/refund-authority/state.test.ts`; `test/shared/provider-refunds/state/contracts.test.ts`; `test/integration/refund-authority-architecture.test.ts` | | Conflict, choice, and lifecycle | `RefundConflictDecisionSchema`; `refundConflictDecision`; `refundConflictNeedsProviderCheck`; `markRefundOwnerChoiceNeeded`; `mayReplaceRefundWithFreshEvidence`; `markRefundProviderConflict`; `refundOwnerChoices`; `resolveRefundOwnerChoice`; `refundEvidenceActionAllowed`; `refundLifecycleFor`; `refundAuthorityWorkSql`; `refundAuthorityPrunableSql`; `refundMoveRefusalOrNull` | `test/shared/payment/refund-authority-choice.test.ts`; `test/shared/payment/refund-authority-lifecycle.test.ts`; `test/shared/payment/refund-authority/state.test.ts`; `test/shared/provider-refunds/{state,target-conflict}.test.ts`; `test/integration/refund-authority-architecture.test.ts` | | Provider permit and one engine | `ProviderRefundEvidence`; `ProviderRefundTarget`; `ProviderRefundResult`; `RefundAuthorityReceipt`; `ProviderRefundDependencies`; `RefundEngineProvider`; `loadRefundProvider`; `AuthorizedRefundRequest`; `authorizeDurableRefundSend`; `requireProviderRefundAuthorization`; `requestProviderRefunds`; `requestProviderRefund`; `recordProviderRefunds` | `test/shared/provider-refunds/{target,state,work,send,budget}.test.ts`; `test/shared/provider-refunds/send/outcomes.test.ts`; `test/shared/provider-refunds.test.ts`; `test/integration/refund-authority-architecture.test.ts` | | Atomic authority storage | `prepareRefundAuthority`; `createOrLoadRefundAuthority`; `bindRefundCallbackIfChargeExists`; `transitionRefundAuthority`; `resolveRefundAuthorityMoney`; `completeRefundAuthority`; `markRefundAuthorityRecorded` | `test/shared/db/provider-refund-authority.test.ts`; `test/shared/provider-refunds/{target,state,work,send}.test.ts` | | Callback and placeholder cutover | `requestSessionRefund`; `refundRejectedCharge`; `storeRefundedBooking`; `attendeePaymentProvenance`; `finishPlaceholderRefund`; `resumePlaceholderSession`; `completePlaceholderMoney`; `advanceSessionFailure`; `loadAnchorRowWork` | `test/features/api/payment-processing/refunds/{provider-result,rejected-charge}.test.ts`; `test/features/api/payment-processing/{index/refunds,store-refund,store-refund-authority,placeholder-resume}.test.ts`; `test/shared/db/processed-payments/outcome-advance.test.ts`; `test/shared/db/payment-anchor/held-work.test.ts`; `specs/payments/unreadable-payment-kept.feature`; callback integration suites under `test/integration/server/` | | Admin claims and dispatch | `claimLeaseMs`; `checkingClaimFor`; `claimAttendeeRows`; `underAttendeeClaim`; `runRefundReadiness`; `rememberReadinessFailureFindings`; `requestReadyRefund`; `dispatchRefundBatch`; `refreshClaimedPayment`; `AuthorityBearingReference`; `recordedRefundAuthorities` | `test/shared/payment/claim.test.ts`; `test/shared/db/payment-claim/{admission,take,unrecorded-date}.test.ts`; `test/shared/db/payment-claim/take/shared-references.test.ts`; `test/features/admin/refunds/{claim,readiness-failure-evidence}.test.ts`; `test/features/admin/refunds/readiness-findings/authority-failure.test.ts`; `test/features/admin/refunds/dispatch/{budget-lifecycle,write-order}.test.ts` | | Owner recovery | `ProviderRefundCase`; `listProviderRefundCases`; `loadProviderRefundCase`; `resolveProviderRefundCase` | `test/shared/db/provider-refund-cases{,-validation}.test.ts`; `test/shared/provider-refunds/state-owner-revision.test.ts`; `test/integration/server/privacy-refund-recovery{,-active-revision,-race}.test.ts`; `test/ui/templates/admin/provider-refund-cases.test.tsx`; `specs/payments/resolving-uncertain-refunds.feature` | | Local work and destructive moves | `PaymentWorkStatus`; `PaymentRecoveryAction`; `PAYMENT_ROW_LIFECYCLE`; `paymentWorkFor`; `paymentWorkForMirrors`; `mirroredMoveRefusalOrNull`; `mirrorFor`; `PaymentMoveSnapshot`; `loadPaymentMoveSnapshot`; `assertRowsFreeToMove`; `PaymentRowsBusyError`; `ATTENDEE_DATA_RULES` | `test/shared/payment/admit-move.test.ts`; `test/shared/db/payment-admit-move.test.ts`; `test/features/admin/attendee-page/actions.test.ts`; `test/integration/server/attendees/delete.test.ts`; `test/shared/db/attendees/{dependent-data,delete}.test.ts`; `test/shared/db/prune/payments.test.ts`; `specs/payments/recovering-the-money-record.feature` | | Confirmation and local review | `confirmRefund`; `insertRefundConfirmation`; `PAYMENT_REVIEW_RETIREMENT`; `getPaymentReviewState`; `acknowledgeCurrentPaymentReview` | `test/features/admin/refunds/confirmation.test.ts`; `test/shared/db/{refund-confirmations,payment-review}.test.ts`; `test/features/admin/attendee-payment-review.test.ts` | | Exact Money repair | `refundMoneyReturned`; `refundMoneyAccountedFor`; `computeAttendeeRefunds`; `RefundLedgerResult`; `REFUND_LEDGER_BATCH_DATABASE_CALLS` | `test/shared/payment/resources.test.ts`; `test/shared/refund-ledger/plan/{partial,reference-placement,whole-account}.test.ts`; `test/shared/refund-ledger/record/batch.test.ts`; `test/features/admin/refunds/provider/batch/{ledger-findings,ledger}.test.ts` | | Bounded command support | `refundReadinessSubrequestCost`; `refundPreparedSubrequestCost`; `REFUND_SETTLEMENT_SUBREQUEST_RESERVE`; `REFUND_LEDGER_SUBREQUEST_RESERVE`; `REFUND_OBSERVED_AUTHORITY_DATABASE_CALLS`; `REFUND_ACTIVE_AUTHORITY_DATABASE_CALLS`; `REFUND_TERMINAL_AUTHORITY_DATABASE_CALLS`; `REFUND_RETRY_HEADROOM_DATABASE_CALLS`; `REFUND_RESULT_DATABASE_RESERVE`; `withSubrequestReserve`; `withDeferredErrorReports`; `getRefundCandidates`; `getRefundAllSummary`; `loadRefundAllBatch` | `test/features/admin/refunds/{budget,claim-settlement,candidates}.test.ts`; `test/features/admin/refunds/provider/batch/budget.test.ts`; `test/features/admin/refunds/dispatch/budget-lifecycle.test.ts`; `test/features/admin/refunds/readiness-run/refresh-budget.test.ts`; `test/shared/provider-refunds/{budget,send}.test.ts`; `test/shared/subrequest-budget.test.ts`; `test/integration/logger/log-error.test.ts` | | Private indexed references | `storePaymentReference`; `loadPaymentReference`; `prepareClaimedAttendeePaymentAnchor`; `attendeePaymentProvenance`; `getRefundPaymentReferences`; `MAX_REFUND_REFERENCES_PER_ATTENDEE` | `test/shared/db/payment-anchor/{attendee,reference}.test.ts`; `test/shared/db/payment-references{,/readiness,/storage}.test.ts`; `test/shared/db/{refund-all-candidates,attendees/create,attendees/pii}.test.ts`; `test/features/api/payment-processing/{store-refund,store-refund-authority}.test.ts`; `test/integration/refund-authority-architecture.test.ts` | | Attendee refund controls | `refundWorkRemains`; `loadAttendeeForEdit`; `attendeePage` | `test/features/admin/attendee-page-data/{refund-actions,refunds-ui}.test.ts`; `test/features/admin/attendee-page/actions.test.ts`; `test/integration/server/attendees/payment.test.ts`; `specs/payments/checking-before-a-refund.feature` | | Declared machines and their map | `MachineNode`; `MachineEvent`; `MachineMoves`; `movesIn`; `machineRep`; `factsAndStart`; `factsFromNode`; `nodeIdsWhere`; `derivedNodeIds`; `atlasMachineFrom`; `ROW_NODES`; `ROW_EVENTS`; `ROW_MOVES`; `rowNodeOf`; `rowWorkMirrorSql`; `REFUND_NODES`; `REFUND_EVENTS`; `REFUND_MOVES`; `refundNodeOf`; `refundNodeSendsMoney`; `REVIEW_NODES`; `reviewNodeOf`; `SCHEMA_ATLAS_MACHINES` | `test/shared/payment/{row,refund,review}-machine-spec.test.ts`; `test/shared/payment/{row,refund}-machine-spec/graph.test.ts`; `test/test-utils/machine-graph.{ts,test.ts}`; `test/ui/templates/admin/schema-atlas.test.tsx`; `test/integration/refund-authority-architecture.test.ts` | | Machine seams and stored checks | `AuthorityFact`; `JointRowFact`; `ILLEGAL_JOINT_STATES`; `authorityFactOf`; `jointRowFactOf`; `illegalJointReasonOrNull`; `assertJointStateLegal`; `scanSchemaAnomalies`; `SchemaAnomaly`; `listUnansweredSumupMoney` | `test/shared/payment/joint-state.test.ts`; `test/shared/db/schema-anomaly-scan.test.ts`; `test/test-utils/joint-state.{ts,test.ts}` (the witness every crash manufacture runs); `test/test-utils/db-fault.ts` (real SQLite faults); `test/features/admin/schema-atlas/server.test.ts` | - **Authority module ownership.** `payment/refund-authority-state.ts` owns the one stored union, its parser, serializer, validation, and SQL mirrors. `payment/refund-authority.ts` owns pure automatic transitions; `payment/refund-authority-choice.ts` owns the explicit owner/conflict transitions, legal-choice derivation, and the exhaustive `mayReplaceRefundWithFreshEvidence` rule; `payment/refund-conflict-decision.ts` turns fresh Money evidence into the exact `not_sent | returned | wait` decision; and `payment/refund-authority-lifecycle.ts` is the one exhaustive declaration of each state's admitted evidence actions (`check_provider`, `observe_pending`, and `replace_with_fresh_evidence`), blocker, clearer, choice requirement, operator route, and pruning rule. Both the destructive-work SQL and its exact pruning inverse derive from that declaration. The database identity and state writers remain `db/provider-refund-authority.ts` and `db/provider-refund-authority-change.ts`; owner decisions plus their activity audit remain one transaction in `db/provider-refund-case-resolution.ts`. These are parts of one authority, not interchangeable implementations. - **Refund-only rules.** `src/shared/validation/kind.ts` and `src/shared/payment/{resources,conflict,diagnose,refund,admit-refund}.ts` define one charge's captured/refunded facts and one `refundOutcomeOf` judgment. Its pure `PaymentConflict` result can name `refund_exceeds_capture`, `partial_refund`, or `multiple_pending_refunds`; those are charge-judgment values, not variants stored in the local `PaymentReviewReason` schema. Durable disagreements use the canonical provider authority's `provider_conflict` evidence. Only exact zero or full return admits a real owner decision in `needs_owner_choice`; partial or inconclusive evidence lives in `needs_provider_check` and can only be observed again. Both retire through the one canonical Refund recovery route. `failed_refund` is deliberately absent: an attempt that certainly moved no money may be retried, while a failure reported beside returned money is diagnosed from the returned facts. The judge never invents the signed checkout total or booking allocation that this slice cannot read. - **Reference identity and storage.** Migrations `2026-08-10_refund_authority_records.ts`, `tables-attendees.ts`, `db/payment-reference-store.ts`, `db/payment-reference-rows.ts`, `db/payment-references.ts`, `db/payment-anchor/{reference,session,attendee}.ts`, and `db/attendees/payment-provenance.ts` keep a new raw reference under the owner key and a DB-keyed blind equality index beside it. That index is deliberately NOT unique: several attendee/payment rows may legitimately represent one provider charge, and the claim must expand to all of them. `protected_state` is only a plaintext state word. `getRefundPaymentReferences` accepts an explicitly named, decrypted `currentPaymentId` and returns the exhaustive `complete | legacy_unindexed | provider_unknown | too_many_references` `RefundPaymentReferenceSet`; it never exposes only an unsafe subset. Its SQL names only the selected attendee ids and retrieves at most the declared ten references plus one proof of overflow per attendee. A non-empty historical `processed_payments.payment_reference` with a blank `payment_reference_index`, or a current PII payment id absent from that attendee's indexed identities, makes the attendee `legacy_unindexed` before provider I/O. An eleventh reference makes it `too_many_references` before decryption or provider I/O. An indexed reference without an authenticated provider tag makes it `provider_unknown`. Once any refusal is proved, the attendee cannot reach provider I/O; unindexed and oversized sets are also refused before their reference bodies are decrypted. `loadPaymentReference` requires owner-key `hyb:1` ciphertext and throws at the storage boundary for a raw or DB-key value. There is no live plaintext decoder. Historical rows with no usable owner-encrypted index stay unavailable to in-app refunds until the owner rebuilds them at `/admin/migrate/rebuild-payment-references`. The page lists the rows one run would rewrite, because a site can change provider and the stored row never says which. The action decrypts each stored reference with the request's private key. It tags each reference with the provider the owner states for that row, and an untagged row with no stated provider fails the run. It writes the encrypted value and its blind index back. Guarded conditional writes stop a replayed or concurrent run from overwriting a row another path has indexed. The action writes the attendee provenance pointer only for the row whose reference equals the payment id in that attendee's PII blob - the comparison a booking finalize makes - and reports the rows that left their attendee unqualified, so those attendees stay refused rather than being offered an action the refund path would reject. The rebuild contacts no payment provider and moves no money. Outside that action there is no population decrypt, no request-time recovery, no dual read, and no compatibility writer. Neither attendee save nor merge backfills them. A payment stored before the reference column existed has no row to rebuild. Those payments stay on the manual provider-refund path. `attendees.pii_payment_session_id` makes that boundary queryable without opening PII. `NULL` means historical or otherwise unqualified; `''` proves the encrypted PII had no payment id; a non-empty value names the exact `processed_payments.payment_session_id` whose owner-encrypted, blind-indexed reference proves the PII id. A normal paid booking writes the pointer in its existing all-or-nothing booking transaction. A callback placeholder writes it through `attendeePaymentProvenance` in the placeholder transaction. A later balance payment never changes the pointer, so it cannot make an older PII-only deposit look complete. Merge propagates a source `NULL` to the target in the same merge transaction, so deleting the source cannot erase evidence of hidden old history; a source `''` or non-empty qualified pointer leaves the target's provenance unchanged. Pruning retains the exact processed row named by a non-empty pointer. The PII-free Refund All summary treats `NULL` as `legacy_unindexed`, even beside a newer indexed balance, and refuses before selecting, decrypting, or calling a provider. `prepareClaimedAttendeePaymentAnchor` accepts only a provider-tagged identity and is called only by the validated callback placeholder transaction. It prepares the owner-encrypted anchor and its canonical `PaymentRowState` `checking` claim as one insert; the attendee, booking rows, indexed reference, destructive-write fence, and conservative original-session outcome therefore commit atomically. It cannot be called by attendee save or merge; the architecture test pins that sole production caller. Distinct old deposit, balance, merge, session, and PII-only references remain unsupported by the current in-app refund path. `legacy_unindexed` and `provider_unknown` are derived historical-data refusals, not runtime compatibility paths or persisted work, and neither acknowledgement nor a generic clear retires them. Historical application behavior never assigned one payment ID to separate attendees, so this path does not scan or decrypt unrelated attendee PII for a hypothetical old sharing case; indexed representations and merges still expand by blind identity. - **Provider ownership.** New checkout, callback, and placeholder writes carry the provider inside the owner-encrypted reference. Admin admission accepts only that provider-tagged identity. An untagged indexed reference returns `provider_unknown` before loading any provider; it is never searched for, inferred from current configuration, or bound by a runtime repair. The old provider-discovery and binding modules were deleted with their callers and tests, and the architecture gate rejects their return. Historical returned markers do not attest a provider or amount. A tagged provider is authoritative, not a search hint: if it is unconfigured or its read is missing, unavailable, or invalid, the row stops there and no other adapter is tried. Every refund-facing adapter load receives the complete `TaggedPaymentReference` and goes through `loadRefundProvider`, which loads only `reference.provider` and verifies that the returned adapter matches the tag. Refund code cannot accept a bare provider type, consult current/last configuration, enumerate stored credentials, call raw `loadPaymentProvider`, or import provider adapters directly. Readiness, capability, and dispatch remain per reference, so one merged attendee may safely carry payments from different providers. `provider_unknown` is a bounded typed limitation, not recoverable work. The attendee page explains that the old row did not record its provider and does not render a Refresh form whose route can only refuse; Single Refund, Refund All admission, and a direct Refresh request all remain fail-closed with zero provider calls. The old ambient provider-to-dashboard guess was deleted too: an untagged payment id is inert display text, with no provider dashboard link or external-link target even when one provider is currently configured. The owner handles that historical payment in the provider dashboard; current refund code never qualifies it. - **Provider boundary.** `payment/{provider-read,provider-failures,refund-attempt,refund-network}.ts` and the Stripe, Square, and SumUp adapters replace nullable reads and booleans with `found | missing | unavailable | invalid` and `completed | accepted | rejected | not_sent | uncertain`. Stripe and Square send the exact captured minor units and currency admitted from the read. SumUp exposes only a full-refund-by-transaction operation, so the exact admitted transaction is its send boundary rather than an amount argument. Stripe checks `captured`, `paid`, and `status`, then derives Money from the documented `amount_captured`, `currency`, and `amount_refunded` fields; the provider's boolean `refunded` flag is never enough. Square validates payments, refunds, and orders at the wire boundary and treats only an explicit 404 as missing. Its payment webhook admits exactly `APPROVED`, `PENDING`, `COMPLETED`, `CANCELED`, and `FAILED`: a missing, non-text, empty, or unknown status throws at the boundary; a known non-completed status is the only status acknowledged without processing; and `COMPLETED` must also carry its Order id. The shared `isSquarePaymentStatus` declaration and `test/shared/square-provider/webhook-fields.test.ts` pin that set, so a new provider word cannot silently become a safe skip. SumUp never turns a missing proof into a full return: a top-level `REFUNDED` transaction without at least one refund event is invalid `missing_documented_resource`. A `SUCCESSFUL` transaction with an absent or empty event list means zero returned. Every present event must name a known type, a chargeback invalidates the read, and every refund event must have a supported status plus a valid amount in the transaction currency. `providerFailure` gives every adapter the same read-side transport table: 404 is `missing`; 400/422 is invalid `rejected_request`; 408/504 is unavailable `timeout`; 429 is unavailable `rate_limited`; every other HTTP failure is unavailable `provider_error`; a malformed answer is invalid; and a connection failure is unavailable. An ambiguous send gets one bounded reread, never an adapter loop. SumUp also treats a rejected response as local evidence about that request, not proof that no refund happened beside it. Every SumUp result except `not_sent` takes exactly one fresh charge read. Its rejection goes through the same `refundOutcomeAfterReread` judgment Stripe and Square use: a fully returned charge completes from that fresh observation, and only a fresh `found` charge that is still `ready` preserves rejection. Pending or conflicting money becomes `uncertain: observed_refund`; mismatched captured money becomes `uncertain: mismatched_money`; a missing read becomes `uncertain: missing_documented_resource`; and an unavailable or invalid read becomes uncertain with that read's exact reason. None invent completion, and none lets a request-local rejection release the claim as conclusive when the fresh charge itself is unreadable. `test/shared/sumup/{transaction,provider-money}.test.ts` and `test/shared/payment/refund-attempt.test.ts` pin the wire meaning, one-read count, and this evidence table. - **Canonical authority storage.** `db/migrations/2026-08-10_refund_authority_records.ts` rebuilds the previously unwritten `payment_charges` table only after proving it empty; a populated table makes deployment fail rather than reinterpreting unknown data. Its schema stores provider, owner-key-encrypted reference, blind reference and callback replay identities, exact captured/refunded Money, provider capability, one validated state document, its SQL mirrors, next due time, and revision. Reference and callback identities are each globally unique. The JSON/mirror checks make a state disagreeing with its indexed work columns unstoreable. Exactly two modules form its one writer boundary: `db/provider-refund-authority.ts` creates identities and binds callback evidence, while `db/provider-refund-authority-change.ts` owns every Money and state transition as one revision-conditional statement. The architecture test rejects any third writer. `db/provider-refund-cases.ts` owns bounded case reads and decrypts the reference only for the owner-only detail page; `db/provider-refund-case-resolution.ts` applies the exact decision and writes its activity audit in one transaction. - **Exact claims and retry capability.** `payment/{row-state,claim,review,admit-move}.ts`, `db/payment-claim{,/scope,/take}.ts`, and `features/admin/refunds/{claim,dispatch,authority}.ts` admit only a complete indexed attendee set. Inside the claim transaction, the command rereads every non-empty reference row owned by the selected attendees, including blank-index rows, plus indexed sharing rows. That expansion is hard-bounded: `MAX_SHARED_PAYMENT_ROWS_PER_CLAIM` admits at most 100 rows outside the selected attendee set, and the SQL reads at most 101 so the last row is only an overflow sentinel. Seeing it returns the typed `too_many_reference_holders` refusal before any sharing row's encrypted `failure_data` is decrypted, before a claim write, and before provider or ledger I/O. The refund and Refresh surfaces map that finite result to exact operator copy; they never truncate the sharing set. A blank-index row present at initial load yields `legacy_unindexed`; an indexed but untagged reference yields `provider_unknown`; and a row that appears between load and claim makes the exact row-set comparison return `changed`. None can reach the provider authority. A blank index is a separator, never an equality value: it is omitted from the sharing lookup, two blank indexes never make rows holders of one charge, and a selected set with no non-empty indexes skips the sharing query entirely. The transaction also fences each attendee's exact `pii_blob` revision and exact `(attendee, session, reference index)` set. Any PII revision change or payment-row addition, deletion, or reassignment between load and claim returns `changed` for the whole command; no row receives a partial claim. The claim expands matching indexed representations and preserves the initiating attendee scope after a merge. `processed_payments` holds only this short-lived `checking` fence; it stores no provider send phase, capability, or completion projection. `checkingClaimFor` is the one constructor for that fence, used by both ordinary claim acquisition and the automatic placeholder anchor. There is no second hand-built claim shape for callback recovery. `test/shared/db/payment-claim/take/shared-references.test.ts` proves the blank-index separator and zero-index-query rules, proves the 100-row edge is accepted, and proves 101 is refused without decrypting even deliberately corrupt shared state. `test/shared/db/payment-claim/{admission,take}.test.ts` prove the exact row-set and attendee-revision fence. The readiness admission regression proves the typed refusal remains finite at the feature boundary. `payment/refund-{authority,authority-state,authority-choice,authority-lifecycle,conflict-decision,provider-authorization,replay-window,request-identity}.ts`, `db/provider-refund-authority.ts`, and `shared/provider-refunds.ts` and `shared/provider-refunds/{target,send,state,work,budget}.ts` own the irreversible lifecycle. One globally unique blind reference index identifies the charge, one unique callback index deduplicates callback delivery, and every transition compares an exact revision. `ready` means evidence permits one attempt; `send_armed` means that exact generation may have crossed the network boundary; `observing` waits for fresh evidence; `completed` separately tracks provider return and local Money recording; `needs_owner_choice` means the stored evidence schema admits at least one specific owner answer; and `needs_provider_check` means it admits none and only another observation may advance it. Both attention states block sends and destructive deletion. An observe-only check may retire an ordinary ambiguity when fresh provider evidence proves the money fully returned. Fresh partial, invalid, backward, wrong-currency, excessive, or pending evidence instead replaces that ordinary ambiguity with `needs_provider_check`, advances the authority revision, and preserves or raises the proved returned-Money floor. It therefore invalidates a stale “not sent” form rather than letting an old guess erase newer evidence. Identical provider-check evidence is the only recheck that preserves the current revision. Any changed returned amount or changed conclusion advances it, including evidence that moves backwards and therefore becomes `wait`. A conclusive `provider_conflict` owner-choice revision is immutable under later reads. Stripe and Square declare `keyed`; SumUp declares `keyless`. Keyed replay uses the same identity only inside its finite provider window. Keyless work never automatically resends an armed generation. A fresh `accepted` result stays `observing` until provider evidence proves the return; an idempotency key is not settlement proof. `ProviderRefundTarget` makes send versus observation explicit. A clean observe-only read with no existing authority returns `unchanged`; it does not invent durable work. Once an authority exists, callback and admin entry points both reconcile that same row through the exhaustive `changed`, `needs_owner_choice`, `needs_provider_check`, `pending`, `ready`, `returned`, `unchanged`, or `withheld` result. `unchanged` is possible only for a clean observe-only probe with no authority to create; `changed` is the explicit lost revision result. A `ready` authority cannot remain a dead Send control when its provider read is unreadable. `missing` or `invalid` evidence moves it immediately to `needs_owner_choice/provider_unreadable`; `unavailable` gets one finite five-minute grace from `readyAt`, then the same transition if it still cannot be read. Every such attempt makes zero provider refund sends. The owner page explains that no refund was sent and requires the ordinary explicit provider-returned/provider-not-sent choice; it never offers a generic clear or a second send path. Only `shared/provider-refunds/send.ts` can mint `AuthorizedRefundRequest`, and every provider adapter requires that permit. The architecture test rejects any second mint import, adapter/API caller, `payment_charges` writer, legacy refund path, or live read/write of `provider_refunded_at`. It also rejects current/last provider selectors, `existingPaymentProviderState`, the deleted `orderedCredentialedPaymentProviderTypes`, raw `loadPaymentProvider`, and direct adapter imports in refund-facing code. `loadRefundProvider` has an exact production allowlist: admin refund readiness and the provider-refund authority. Attendee-claim settlement still matches the exact command id, lease time, and `checking` phase, so a stalled predecessor cannot release a successor's fence. `paymentRowStateStatement` writes the encrypted `failure_data` record and its derived plaintext mirror in ONE conditional statement, matching the exact prior `failure_data`. `PaymentRowSettlement` requires an explicit claim keep/release decision and changes only the optional books/review facts the run proved; an absent decision never clears an older repair target, review, or terminal outcome. A CAS loser changes nothing and cannot overwrite a successor. Paid-balance completion uses `db/payment-finalize.ts:balanceFinalizeStatements`, whose transaction guard aborts while an admin refund claim holds that attendee; the callback retries instead of racing a booking-finalization write against money leaving. - **Bounded orchestration.** `features/admin/refunds/{attempt,budget,candidates,claim,dispatch,provider,provider-requests,readiness-run,waves}.ts`, `subrequest-budget.ts`, and `db/client.ts` price physical provider retries, database retries, rollback, settlement, and caller tails at several checkpoints before fresh provider I/O. The readiness budget prices every active tagged reference from its stored provider identity. It has no configured-provider argument and never assigns zero cost because credentials appear absent; ambient settings cannot change refund admission. A selected command that cannot fit refuses without narrowing or sending. Dispatch reserves the exact post-arm send, bounded reread, and returned-money recording allowance around the arm transaction itself. Claim acquisition runs inside the settlement reserve; it is not counted a second time in the plan. An arm retry therefore cannot consume the permission it is about to persist: exhaustion before commit is proved `not_sent`, while a durable keyless `send_armed` phase is reserved for a genuinely uncertain dispatch. The engine's declared database envelopes price each statement once: one call for an already-known terminal authority, four for an observation that creates durable work, five for an active send, plus one shared retry ladder per admission plan for the odd contended statement. Pricing every statement at every physical attempt refused ordinary one- and two-payment refunds under real route spend, so admission no longer does that; a run that outlives its shared headroom stops at the subrequest guard and finishes through the durable recovery states. The reserves guarding irreversible windows still price every physical attempt: eight calls held while a provider call is in flight, and eight for settlement. Returned-work retirement is not pre-reserved: a completed return whose local books are due is canonical recoverable work, not an unsafe partial send. A deposit-and-balance pair now fits from the request boundary; five independently retryable sends, or eight observations, still refuse before either provider read. Provider calls overlap by at most five. One attendee may still carry up to ten references, so even the one-attendee Refund All batch can refuse safely before its first provider call. `logger.ts:withDeferredErrorReports` queues non-critical activity, notification, and Sentry fan-out until the money-critical command finishes, and flushes the queue even on a throw, so diagnostics cannot consume the subrequests reserved for settlement or rollback. Nested scopes share one outer flush boundary, and independent or overlapping errors each persist; only recursive reporting caused by that persistence attempt is suppressed (`test/integration/logger/log-error.test.ts`). - **Refund All admission.** `db/refund-all-candidates.ts` first computes a PII-free whole-listing count and detects any visible `review`, `unrecorded`, or incomplete historical-payment blocker among that same complete refundable set. Its booking CTE requires the event-group-scoped ledger sale and payment facts, so a paid booking with no reference-bearing processed-payment row contributes a `legacy_unindexed` blocker without reading its PII; a free booking or an abandoned checkout with only a sale contributes nothing. Together those facts prove only that the booking took money, not its raw provider reference or provider ownership. Settled non-candidates keep their own protection and repair state but cannot strand an unrelated refund. The summary runs before selection, so any SQL-visible blocker on a refundable candidate refuses the whole command. The GET/summary route decrypts zero attendee PII blobs. The POST batch loads at most one encrypted blob, with existing claims first, and decrypts zero when the summary is blocked or empty and exactly one otherwise. It never decrypts an attendee array and then slices it. Typed candidate admission catches a current PII payment id with no indexed identity and a row that appears after the summary; an incomplete attendee rejects the request before provider I/O. That historical reference remains a manual provider-dashboard refund; no later milestone is expected to qualify it for this command. The selected attendee passes through the same claim and budget admission as a single refund. `features/admin/refunds/candidates.ts:getRefundCandidates` drops quantity-zero rows and deduplicates by attendee before loading references, so several booking rows for one person consume one place, one tally, and one orchestration run. One submission retires at most one person and reports the remaining count; another submission takes the next person. This conservative batch size follows the proved Bunny envelope for one attendee's complete reference set; the provider overlap ceiling remains five within that attendee. M7 still owns a durable cursor/job that remembers and resumes the operator's whole-listing intention after a crash. - **Money settlement.** `shared/accounting/{queries,store}.ts`, `shared/refund-ledger/{plan,result,record,log}.ts`, and `features/admin/refunds/{ledger-findings,result-findings,provider,claim,refresh}.ts` carry exact `recorded`, `unrecorded`, and `review` findings back to each held reference and settle each attendee independently, so doubt for one person cannot hold proved-finished neighbours. A provider-confirmed return is conservatively `unrecorded` until the ledger disproves it. Every returned observation enters the settlement findings before the fallible canonical authority write. If that write fails for the same charge or a sibling, all returns already proved in the wave remain exact `unrecorded` work; the authority error still propagates. If another provider read fails, successful sibling observations are likewise not discarded: returned money remains exact `unrecorded` work, in-flight money retains doubt, a contradiction becomes review, and ambiguous provider identity beside money movement stays `in_doubt`. An unexpected preparation throw likewise keeps every still-unproved row in doubt. `underAttendeeClaim` attempts settlement on both the returned and thrown paths; it never promises that settlement itself cannot fail. A failed settlement may retain the claim and is propagated when work otherwise succeeded. A ledger throw first turns every provider-confirmed return into exact `unrecorded` work and keeps every still-unproved row in doubt, then attempts settlement before propagating the original error. A secondary settlement failure is reported without replacing that root failure, and a successful return is not exposed to the caller until settlement lands. Accumulated evidence — never control-flow optimism — decides what may release. Repeated repair preserves the first `unrecorded.returnedAt` date (`test/shared/db/payment-claim/unrecorded-date.test.ts`). The batch ledger uses one bounded snapshot: a stored conflict parks only its attendee, while a database failure marks every unproved plan unrecorded. The bound is executable, not aspirational: `REFUND_LEDGER_BATCH_DATABASE_CALLS` is four, and `test/shared/refund-ledger/record/batch.test.ts` proves thirty returned attendees still take exactly those four database round trips. `test/features/admin/refunds/readiness-findings/authority-failure.test.ts` pins the before-authority ordering for one returned charge and returned siblings. - **Confirmation and review.** `features/admin/refunds/confirmation.ts`, `db/refund-confirmations.ts`, `db/payment-review.ts`, `db/notes/{queries,types}.ts`, and migration `2026-08-10_refund_authority_records.ts` store one replay-safe confirmation keyed by the attendee and sorted provider-aware blind indexes. The confirmation, held-row assertion, generic activity, and optional named confirmation note commit together. Confirmation never scans, decrypts, or deletes historical note history, including for an old anchor. Old manual-refund warnings are compatibility display history only: a successful indexed confirmation leaves them visible beside the authoritative confirmation, and the owner may delete each note through the ordinary note action. This deliberately trades automatic cleanup of old display history for constant request work; do not restore a history scan to tidy it. Local `PaymentReviewReason` stores only `shared_reference` and `partially_returned_obligation`. Acknowledgement stamps the exact case revision to record that the owner saw it; it does not clear the marker, authorize a refund, or decide an allocation. The evidence-owned clearer is exhaustive: `shared_reference` retires only after one unique indexed representation is proved, and `partially_returned_obligation` only after every exact reference is returned and recorded. Provider uncertainty and charge conflicts never enter that local review schema. `provider-refund-cases.ts` exposes the canonical authority's own revision. `payment/refund-authority-choice.ts` derives the only legal action from the exact stored evidence: ordinary uncertainty offers returned or not sent; an exact zero-return conflict offers only not sent; an exact full-return conflict offers only returned; and a partial return or invalid, backward, wrong-currency, excessive, or pending evidence offers no money answer and can only be checked again. The stored union makes those last cases `needs_provider_check`, while `needs_owner_choice` is constructible only with a non-empty legal choice set. A fresh partial or inconclusive reading replaces an ordinary ambiguous choice instead of leaving a stale not-sent answer available. `money_recorded` is the separate proof that the local ledger caught up. There is no generic clear. `PaymentWorkStatus` is exactly `clear | moving | needs_money_record | needs_provider_recovery | needs_review`. A live claim comes first, then an explicit owner review, then mechanical ledger repair, then canonical provider recovery. In particular, a generic repair action cannot hide the decision it is waiting for. The local-review form HMAC binds the complete sorted `[sessionId, caseId, reason]` set; its transaction rereads that set, stamps only unacknowledged cases, and writes one activity entry. Any claim blocks acknowledgement, regardless of age or phase. Managers can neither acknowledge review nor send refunds, in the rendered UI or at GET/POST authorization boundaries. Refresh intentionally remains available to an authenticated manager because it observes and repairs existing work but has no provider-send permit. The current pending copy says both “Do not send the refund again” and “Refresh payment status after it completes”; it never labels a resend as the recovery action. - **Canonical Refund recovery.** `db/provider-refund-cases.ts`, `features/admin/privacy.ts`, and `ui/templates/admin/provider-refund-cases.tsx` derive the owner queue directly from `payment_charges`; there is no copied review or warning-note index. The queue keyset-pages twenty summaries without decrypting references. Only its owner-only detail opens the reference. `ready` renders one clearly marked Send action; `send_armed` and `observing` render observation only; `completed/due` requires the separate Money-recorded confirmation; `needs_owner_choice` renders only the answer justified by the exact conflict table above; and `needs_provider_check` renders only Check again, never a returned/not-sent choice. Stored Money is displayed with its own currency divisor, symbol, and code, never the site's current currency. A not-sent answer increments the generation and leaves `ready` work; saving the choice itself sends nothing. Every form binds the exact id and revision. The server reloads the current evidence, rederives its legal choices, and rejects a crafted submitted answer that the evidence does not admit; the authority stays unchanged. A ready Send carries the bound pair into authority admission and loses before provider I/O if the inspected revision changed. The same fence covers an active transition: a form rendered for revision 1 cannot check or send after revision 2 has become `send_armed`. An owner money decision compares its pair inside the transaction that changes the authority and writes its finite activity audit. Check again is observe-only: its POST first refuses a stale submitted revision, and any concurrent change after the read loses the transition CAS and is reread instead of overwriting newer evidence. An audit failure rolls an owner decision back. The HTTP and state-level races are pinned by `test/integration/server/privacy-refund-recovery{,-active-revision,-race}.test.ts`, `test/shared/db/provider-refund-cases-validation.test.ts`, and `test/shared/provider-refunds/state-owner-revision.test.ts`. `refund-authority-lifecycle.ts` exhaustively declares admitted evidence actions, the clearer, whether a choice is required, the real route, pruning, and merge/delete policy for every state. The architecture test proves every declared clearer is exported and every route is registered; the Cucumber story proves each rendered control reaches the real exit. - **Review retirement and reachable repair.** `payment/admit-move.ts` has one exhaustive `PAYMENT_ROW_LIFECYCLE` entry for every non-settled `PaymentRowState` field; the winning entry supplies its mirror, status, recovery action, refusal, and delete/merge behavior. The recovery action is constrained to the real attendee-action schema. `db/payment-admit-move.ts` turns those row mirrors plus canonical charge work into one `PaymentMoveSnapshot`: a `delete | merge` admission record and the operator-facing `PaymentWork`. Both page rendering and transactional writers consume that same decision. The attendee Actions page therefore does not offer Delete to either an owner or a manager while payment work blocks it. A direct GET of the delete URL returns 400 and renders the reason without a confirmation form. Claims block both delete and merge; review, unrecorded Money, and canonical provider work block deletion but remain mergeable because their indexed row moves and the global charge authority does not. `features/admin/{attendee-page,attendee-page-data,attendee-payment-review,attendees-route-helpers}.ts` `ui/templates/admin/attendees.tsx`, and `ui/templates/admin/attendees/delete-confirm.tsx` render from authoritative indexed payment work, not legacy PII `payment_id`, so a merge cannot hide the only Refresh form. The per-attendee Refund action is admitted from that same complete reference set, `refundWorkRemains`, and an active booking line. It is absent when no tagged automatic payment exists, no active booking remains, or the exact authority says the cash work is finished; a legacy PII id or paid display value cannot make the action appear. These rules are pinned by `test/features/admin/attendee-page-data/refund-actions.test.ts`, `test/features/admin/attendee-page-data/refunds-ui.test.ts`, `test/features/admin/attendee-page/actions.test.ts`, `test/integration/server/attendees/{delete,payment}.test.ts`, and the real visitor journey in `specs/payments/recovering-the-money-record.feature`. Clean evidence retires only the exact local review it proves gone. `PAYMENT_REVIEW_RETIREMENT` is exhaustive over its two stored reasons: `shared_reference` needs one unique indexed representation, while `partially_returned_obligation` needs every exact reference returned and recorded. `partial_refund`, `refund_exceeds_capture`, `multiple_pending_refunds`, and uncertain keyless sends live only in the canonical provider authority and retire through Refund recovery. They are never copied into a parallel local-review lifecycle. Acknowledgement retires neither local reason. - **Every destructive consumer.** `db/payment-admit-move.ts`, `db/attendees/dependent-data.ts`, `db/attendees/delete.ts`, `db/prune.ts`, `merge/attendee-merge.ts`, and `db/orphan-attendees.ts` make claims block merge and delete, while review and unrecorded money block delete and travel with a merge. The same admission joins matching `payment_charges`: every unfinished generation, owner choice, or completed return whose Money is due blocks deletion. Merge is relocation rather than destruction, so it may move the indexed processed reference while the globally unique charge authority stays in place; merge never copies or reparents that authority. The surviving attendee therefore reaches the same unresolved row and its real recovery route. The preflight is guidance only: merge and delete repeat the same admission on their own write transaction, so a claim that arrives after the page rendered still wins and rolls the destructive write back. The two lifecycle-derived plaintext mirrors gate prune and orphan purge. Empty local-row work permits a referenced payment row to age out only when that row's exact blind reference index matches a prunable canonical charge authority. An attendee-wide `refund_cash` leg and a returned sibling charge are never deletion authority; an ambiguous legacy reference with no exact canonical match stays retained. This preserves ordinary retention for the exact settled row without deleting another charge that is still paid. Conversely, any non-empty `protected_state` or non-prunable refund-authority state prevents pruning however old or stale it is: staleness can make recovery possible, but never makes a safety record disposable (`test/shared/db/prune/payments.test.ts`). Merge admits both source and target inside the transaction, because the source rows move and the target set grows. It relocates only reference-bearing rows already admitted by that transaction; it never mints a row from either attendee's PII payment id. This prevents both a false `shared_reference` case and a provider-less identity from becoming live authority. `ATTENDEE_DATA_RULES` exhaustively declares delete, repoint, or retention for every attendee-linked table, and production delete statements derive from it. Its schema tests walk the live database, reject undeclared attendee-id-like columns and payment tables, require every named column to exist, require children before parents, and prove production emits the declared operations (`test/shared/db/attendees/{dependent-data,delete}.test.ts`). Protected orphans are excluded from both scheduled and manual purge. The owner-only “Outstanding payment work” queue at `/admin/privacy` starts at the partial state index, selects distinct attendee ids, and keyset-pages twenty plus one lookahead without loading or decrypting attendee PII. It links by attendee id to the still-live attendee page. Review and Refresh are attendee-scoped, so their route and rendered control remain usable after the final listing is deleted (`test/integration/server/privacy.test.ts`, `test/features/admin/attendee-page/actions.test.ts`, and `test/features/admin/attendee-payment-review.test.ts`). Canonical provider work has its own “Refund recovery” list on the same owner page and links by authority id, so it remains reachable even when the listing no longer exists. - **Migration and restore parity.** Migration `2026-08-10_refund_authority_records.ts` installs the partial `processed_payments(attendee_id) WHERE protected_state != ''` recovery-queue index. `schema-sync.ts` emits partial predicates and treats columns used only by a predicate as dependencies; historical restore support finds the same dependencies from `sqlite_master.sql`. Fresh installs, upgrades, and old-schema restores therefore converge on the same bounded index. - **Fail-closed booking/provider errors.** `payment/checkout-failure.ts` exposes only provider, a finite reason, and an optional numeric status; Stripe maps it in `stripe/runtime.ts`, SumUp in `sumup.ts`, and Square in `square/checkout.ts`. Recognized provider/network/timeout/invalid-response failures carry no raw body, message, cause, request id, session id, or buyer contact into retained diagnostics. Unknown application errors keep their identity and propagate. `classifySessionIntent` distinguishes `ready`, foreign/unverifiable input, and unreadable trusted input: a valid price proof with a malformed booking intent, a provider network/API failure, or a malformed non-null checkout response fails before reservation, booking, or refund and returns a retryable 503 where appropriate. Only expected `PaymentUserError` input becomes a visitor-facing refusal; Square's buyer-correctable email/phone validation remains in that class. A completed Square callback whose ticket-shaped metadata omits or corrupts `price_proof` is unreadable trusted input and remains retryable; it is never acknowledged as foreign. For that early Square malformed-versus-foreign distinction, any non-empty `_origin` remains an application marker across a hostname change; it is not compared with today's domain. It is not ownership proof — only the signed `price_proof` supplies that — but it prevents an older app checkout with damaged signed metadata from being terminally acknowledged as an unrelated order. `processed-payments.ts:parseSessionFailure` likewise lets decryption or schema corruption throw at the stored-data boundary instead of inventing a generic handled failure. The booking integration tests pin thrown database and application errors, malformed signed intents, the hostname-change marker, and the real Square webhook cases so none can become a silent success response. - **Recoverable automatic placeholders.** `db/payment-anchor/{reference,attendee,session}.ts`, `db/attendees/{create,create-batch,payment-provenance}.ts`, `db/provider-refund-authority.ts`, and `features/api/payment-processing/{refunds,store-refund}.ts` prepare the paid session's provider-tagged reference, ready refund authority, and conservative terminal result before any provider refund I/O. `prepareClaimedAttendeePaymentAnchor` derives the same canonical `checking` claim as admin work through `checkingClaimFor`; `prepareRefundAuthority` prepares the same `payment_charges` insert used by ordinary refund admission. One `createAttendeeAtomic` transaction then writes the quantity-zero attendee, every booking row, the owner-public-key-encrypted reference and blind index under a synthetic `legacy:` anchor, the exact `PaymentRowState` claim, the attendee's non-empty provenance pointer, the `ready` charge authority, and the terminal result on the original payment session. The prepared statements run as one batch, and every expected write is checked. Failure to write any member rolls the attendee, bookings, terminal result, provenance, anchor, and authority back together. There is no committed state in which the callback is terminal but the refund has no durable recovery authority. The synthetic anchor attests only that this attendee owns that tagged reference: it is not a sent attempt or provider completion. After commit, `requestSessionRefund` passes the already-validated callback's captured Money to `requestProviderRefund`, which loads and advances that same ready authority before a fresh provider read; it does not create a second authority or refund path. The read still gates every send. An unavailable read therefore leaves reachable canonical work instead of a note that deletion can erase; a read whose captured Money disagrees becomes a durable `provider_conflict` owner choice. The quantity-zero attendee cannot be deleted or pruned while that authority is active. It can be merged once its short-lived claim settles because the authority and indexed reference travel safely. The explanatory system note contains only the finite placeholder reason and is never a safety authority, named work item, or clearing mechanism. No warning-note refund path remains. `storeRefundedBooking` releases the anchor claim only after the provider result, placeholder ledger post, any canonical-authority Money recording, refund activity, and explanatory note have all finished. A throw before that point leaves the atomically minted claim protecting the attendee; it cannot expose an unclaimed deletion window between attendee creation and recovery work. Settlement uses the prepared exact command, timestamp, session, and `checking` phase, so it cannot release a successor's claim. Only after that settlement does `sessionFailure.replace` compare-and-set the conservative terminal outcome to the exact final result; it cannot overwrite a racing or unrelated outcome. The terminal outcome and ready recovery authority already exist when placeholder creation commits. If the caller loses that committed reply, a redelivery replays the same attendee, indexed anchor, and authority without a second create or provider send. The indexed anchor makes the attendee's real Payment Details render immediately; canonical recovery lives in the owner-only Refund recovery queue, and no attendee save or population decrypt is needed. This does not make the provider result or placeholder ledger post part of the creation transaction. M7 still owns the original checkout's durable handled/effect marker after `processed_payments` retention and the placeholder-ledger replay-marker gap; it must attach that work to this authority, never resurrect a legacy replay path or reinterpret the synthetic anchor as the marker. `test/features/api/payment-processing/{store-refund,store-refund-authority}.test.ts` exercises all-row rollback, atomic terminal/anchor/provenance/authority creation, tagged identity, recovery after a failure immediately after commit, and deletion refusal while work remains. `test/features/api/payment-processing/index/refunds.test.ts` exercises the lost committed-reply replay. - **Privacy-safe diagnostics.** `shared/payment-review.ts`, `features/admin/refunds/report.ts`, `shared/refund-ledger/log.ts`, and `shared/invariant-errors.ts` accept closed reasons and row/count context, not arbitrary provider references, attendee names, or payment-session ids. The safe description and caught exception travel separately, preserving the original stack for Sentry without copying money identifiers into console, notification, or retained activity text. Adding a raw identifier is therefore a visible type-boundary change rather than an easy string interpolation. `payment_charges` deliberately exposes provider, Money, state, revision, and due timestamps to SQL, because the database must schedule and police the lifecycle. Its raw provider reference is owner-key ciphertext; its reference, callback, and request identities are DB-keyed one-way indexes. It stores no buyer name, email, phone, address, attendee PII blob, raw callback session id, provider response, or credentials. A database holder with `DB_ENCRYPTION_KEY` can read the finite explanatory placeholder note and lifecycle metadata. With a v2 password-wrapped owner key they cannot open new raw payment identifiers or buyer PII. A dormant v1 owner wrap can expose the data key and site private key from the same database and environment key, and provider credentials encrypted with the DB key may allow provider-side queries; both remain the explicit system-wide caveats above. - **Visitor-level proof.** The Cucumber stories `specs/payments/{checking-before-a-refund,waiting-for-a-refund,recovering-the-money-record,refunding-a-booking,refunding-from-two-windows,only-owners-refund,refunding-everyone-at-once,resolving-uncertain-refunds}.feature` cover the pre-send check, a delayed provider result, repair of returned money missing from the books, two-window races, owner-only controls, bounded Refund All requests, whole-listing blocker admission, and per-request failure isolation. `resolving-uncertain-refunds.feature` drives the real Privacy queue through ready send, provider observation, required owner choice, and separate Money recording. The stories submit the rendered forms and cross the real provider and ledger boundaries. Refund-safety purchases use the public booking page: Stripe follows the production completion route, while SumUp follows the real checkout staging write and unsigned webhook handler. No story rewrites a stored payment's provider to manufacture the state it tests. The focused regressions exercise Delete disappearing while work remains and returning after recovery, one old blank-index row stopping every Refund All send, and a PII-only deposit remaining a blocker even beside a later indexed balance. The final branch gate, rather than this plan, is the authority for pass counts. Known limits are deliberate and remain visible. Part A protects old history by refusing an incomplete or untagged selected attendee; it does not make that history refundable and never decrypts or backfills an attendee population. Attendee save, merge, and M11 do not mint payment rows from old PII. A historical or unqualified attendee provenance remains a permanent fail-closed boundary for current in-app refunds; the owner handles it directly in the provider dashboard. A later indexed balance cannot erase that boundary. This is a product choice, not deferred compatibility work: do not add a whole-table scan, a re-save backfill, a migration-only PII refund reader, or a parallel legacy refund engine. Refund All's PII-free summary can use booking ledger facts and the provenance marker to detect and block the incomplete history, but it cannot recover or provider-qualify the raw PII reference. Part A does not erase historical plaintext references or old DB-key-encrypted warning notes. It does not cut the buyer callback classifier over: `payment-processing/classify.ts` still judges a paid session from its signed total and currency and does not read `refundOutcomeOf` or charge-level refund evidence. A provider session that still reads paid after its charge was externally returned can therefore still enter the current booking completion path. The atomic cutover combines M6's whole-checkout reader with M8's durable completion and deletes that displaced writer before activation. Before a provider-tagged refund target exists, the current checkout entry still uses `shared/existing-payment-provider.ts` to choose the ambient current or last provider. A provider switch can therefore strand or misroute an in-flight checkout. A paid callback with a blank provider reference can still be terminally acknowledged, and an authenticated malformed Stripe completion can still be terminally dropped. These are whole-checkout admission and completion defects for the atomic M6/M8 cutover; they are not alternate paths the M4 refund authority should learn to tolerate. M6–M11 must replace and delete `getPaymentProviderForExistingPayments` and every runtime payment-ownership caller in the same activation. After that cutover, credential enumeration may exist only inside settings/activation recovery. It is never payment ownership and must not enter refund admission, budgeting, provider loading, observation, sending, or dashboard links. Part A also does not solve stable booking obligations, exact allocation, ledger order identity, a durable Refund All intention, or the original checkout's durable handled marker described below. Those are authorities at different layers, not piecemeal extensions to the provider-refund lifecycle. In particular, the automatic placeholder's atomic attendee/reference/terminal- outcome creation and `checking` fence plus its pre-read provider authority close the missing recovery-control, duplicate-create, and destructive-cleanup faults. They do not supply the original checkout's durable effect marker after `processed_payments` retention or close the placeholder-ledger replay-marker gap. M7 owns those exits on the same authority. Standalone value: no live admin refund path or admitted provider-tagged callback refund target can guess a provider, default malformed evidence into success, send without the one durable permit, silently lose a known returned refund after a local failure, decrypt a whole attendee population, or make a manager decide or move money. Whole-checkout callback admission remains the explicit M6/M8 cutover debt above; this statement does not turn its ambient provider selection or legacy terminal acknowledgement into M4 guarantees. #### M5: Payment cases — remaining aggregate case work (was PR 5) M4 Part A already ships the current-row case needed by live admin refunds: revisioned review evidence, an owner-only view and acknowledgement, automatic evidence-owned retirement, a reachable Refresh action, manager refusals, and merge/delete/retention rules. Acknowledgement records that the owner saw one exact revision; it deliberately neither clears unresolved money nor grants a send permit. There is no remaining honest standalone M5 foundation. Whole-checkout and booking-level cases cannot exist until M6 can read and persist their complete evidence, and their money-moving or completion choices cannot exist before the M7/M8 engines that execute them. Land each remaining case kind with that real producer and every action that resolves it, reusing the M4 lifecycle declarations instead of opening a parallel review system: - M6 adds revisioned cases produced by the whole-payment judge, the owner list and detail evidence, stable buyer result, and retryable current-revision alert. The durable case commits before best-effort notification; the ntfy copy carries only a finite case code, never buyer/provider identifiers or amounts, and directs the owner to the protected detail. Every rendered link is gated by the same permission and existence fact as its target. - M7 adds refund/allocation decisions and closes both the provider result and Money before retiring their case. M8 adds complete-booking decisions and durable completion effects. - A genuine ambiguity stores immutable reviewed evidence and a required choice with no default. A generic acknowledgement is never a substitute. A case re-judges on evidence change and closes only when the exact condition is gone or its required decision completed atomically. - Aggregate cases are repointed or settled inside attendee merge/delete transactions and are covered by the same declared storage lifecycle as every other attendee-linked table. Milestone value: every aggregate problem becomes visible and resolvable in the same slice that first makes it possible, with no dormant case vocabulary and no second source of truth beside M4's live-work rules. ### Atomic aggregate cutover — work packages M6–M11 M6–M11 ship as one cutover PR and one release. The package boundaries below are for implementation, review, and focused tests; none may become an independently merged live layer. Before activation, the release pauses payment creation, callbacks, refund commands, completion, attendee mutation, and pruning behind one durable cutover fence. Its explicit owner-authenticated migration reads the old tables in bounded resumable pages, writes canonical rows, adopts every in-flight checkout, and gives every source row either a verified canonical identity or a complete owner decision. After verification, one transaction raises the cutover epoch. Only then may canonical readers and writers serve requests. The displaced readers, writers, interfaces, and maintenance paths are deleted in this same release; there is no read-through, dual write, runtime fallback, or mixed-store answer. Frozen old tables remain only as migration audit input for M12/M13 redaction and removal. This is also the rule for every later change to M4's live refund authority, not just this named aggregate cutover. Extend the canonical state and every consumer in place, or fence requests, migrate and verify every retained row, switch one epoch, and delete the displaced schema, readers, writers, routes, and jobs in the same release. Record age, missing optional fields, provider kind, and deployment version may preserve evidence or cause a typed refusal; none may select a legacy runtime, fallback authority, dual write, or alternate refund engine. If a future change cannot make that atomic replacement safely, it does not activate. Every dormant aggregate table must hold its complete production role at that epoch or be dropped. The cutover cannot activate without the owner's private key: owner-encrypted retained payment evidence and buyer snapshots cannot be opened under `DB_ENCRYPTION_KEY`. That ceremony must not scan attendee PII to recover old payment ids. Until the owner performs it, the current M4 runtime stays wholly in charge; after it, old unindexed payments still require a manual provider refund rather than a legacy engine. #### Work package M6: Aggregate checkout creation and reads Src target: 1,200–1,800 within the sanctioned atomic-cutover exception. Creation and reads move together precisely so no `sumup_checkouts` projection, checkout-metadata preservation bridge, or projection-repair machinery exists. The dormant `payment_sessions` buyer-bearing fields currently accept DB-key `enc:1` ciphertext. Before the first M6 writer exists, rebuild those fields to require owner-key `hyb:1` ciphertext or drop the dormant table and replace it with the one canonical owner-key-protected session store. The current shape may never become a live compatibility store. The M6 release itself carries the restore-deploy guard (as its own commit is fine): once the cutover release has shipped, `.github/workflows/restore-deploy.yml` refuses to deploy any commit that predates the aggregate cutover — regardless of what the restored database contains, because a pre-cutover backup carries no aggregate marker and its recorded commit would restart the old writers. Restoring an old backup means loading it into the current application and completing this same migration ceremony before activation. Document this in the operator restore guide beside the backup's recorded commit; the guard is live before the first aggregate write can happen in production. - Creation: save immutable expected money and booking intent before every provider call; claim creation; one payment identity and idempotency key; adopt the original provider resource after an uncertain response; return the same buyer URL on replay; release or schedule every failed claim. All three providers at once. SumUp keeps its local payment, checkout, and transaction IDs distinct and uses stored currency. A checkout spanning several listings stays one payment with one shared order: the stored intent allocates the ticket-line money across its listing lines exactly — the existing largest-remainder rules make the parts sum to the whole — and keeps every booking fee or price-modifier extra as its own fact, never folded into a listing's share, because Money credits those to their own accounts. Listing lines plus extras sum to the payment total, reconciliation validates money against that allocation, and no listing is ever credited the full payment or another line's fee; the shared order is never collapsed. A deposit (reservation) checkout stores two levels for every line and extra: the amount charged now, whose parts sum to this payment's total, and the full obligation it represents, so the later balance payment knows what remains and the full modifier fact is never lost to the deposit fraction — today's pricing folds the deposit share into ticket lines while Money records the whole modifier, and both facts must survive as themselves. One canonical allocator produces this record for the whole payment — every listing line, every extra, and every price-modifier application as its own signed fact, discount or surcharge, even where today's pricing folds it into line prices — in one pass, each part with a stable identity. A reservation's deposit and balance payments share one booking-level obligation identity: the line, extra, and modifier identities are created once, under that obligation, when the deposit's allocation is stored, and the balance payment's allocation references those same identities — the full obligation is stored once and referenced, never duplicated, which is what lets M7 cancel it exactly once and M8 lease every payment that shares it. The record is stored with the payment: reconciliation, refund routing, and balance completion read the stored allocation and never re-derive it, so no two consumers can disagree about a part. Today's ticket-only `allocateReservationDeposit` does not survive the move. Store provider and currency on every aggregate charge. M4 already tags new current-store references and refuses every older indexed-but-untagged reference before provider I/O. The aggregate copy preserves old incomplete evidence as unsupported accounting history; it does not qualify that evidence for an automatic refund. Provider and currency are intrinsic to every charge the new runtime may act on. No migration or request-time attendee scan, runtime provider-discovery fallback, or configuration guess exists. Old PII-only and untagged references remain manual provider work. - Reads: keep M4's tagged `ProviderRead`/`RefundAttemptResult` contract for one charge, then add the whole-checkout observation only here, beside the reader that supplies its complete signed intent, session, charges, account, currency, amount, parent, and provider-specific child evidence. Square payment IDs are named by validated orders, not short-list scans. Missing, invalid, unavailable, pending, paid, free, and failed stay distinct. Only this complete reading may introduce the expected-money conflict kinds and whole-payment judge that M4 deliberately did not ship. The provider-specific traps remain binding: - Square coalesces the webhook-named payment with its own order tender so one charge cannot look duplicated, uses the order total as session total rather than one tender's amount, and reads refund evidence for every payment-backed captured sibling. An absent documented `refunded_money` means zero; a present but incoherent Money value is invalid. A card tender counts only when its status proves capture. A captured non-payment tender without a payment id is real captured money with unknown refund evidence: it parks for owner review, is never defaulted to zero, and is not subjected to an impossible payment read. - A checkout-linked SumUp observation is the named transaction plus the checkout's vouched successful children and, for a multi-child reading, each child's transaction refund evidence. A checkout-less historical reference can honestly carry only its transaction evidence. - Square tender and SumUp child lists are provider-controlled and therefore have one declared cap shared by callbacks, admin, and refresh. Exceeding the cap records bounded owner work without chasing the remaining list or moving money. - One claimed reconciliation function serves signed callbacks, buyer returns, manual refresh, case refresh, and scheduled retries: it reads, resolves via `outcomeOf`, and persists once — evidence, charges, state, due time, revision, and case changes in a single transaction. Store every charge identity once and reject cross-payment reuse. Multiple captures open an M5 case. Reconciliation fingerprints a stable, order-independent resource set: unchanged terminal evidence replays; later visibility of the application's own refund confirms it; genuinely new external evidence is merged and surfaced without re-running booking or refund. Evidence merges by stable provider-plus-resource identity. A lagging subset never removes a known charge; cumulative refunds keep their maximum and capture state moves only forward. Conflicting facts with no safe ordering, including parentage, currency, or session total, are retained as contradictions that block money movement. The whole merged reading is re-judged; an earlier outcome is never carried forward. A compare-and-set loser carrying distinct evidence retries boundedly or answers retryably, never acknowledges that evidence away. - At activation, the aggregate never references a deleted attendee: `applyAttendeeMerge` repoints aggregate payment sessions, charges, cases, effects, deliveries, and queued work to the surviving attendee inside its transaction, while `deleteAttendee` settles or repoints them before the attendee row goes. The migration fence pauses merge, delete, PII edits, pruning, and stale-reservation deletion before it takes its source snapshot, so no historical booking or payment-identifying fact can disappear between verification and copy. Pre-fence privacy deletion remains intentional; like a backup, the migration copies what still exists when the fence rises and never recreates data already removed under the current retention rules. - The aggregate readers replace `resolveWebhookSession` and `retrieveSession` for every caller — signed webhook callbacks, buyer return and cancel pages, and paid-session validation — and the displaced methods leave the `PaymentProvider` interface and all three provider implementations in this same cutover. They feed the durable completion and effect machinery described in M8–M10; no old completion, finalize, delivery, or maintenance writer remains active beside them. Every admin and callback refund already writes M4's canonical `payment_charges` authority. M6 links that same row to the complete payment and charge observation; it never copies its state into a second refund record. The claimed whole-payment reconciliation folds both provider evidence and that authority's current revision. Pull-only SumUp work is already due through `next_refund_action_at`, so no writer-through marker or provider-specific side queue is needed. This is an atomic replacement, not a selector between generations: after the epoch changes there is no runtime fallback to either displaced checkout reader, no read-through to old payment storage, no dual write, and no second completion or refund authority. Migration-only decoders remain unreachable from request handlers and are removed with their frozen sources. - During the fenced migration, adopt or expire every in-flight pre-cutover checkout (all three providers) atomically and idempotently: same claim, identity, and validation rules; concurrent callbacks and migration runs bind to one claim; defined outcomes for paid, pending, expired, and unavailable sessions. No paid checkout is stranded without an aggregate row. - Panels, exports, statistics, refund targets, callbacks, and maintenance switch to the canonical repositories only after every retained source row verifies. The old runtime readers and writers are absent from the activated build. The migration itself folds a source row's local completion facts — attendee, ticket result, and recorded failure — onto its canonical identity before declaring that source settled, so identity deduplication never drops the booking outcome. Cutover contribution: no live checkout can lose its intent or create a second provider checkout after an interrupted request, and every route, worker, page, and export gets the same authoritative answer for the same payment. #### Work package M7: Refund jobs and stable booking obligations M4 Part A is the authority, not work to repeat: admin single, one-attendee Refund All, Refresh, and buyer callbacks already share tagged reads, the revisioned `payment_charges` state machine, one provider-send permit, conservative settlement, required owner choices, and local Money repair. M7 adds a durable whole-listing intention and stable booking-obligation effects around that same provider authority. It does not build a second aggregate refund engine. The cutover links the current authority to stable aggregate identities and removes the processed-payment claim and reference paths whose callers have all moved before activation. - **Durable Refund All.** Before the first provider call, persist the operator's whole-listing intention, every immutable payment identity it covers, and a cursor. Each request claims, budgets, and processes one bounded page, records every result, and advances only past terminal items. A permanent refusal opens required owner work; a transient failure remains due for bounded retry and cannot be skipped by the cursor. A crash after page one therefore leaves a visible resumable job naming the remainder. M4's PII-free whole-listing safety summary and one-attendee interactive request remain the safe input shape, but repeated manual submission is no longer the only continuation. - **Extend one callback/admin lifecycle.** M4 already persists the exact charge, capability, captured Money, callback replay identity, generation identity, due time, and revision before a validated callback can send. A duplicate makes no second call. Stripe and Square may replay only the exact keyed generation inside its finite window; SumUp never automatically replays an armed keyless generation. `accepted`, lost responses, and contradictory reads all have durable, reachable exits in Refund recovery. The missed-SumUp-checkout task already runs on the current path as `sumup_checkout_recovery`; M7 adopts that task instead of building it again. M7 attaches scheduled refund retries and the original checkout's handled marker to this same lifecycle. It must not reinterpret a placeholder's `legacy:` anchor as the original checkout, a sent attempt, provider completion, or durable replay marker. - **Durable job inputs around the existing engine.** Individual, selected bulk, balance, automatic, callback, and case-decision refunds continue through `requestProviderRefunds`, now fed by M6's stable aggregate charge and allocation identities. Job items are self-contained and never rebuild their target from live attendee PII or current settings. Provider success followed by a local failure remains due on the authority and repairs Money idempotently. Refunds stay available while new sales are disabled. - **Stable booking obligations.** Cash return and booking cancellation are separate durable effects. A payment page returns and posts only the cash that charge moved. The stable obligation identity owns the sale, modifier, and fee facts; its cancellation is claimed with its decision and applied exactly once. If one payment returns while another remains captured, require the decided revision-fenced owner choice — keep the booking and make the return due, return the rest then cancel, or cancel while retained cash remains visible refund work. Recovery can retry either effect but cannot infer one from the other or cancel twice. - **Shared and ambiguous legacy evidence.** An indexed current-store reference with multiple representations remains M4 `shared_reference` review; its acknowledgement is not an allocation. The aggregate action either records positive Money parts, all in the captured currency and summing exactly to the capture, across stable obligations, or records an explicit owner rejection. Likewise, an ambiguous provider choice is limited to providers a fresh read revalidates as `found`. Every choice is fenced on the exact held rows and evidence revision. No equal, proportional, first-attendee, configuration, or current-row default is permitted. - **Changed evidence and destructive writers.** Re-run the outcome and blocking case checks in the claiming transaction before each provider call. New captured money, an external refund, a changed allocation, or a new case parks the item for the owner rather than acting on a stale snapshot. A queued refund built from pre-merge Money facts blocks merge/delete until it settles or is explicitly cancelled; repointing cannot make a frozen allocation safe after a merge has posted its own adjustments. - **Migration boundary and removal.** The cutover's M11 work package migrates and verifies every retained `processed_payments` record before enabling the aggregate runtime. It copies only payment references already present in retained payment storage; it never searches attendee PII for another one. Unsupported history receives a terminal accounting disposition and remains a manual provider refund, not an actionable canonical charge. This work package removes the processed-payment claim and reference paths as their final callers move, and the architecture gate continues to prove that only one send permit mint, provider API path, and `payment_charges` writer exist. Cutover contribution: a whole-listing intention survives request death, the one durable retry/evidence machine gains stable allocation and scheduled work, and cash or booking obligations can never move twice or be silently inferred from each other. #### Work package M8: Durable paid completion, including its failure path Src target: 1,000–1,500 — cutover exception; the success and failure paths are one machine and merge together. - Persist the exact capacity, attendee, answer, modifier, package, balance, and Money effects before running them; snapshot paid facts so listing edits cannot change delivery. Complete each effect idempotently, schedule unfinished work, and stop one permanent failure from starving later payments. A multi-listing payment draws each line's capacity, tickets, and Money from M6's stored allocation, but the commit is all lines or none in one transaction — as `createBookingAtomic` commits the shared order today — so one line selling out after payment sends the whole completion down the failure path (refund or owner case) rather than half-booking the order. That refund is an unhonoured-payment refund: it returns cash only and posts no booking-level obligation cancellation, because the all-or-none commit never posted the obligation — a cancellation runs only with proof the booking obligation effect completed. Before the effect runner claims its first payment, the fenced migration classifies every retained completion: a completed source result is marked done and is never re-booked or re-posted to Money; a paid payment with no completion result becomes due work; and a recorded completion failure becomes the matching durable failure effect — its chosen refund path or an owner case — never marked done and never re-run as a fresh booking. The migration begins only after in-flight old commits drain or fail and it revision-checks every source before settlement. Adoption also gates on a completion-safe `outcomeOf` state with no open blocking case: a paid payment stopped for owner review — captured money on a failed checkout, multiple captured charges — stays in its case workflow, because due work must never bypass a required owner choice. Refund and completion claims are mutually exclusive through one payment-wide claim: the payment session row's shipped lease (`lease_token`, `lease_expires_at`). Refund jobs, the adoption pass, and the effect runner each acquire that lease atomically before acting and verify no unfinished refund job or effect owns the payment, so two runners can never both read "nothing done yet" and act, and a booking can never complete while its irreversible refund is in flight. For a reservation whose deposit and balance are separate payments, the claim spans them: a runner acquires the lease of every payment sharing the booking-level obligation, always in one fixed order, before acting — so refund and completion can never split one booking between two sessions. - At activation, completion stops storing payment references in attendee PII — the aggregate owns the attendee-to-payment link. M4 already keeps every new `processed_payments` reference under the owner key and refuses to scan PII-only history. The cutover carries forward only links already proved by retained payment storage; it does not consume old attendee PII before deleting the writer. - When completion cannot be honoured, persist the chosen refund path and record the provider refund and local Money completion as separate durable effects driven by the M7 engine, with explicit provider, database, and total subrequest budgets. - The M5 complete-a-proven-booking case action lands here. - Extend M4's current-row merge/delete/prune/orphan protections to aggregate cases and effects. Fence listing deletion against pending payment work, establishing the claim before any irreversible step: `performListingDelete` now deletes the database rows before removing the stored attachment, and the fence must precede both. Fence attendee deletion the same way: an attendee with unfinished completion work or durable effects cannot be deleted until that work settles or is repointed, checked inside the deleting transaction. Repoint payment work and open cases during attendee merges. The cutover fence blocks merge, delete, prune, PII edits, and `deleteAllStaleReservations` while historical rows are being copied, so activated maintenance code can be canonical-only; it never needs a copied/not-copied branch or migration snapshot. - Install the old-write fence before the first migration page and verify it in every old committing transaction until all in-flight requests drain. Provider refunds need no exemption: M4 already writes their state only to canonical `payment_charges`. The cutover epoch rises only after the last source row is settled, and every old writer is deleted before requests resume. Cutover contribution: interrupted paid bookings resume without charging, booking, or recording Money twice; a provider refund that outruns a local failure repairs itself; and no admin action can orphan payment work. #### Work package M9: Durable messages and outgoing webhooks Src target: 300–600. Store prepared message and webhook bodies plus buyer facts before delivery; resolve the owner recipient from the current business address at send time; attempt and schedule each delivery independently, keeping each run within explicit provider, database, and total subrequest budgets; mark permanent failures without blocking later work. Every delivery carries a stable identity: webhooks send it in a header consumers deduplicate, and email uses the provider's idempotency key where the provider offers one. Where it does not, the attempt record persists the provider's acceptance evidence before the delivery is marked complete; if that evidence write itself is lost, the retry may duplicate an email — an accepted, documented outcome for messages only, never for webhooks or money. The dormant `payment_completion_deliveries.data` schema currently accepts DB-key `enc:1` ciphertext even though the payload can contain the buyer's name, email, phone, and address. Before the first M9 writer exists, change that column to require owner-key `hyb:1` ciphertext or drop the dormant table and replace it with the one canonical owner-key-protected delivery store. It may never become a live DB-key-readable queue or a compatibility mirror beside a new queue. Cutover contribution: the M8 completion runner's messages and webhooks use the current owner address, recover on schedule after an interruption, and one permanently failing destination no longer blocks the rest of the queue. #### Work package M10: Durable site assignment and renewal Src target: 300–600. Persist site assignment and renewal effects before remote work; serialize concurrent paid renewals; keep remote calls outside transactions; repoint queued site work during attendee merges; schedule unfinished work within explicit provider, database, and total subrequest budgets. Each persisted renewal effect carries a stable provider idempotency key, or the runner reads the remote state before re-attempting — an uncertain success is never blindly replayed, so a site cannot be extended twice. Assignment claims a site atomically, and that claim already exists: `claimBuiltSiteStatement` (`src/shared/db/built-sites/claims.ts`) requires the site to still be assignable inside the mutating statement itself, and the loser moves to the next site or builds one. #2418 shipped that claim and its racing-claim regression test, which closes fault F45. A site build gets the same no-blind-replay rule as renewals: the persisted effect carries a stable creation identity, and a retry after a lost response reads the provider and adopts the site that identity already created instead of provisioning a second one or failing on the duplicate name. Cutover contribution: paid site delivery and renewal recover safely after an interruption, a concurrent payment, or an attendee merge. #### Work package M11: Verify and copy retained payment records Src target: verifier 400–600; copy 800–1,200 (cutover exception). The verifier is read-only and parallelizable — #2056 already started it, and that pull request is still open. The copy is the activation gate for the complete M6–M11 runtime, not a later bridge. - The verifier reads the retained payment stores — `processed_payments`, `checkout_stages`, `sumup_checkouts`, their explicit merge links, and M4's canonical authority — into one lossless model without writing cases. It does not scan attendee PII or old free-text notes for payment references. Group one provider payment before pagination, convert old timestamps, and report contradictions through operator diagnostics and backup verification. An owner-encrypted reference already present in payment storage is opened only through the request-scoped private-key path; key material and raw references never enter progress records, logs, audits, or backups. If required retained evidence cannot be opened, block the copy and preserve the source row. Back up databases before migrating. - The cutover fence stops every old payment writer and drains in-flight commits before copying. It pauses payment creation and callbacks, admin refund commands, completion, attendee merge/delete, pruning, and stale-reservation deletion while relationships are established. It need not pause PII edits to recover hidden payment ids because hidden ids are not migration input. Existing provider work already lives in M4's `payment_charges` authority and is linked, never copied into a second state machine. Lease ownership, renewal, timeout, and safe abort details are fixed in the cutover behavior contract before the first cursor page. - Copy every retained payment row by stable cursor in bounded, verified pages. Never split one provider payment across pages, mistake an empty joined page for the end, let deleted booking rows block, or resurrect ticket-use state. A tagged, owner-encrypted reference already present in retained payment storage may become a canonical charge. A blank-index, untagged, PII-only, or otherwise unproved reference does not: preserve its SQL-visible accounting facts as a terminal unsupported record and direct any cash action to the provider dashboard. Current provider, last-active provider, credential order, identifier spelling, owner guess, a note, or a dashboard URL can never qualify it. Do not read attendee PII, decrypt a population, or make an interactive request perform the copy. - A reference already owned by M4's authority is linked to the aggregate payment, never copied as a new charge. Before a source is settled, fold its local completion facts — attendee link, ticket result, and recorded failure — idempotently onto canonical completion records. Preserve unknown or contradictory facts without inventing them. A terminal unsupported or unmigratable disposition keeps only a bounded accounting record: an allowlist of non-buyer facts already present in retained payment storage, encrypted for the owner-only case page, with no buyer PII, ticket tokens, credentials, raw note text, or newly recovered reference. That lets M13 retire the old table without pretending the current engine can refund it. - A source is settled when it is either copied and verified into a canonical payment or terminally preserved as unsupported/unmigratable. Record progress and release leases within the call budget; interruption resumes from the same cursor. A final verifier proves counts, identities, Money, local outcomes, and provider-work links before the cutover transaction raises the epoch. - The activated application has only canonical panels, exports, statistics, refund targets, callbacks, completion, effects, and maintenance. Every old runtime reader and writer is deleted in this release. Migration-only decoders for retained payment rows cannot be imported by request handlers; the architecture gate rejects a second refund writer, completion projection, read-through, or dual write. Cutover contribution: every retained payment row receives a verified canonical or terminal accounting disposition. Only proved current evidence becomes actionable; unsupported historical refunds stay manual. ### Follow-on history retirement (M12–M13) #### M12: Redact terminal payment secrets (was PR 15) Src target: 400–700. Redact intent, evidence, ticket tokens, completion payloads, historical DB-key-encrypted refund-warning notes containing raw provider references, and the stored delivery records from M9 — prepared message and webhook bodies plus their buyer facts — only after all work that needs them is terminal, including deliveries that permanently failed. The bounded accounting record M11 preserves on an unmigratable-row case is permanently retained: it carries no buyer secrets by construction, and once M13 drops the old tables it is that payment's only copy, kept as documented accounting history. Eligibility is defined for every terminal outcome — completed, fully refunded, failed, cancelled, expired, and free — each either redacts once its work is terminal or documents why its data is retained, with a cleanup test per state; page cleanup so one bad or ineligible record cannot block later eligible rows. Standalone value: deployed sites keep accounting history while shedding buyer secrets and ticket credentials they no longer need. #### M13: Retire old payment storage (was PR 16) Src target: 600–1,000, mostly deletions — the atomic-cutover exception where this exceeds rule 3's cap: the table drop and every reader, codec, and gate it orphans leave together, because splitting them would hold dead readers alive across a merge. Release only after every fleet database reports every source row settled — copied and verified, or terminally preserved as unmigratable — and no request, worker, page, export, or maintenance command reads an old table. - Drop the old tables and delete the migration reader, progress gates, old codecs, stale TODO entries, temporary exemptions, and dead exports together — except an explicit restore-time conversion step, documented in the operator restore guide, so a pre-aggregate backup can complete the same fenced ceremony before the application starts. It is not importable by runtime request code. - The table-drop migration must not be an unconditional `DROP TABLE` in `initDb`: a restore of an old backup would drop `processed_payments`, `checkout_stages`, and `sumup_checkouts` before the retained reader has anything to read. Make the drop conditional on verified copy progress, or convert at restore time before schema migrations run. - Run full coverage, the quality audit, Cucumber specs, exhaustive targeted mutation, and the final branch mutation gate. Update operator and database documentation. Standalone value: one smaller payment implementation, faster cold starts, and no ambiguity about which path is authoritative. ## Fault ledger Every row is a mandatory input to its owning milestone: close it with a mechanism and a regression test, or — if implementation proves the finding wrong — with a short proof in the PR. Never silently drop one. | # | Finding | Owner | | --- | -------------------------------------------------------------------------------------- | ---------------------------------------- | | F1 | Disabling new payments also disabled existing-payment refunds | Closed by #2020 | | F2 | Unknown unsigned SumUp callbacks triggering outbound reads | Closed by #2060 | | F3 | Pending and completed refunds together exceeding captured money | Closed by M4 | | F4 | One failed decision blocking all reconciliation | M5, M6 | | F5 | Permanent provider or delivery errors retrying forever or blocking a queue | M5, M6, M9 | | F6 | Attendee merge or delete removing records with an open case or unfinished work | M4 current rows closed; M6–M8 aggregate | | F7 | Restore-deploy workflow allowing incompatible code onto a migrated database | M6 | | F8 | Cross-payment duplicate provider charges | M6 | | F9 | Account lookup failure retaining a claim | M6 | | F10 | SumUp return IDs interpreted differently by different routes | M6 | | F11 | Square fallback reads scanning too short a list | M6 | | F12 | Delayed work using live currency rather than stored currency | M4 admin closed; M6 aggregate | | F13 | Charges without a stored provider guessed after a provider switch (#2020 gap) | Closed by M4 permanent refusal | | F14 | In-flight pre-cutover checkouts paid after the cutover, stranded without a row | M6 | | F15 | Old rows changing during aggregate migration | M11 fence | | F16 | Old payment-reference readers surviving after migration | M11, M13 | | F17 | Owner refund decisions closing a case without closing Money | M7 | | F18 | Completed provider refunds missing from Money | Closed by M4 | | F19 | Provider success followed by a local failure having no repair path | Closed by M4 | | F20 | Refund-all conflicting forever with unfinished completion | M7, M8 | | F21 | The same indexed provider reference refunded twice through admin bulk refunds | Closed by M4 | | F22 | A completed refund resurfacing as refundable on an uncopied row | Closed by M4 canonical authority | | F23 | Attendee-only payment references skipped, or refunded without verified facts | Closed by M4 permanent refusal | | F24 | Delayed completion rebuilding facts from edited live data | M8 | | F25 | Listing attachments deleted before a payment fence succeeds | M8 | | F26 | `deleteAllStaleReservations` deleting uncopied legacy rows under the fence or mid-copy | M8, M11 | | F27 | Concurrent renewals racing | M10 | | F28 | Queued site work retaining a deleted attendee ID after merge | M10 | | F29 | SumUp identities split across migration pages | M11 | | F30 | A merged migration page mistaken for end-of-input | M11 | | F31 | Deleted booking rows blocking migration forever | M11 | | F32 | Ticket-use state resurrected during migration | M11 | | F33 | Migrated charges omitted from refund targets | M11 | | F34 | Late refund-completion writes landing after a row was copied and verified | Closed by M4 canonical authority | | F35 | Migration silently skipping retained evidence whose owner key or source is unavailable | M11 | | F36 | Terminal buyer details, completion data, or ticket tokens never redacting | M12 | | F37 | An unconditional table drop destroying a restored old backup before it migrates | M13 | | F38 | Attendee merge or delete destroying attendee-held payment facts before the copy | M11 fence | | F39 | A migrated source payment appearing twice after activation | M6, M11 | | F40 | An attendee PII edit changing legacy payment references mid-copy | Closed: PII is not migration input | | F41 | Redacting the preserved evidence that is an unmigratable payment's only record | M11, M12 | | F42 | A historical identity deduplication dropping its local booking facts | M11 | | F43 | An already-canonical payment reference migrated again as new input | M4, M11 | | F44 | A legacy deletion stripping folded local facts from a sale the aggregate represents | M6 | | F45 | Two concurrent paid completions claiming the same built site | Closed by #2418 | | F46 | A site build replayed after a lost response, provisioning a second site | M10 | | F47 | A parallel refund adapter completing after its retirement pass | Closed by M4; no adapter exists | | F48 | A multi-listing payment credited in full to each listing, or its shared order lost | M6, M7, M8 | | F49 | Source booking facts dropped when its refund authority is already canonical | M11 | | F50 | Unmigratable evidence keeping buyer PII or ticket tokens forever | M11, M12 | | F51 | Two classifiers disagreeing about the same refund evidence | Refund side closed by M4; checkout M6/M8 | | F52 | Checkout fees or price modifiers misallocated into a listing's income | M6, M8 | | F53 | A selected refund command overrunning budget after sending only an initial subset | Closed by M4 | | F54 | One sold-out line half-booking a multi-listing order after payment | M8 | | F55 | The completion runner re-completing a sale already finished before migration | M8, M11 | | F56 | A deposit checkout losing the full modifier fact to the charged fraction | M6, M8 | | F57 | Migration racing an in-flight old commit and re-running its completion | M8, M11 | | F58 | Adoption turning an owner-review payment into due work, bypassing the required choice | M8 | | F59 | A queued refund page stranded by an attendee merge or delete in the M7 window | M7 | | F60 | A refund-all crash after its first page losing the unrecorded remainder | M7 | | F61 | An attendee merge rewriting Money while refund pages are still queued | M7 | | F62 | A reservation refund confusing money charged now with the full obligation | M7 | | F63 | A deposit-plus-balance refund reversing the booking obligation twice or not at all | M7 | | F64 | Adoption stranding payments whose folded result records a completion failure | M8 | | F65 | A cursor advancing past a transiently failed refund, finishing the job around it | M7 | | F66 | A booking completing while its payment's irreversible refund is in flight | M8 | | F67 | A retried refund minting a fresh provider idempotency key and refunding twice | Closed by M4 | | F68 | Transient and permanent refund failures collapsing into one boolean | Closed by M4 | | F69 | An obligation cancellation without a stable identity re-running or never retrying | M7 | | F70 | Two runners both reading "nothing done yet" and acting on one payment | M8 | | F71 | A consumer re-deriving the allocation and disagreeing with the stored record | M6 | | F72 | A booking split across two payment leases, refunding one while completing the other | M8 | | F73 | Repointing replacing the merge fence and replaying a pre-merge allocation | M7, M8 | | F74 | A queued refund acting on stale evidence after the payment's outcome moved on | M7 | | F75 | Cancelling a booking obligation that the failed completion never posted | M8 | | F76 | A discount folded into line prices losing its signed modifier fact | M6 | | F77 | Deposit and balance allocations minting separate identities for one obligation | M6 | | F78 | An indexed refund omitting a PII-only or blank-index sibling charge | Closed by M4 provenance refusal | | F79 | A rejected SumUp request hiding a refund completed beside it | Closed by M4 | | F80 | An automatic quantity-zero placeholder hiding its only payment recovery control | Closed by M4 | | F81 | A rejected send plus an unreadable reread releasing its claim as conclusive | Closed by M4 | | F82 | An anchor-only returned charge hiding a manual-money booking obligation | M7 stable obligations | ## Done means - Every ordinary milestone is merged in dependency order and stands alone. The M6–M11 work packages activate only as their one atomic cutover PR; none is a separately merged compatibility layer. - One production payment path remains; all three providers share one canonical create/read/refund contract; every provider action and local Money action is independently durable and resumable; genuine ambiguity requires an explicit owner choice. - Every provider boundary validates documented data before it reaches booking or money code. Expected visitor errors are values; unavailable or malformed external evidence fails closed; unexpected application and database errors propagate with their identity. No booking/refund path converts an unexpected throw into a successful acknowledgement, empty reading, or default result. - No request discovers payment work by loading or decrypting an attendee population. Whole-population questions use indexed, PII-free summaries; decryption is limited to one bounded page; work larger than a request has a durable cursor and visible continuation. Every physical provider and database retry is priced before the selected irreversible work begins. - New raw provider references and buyer PII require the owner's private key; database-keyed indexes and mirrors reveal only blind equality and state words. With a v2 owner wrap, a database plus `DB_ENCRYPTION_KEY` cannot recover that private key; a dormant v1 wrap remains explicitly weaker until login upgrades it. Historical plaintext or DB-key-encrypted evidence is migrated under the stronger boundary, preserved only in a finite owner case or frozen source awaiting redaction, or redacted. It is never readable by the activated runtime and never silently copied under `DB_ENCRYPTION_KEY`. - Every blocking state is declared exhaustively with its writers, consumers, delete/merge/retention behavior, reachable operator action, exact retirement evidence, and a visitor journey that proves it ends. Unresolved money can end only through provider/ledger proof or a required revision-fenced owner choice, never a generic clear. - Old backups migrate forward into the current version; the restore-deploy guard refuses pre-aggregate code on a forward-migrated database; old code is never redeployed and mixed application versions are never supported. - Payment secrets and buyer details redact once all required work is terminal. - `devenv shell deno task precommit` passes; coverage is 100% and deterministic; changed-source mutation score is 100%; Cucumber payment stories pass; every fault-ledger row and open question is closed.