add refinement phases

This commit is contained in:
hamid
2026-07-10 20:59:47 +03:30
parent a87b47bedb
commit 850cdf3414
11 changed files with 1428 additions and 0 deletions
@@ -0,0 +1,92 @@
# 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](refinement-phase-8-external-rails.md) money rails) · **Unlocks:** ledger⇄bank
> reconciliation
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../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](../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](../server/post-phase-backend-plan.md)** § post-phase-2 (items
2.12.6) — the spec, with every file/line.
- `server/CLAUDE.md` "Refunds, clawbacks & invoices" + "Payments core" — the invariants you must preserve.
- The [Phase 3](refinement-phase-3-contract-batch.md) 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.12.6
- **6.1 (top fix) — wire the unreachable refund settlement.** Add a `ConfirmRefundSettlementCommand` (admin
`POST admin_refunds/{id}/confirm_settlement` + a BNPL-callback branch when the provider confirms customer
cash-back) that transitions `processing → succeeded`, stamps `settled_at`, and posts
`LedgerPosting.RefundPayableClearing` in the same commit; plus a `mark_failed` counterpart. 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 `IAuditable`** to the admin-decided money & trust entities (`Refund`, `NurseClawback`,
`NursePayout`, `NursePayoutBatch`, `NurseVerification`) so approve/reject/process and the `is_verified` flip
leave an audit-diff row. Confirm `[AuditRedacted]` covers `iban_snapshot` first.
- **6.4 — close the refund channel-execute-before-commit crash window** (persist a `pending` refund 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; messaging `is_internal` handler-level tests).
- **6.6 — retire the orphaned `refund_ticket_required` config 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](refinement-phase-8-external-rails.md).
## 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](refinement-phase-8-external-rails.md)) — 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 the
`refund_payable ↔ escrow_held` clearing — 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 `IAuditable` and produce audit-diff rows on admin decisions.
- [ ] The refund crash-window is closed; the previously-untested admin money paths have tests.
- [ ] `dotnet build` zero new warnings; `dotnet test` green (incl. the new tests).
## 7. How to test
- Create a BNPL refund → it lands `processing` → admin `confirm_settlement` → status `succeeded`, `settled_at`
set, and `GetNursePayableBalance`/escrow reconcile (no stranded `refund_payable`).
- `WriteOffClawback` posts a balanced `bad_debt` group; a replayed settlement confirm is an idempotent no-op.
- Inspect `audit_logs` after an admin refund approve / a verification `is_verified` flip → a diff row exists.
## 8. Hand off & document
- Update `server/CLAUDE.md` refunds/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).