5.9 KiB
Refinement Phase 6 — Money-path correctness completion
Mission: close the one genuine money-correctness hole and the referential-integrity gaps left around the (verified-clean) ledger. The load-bearing invariants — balanced ledger groups, the four money DB CHECKs, webhook idempotency, forward-only status machines — are all verified in code. This phase fixes the holes around them.
Track: backend (money path) · Depends on: nothing hard (do before any real BNPL/manual refund — i.e. before Phase 8 money rails) · Unlocks: ledger⇄bank reconciliation Before you start, read ../../phases/_shared/agent-operating-rules.md.
1. Context
This phase is the server audit's post-phase-2. Full file/line evidence and fixes: ../server/post-phase-backend-plan.md § post-phase-2. Read it — it is the spec.
The headline is one real defect: the BNPL/manual refund settlement path is unreachable. A card refund posts
its refund_payable ↔ escrow_held clearing immediately, but a BNPL-revert / manual-bank refund is left in
processing with the clearing "deferred to reconciliation" — and no reconciliation path exists anywhere:
Refund.MarkSucceededAsync has zero callers, no endpoint/webhook/job performs processing → succeeded.
So every BNPL/manual refund permanently overstates escrow_held and strands refund_payable; the ledger will
never reconcile with the bank. This must be fixed before real BNPL/manual refunds exist.
2. Required reading
- ../server/post-phase-backend-plan.md § post-phase-2 (items 2.1–2.6) — the spec, with every file/line.
server/CLAUDE.md"Refunds, clawbacks & invoices" + "Payments core" — the invariants you must preserve.- The Phase 3 refund REQs (019/020/021/035) — coordinate: the customer cancel command + admin preview/approve you may build there interact with the settlement fix here.
3. Scope — deliver plan items 2.1–2.6
- 6.1 (top fix) — wire the unreachable refund settlement. Add a
ConfirmRefundSettlementCommand(adminPOST admin_refunds/{id}/confirm_settlement+ a BNPL-callback branch when the provider confirms customer cash-back) that transitionsprocessing → succeeded, stampssettled_at, and postsLedgerPosting.RefundPayableClearingin the same commit; plus amark_failedcounterpart. Tests for both channels. - 6.2 — add the promised forward-dep FKs (one additive migration + an index):
refunds.ticket_id,nurse_clawbacks.original_payout_id/recovered_in_payout_id,invoices.partner_center_id(+ index). The target tables all shipped and the values are wired; only the constraints are missing. Fix the now-false config comments too. - 6.3 — extend
IAuditableto the admin-decided money & trust entities (Refund,NurseClawback,NursePayout,NursePayoutBatch,NurseVerification) so approve/reject/process and theis_verifiedflip leave an audit-diff row. Confirm[AuditRedacted]coversiban_snapshotfirst. - 6.4 — close the refund channel-execute-before-commit crash window (persist a
pendingrefund row before the external channel call, then execute and update — the two-phase intent/confirm shape the webhook handler already uses). - 6.5 — test the untested admin money paths (
WriteOffClawbackCommand/ bad-debt group; a true racing webhook-insert; messagingis_internalhandler-level tests). - 6.6 — retire the orphaned
refund_ticket_requiredconfig key (b15 superseded it) or repurpose it; fix the false description.
4. Mocks & seams
None new. This is domain/ledger code over existing seams; the BNPL/PSP mocks stay until Phase 8.
5. Critical rules
- The ledger stays append-only and balanced. Every new posting is a balanced group under one
transaction_group_id; the settlement clearing must reconcile (Σdebit = Σcredit). - Idempotency preserved. The settlement confirm and the crash-window fix must be safe to replay (the idempotency key + status machine are the backstop).
- Do 6.1 before real BNPL/manual refunds (Phase 8) — otherwise real money strands ledger state.
- Don't regress the four DB CHECKs (
CK_Bookings_AmountSplit,CK_NursePayouts_NetSplit,CK_Refunds_LegSplit,CK_BnplTransactions_SettleSplit).
6. Definition of Done
- A BNPL/manual refund can reach
succeeded(via admin confirm + BNPL callback) and posts therefund_payable ↔ escrow_heldclearing — proven by a test that the ledger reconciles. - The three forward-dep FK sets + the invoice index exist (additive migration); config comments corrected.
- The five listed entities are
IAuditableand produce audit-diff rows on admin decisions. - The refund crash-window is closed; the previously-untested admin money paths have tests.
dotnet buildzero new warnings;dotnet testgreen (incl. the new tests).
7. How to test
- Create a BNPL refund → it lands
processing→ adminconfirm_settlement→ statussucceeded,settled_atset, andGetNursePayableBalance/escrow reconcile (no strandedrefund_payable). WriteOffClawbackposts a balancedbad_debtgroup; a replayed settlement confirm is an idempotent no-op.- Inspect
audit_logsafter an admin refund approve / a verificationis_verifiedflip → a diff row exists.
8. Hand off & document
- Update
server/CLAUDE.mdrefunds/payments sections + the contract docs for the new admin refund route. Flip the mocks-registry / audit notes for the settlement seam. Save a memory note that the unreachable BNPL/manual refund clearing is now wired (the audit's top code fix).