backend phase 11
This commit is contained in:
@@ -12,6 +12,28 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
||||
- **Notes for frontend:** <anything load-bearing>
|
||||
-->
|
||||
|
||||
## backend-phase-11 — Refunds, invoices & nurse clawbacks — 2026-07-09
|
||||
- **Shipped:** the reversal leg via one migration in the **`payments`** schema — **3 tables** `Refunds`
|
||||
(fee-leg decomposition + `refund_channel` + `amount = fee_leg + payout_leg` CHECK + **nullable `ticket_id`, no FK**
|
||||
until b15) / `NurseClawbacks` (nullable `original_payout_id`/`recovered_in_payout_id` until b13) / `Invoices`
|
||||
(**UNIQUE `invoice_number`** sequential + UNIQUE `booking_id`), plus the `InvoiceNumberSequences` counter row.
|
||||
Features: `CreateRefund` (whole money-path under `lock(booking:{id}:refund)`; decompose → Σ≤captured → channel →
|
||||
balanced ledger reversal via b10's helper; pre-payout `nurse_payable` vs post-payout `nurse_clawback_receivable`
|
||||
fork + `pending` clawback + support alert), `WriteOffClawback`, `ListRefunds`, `GetRefundStatus`, `IssueInvoice`
|
||||
(VAT on commission only from config, sequential number, mocked مودیان), `GetInvoice`. Controllers: admin
|
||||
`AdminRefunds`/`AdminClawbacks`/`AdminInvoices` (rate-limited) + customer `Refunds`/`Invoices`.
|
||||
- **Contracts:** dev/contracts/domains/refunds-invoices.md + openapi snapshot refreshed — **yes**.
|
||||
- **Mocked:** `IMoadianClient` (new), `IBnplProvider` (thin pre-b12 stub), `INursePayoutStatus` (interim derivation;
|
||||
b13 owns real); reused `IPaymentProvider`/`IWebhookVerifier`/`IDistributedLock`/`INotificationDispatcher`. See
|
||||
reports/mocks-registry.md.
|
||||
- **Gate:** build clean (0 new warnings) / tests green (300 pass: 205 Foundation + 91 Api + 4 Identity). Migration
|
||||
applied to the dev DB; swagger snapshot republished.
|
||||
- **Handoff:** backend/handoff/after-backend-phase-11.md
|
||||
- **Notes for frontend:** refunds are **admin-only** (no self-service); the only customer surfaces are
|
||||
`GET refunds/{id}/status` (with the BNPL `expected_customer_refund_eta` date + masked reference) and
|
||||
`GET invoices/{booking_id}`. Money is IRR digit-strings. Card refund = immediate `succeeded`; BNPL = `processing`
|
||||
+ ETA.
|
||||
|
||||
## backend-phase-10 — Payments core: ledger, transactions, webhooks & card capture — 2026-07-06
|
||||
- **Shipped:** the money core via one migration in a new **`payments`** schema — **4 tables** `PaymentGateways`
|
||||
(encrypted `config_json`) / `PaymentTransactions` (the **two filtered uniques** — `gateway_reference_code` WHERE
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Handoff — after backend-phase-11 (Refunds, clawbacks & invoices)
|
||||
|
||||
**The reversal leg of the payments arc is live.** Money can now flow *backwards*: an admin reverses a captured
|
||||
booking payment across both fee legs, the balanced reversal posts to the append-only ledger, and the flow forks
|
||||
on whether the nurse was already paid (clean `nurse_payable` reversal vs a first-class `nurse_clawbacks`
|
||||
receivable). The minimal commission **invoice** (VAT on the commission line, sequential number, mocked مودیان)
|
||||
ships too.
|
||||
|
||||
## What the frontend (f10-b11) can now build
|
||||
- **Admin refund console** — create a refund (`POST admin_refunds`: full/partial by percentage or explicit legs),
|
||||
the refund worklist (`GET admin_refunds` — channel, decomposed legs, status, ETA, policy snapshot), and the
|
||||
clawback write-off (`POST admin_clawbacks/{id}/write_off`).
|
||||
- **Admin invoice issuance** — `POST admin_invoices` (sequential number, VAT on commission, مودیان pending).
|
||||
- **Customer cancellation/refund-status view** — `GET refunds/{id}/status` (tenancy-scoped; status, channel,
|
||||
amount, **`expected_customer_refund_eta`** for the BNPL 7–10-business-day window, masked reference). This is the
|
||||
**only** customer-visible refund surface — refunds are admin-initiated, there is no self-service initiation.
|
||||
- **Customer/admin invoice view** — `GET invoices/{booking_id}` (number, gross, commission, VAT, مودیان status,
|
||||
PDF URL when present).
|
||||
|
||||
## Live endpoints / contract
|
||||
- Contract: **`dev/contracts/domains/refunds-invoices.md`** (enums, DTO shapes, IRR digit-strings, masked
|
||||
references, failure codes, side-effects). Machine schema: `dev/contracts/openapi/swagger.v1.json` **refreshed**.
|
||||
- All money is IRR integer, on the wire as a **digit-string**. `expected_customer_refund_eta` is a **date**.
|
||||
- Admin endpoints are behind the admin policy and **rate-limited** (sensitive). Refunds are **never** customer
|
||||
self-service.
|
||||
|
||||
## What is mocked / waiting
|
||||
- **سامانه مودیان** is mocked behind **`IMoadianClient`** (pending/no-ref by default; a config switch forces a
|
||||
registered 22-digit ref). See `reports/mocks-registry.md`.
|
||||
- **BNPL revert** runs against **`IBnplProvider`** — a **thin local stub** now; **b12 owns the real seam** and its
|
||||
adapter. The `bnpl_revert` ledger legs are already identical to the card path.
|
||||
- **"Was the nurse paid?"** is derived from the booking's dispute-window close (`INursePayoutStatus`) until **b13**
|
||||
ships `nurse_payouts`; **clawback recovery/netting is b13** (this phase only opens the `pending` receivable +
|
||||
supports write-off).
|
||||
- **`tickets`** arrives in **b15**: `refunds.ticket_id` is a nullable column (no FK yet); the "ticket required"
|
||||
rule is config-gated off (`refund_ticket_required = false`).
|
||||
- **Deferred crons** (thin/manual today): the مودیان reconciliation job (`pending → registered`) and the
|
||||
BNPL-revert reconciliation job that clears `refund_payable ↔ escrow_held` for a `processing` refund.
|
||||
|
||||
## Notes for the next backend phases
|
||||
- **b12 (BNPL):** owns the real `IBnplProvider`; the refund `bnpl_revert` path already calls it through the seam.
|
||||
- **b13 (payouts):** implement the real `INursePayoutStatus` (`nurse_payout_booking_links`) and net `pending`
|
||||
`nurse_clawbacks` out of a payout batch (set `recovered_in_payout_id` / `original_payout_id`, post `DEBIT
|
||||
nurse_payable / CREDIT nurse_clawback_receivable`).
|
||||
- **b15 (tickets/partner centers):** wire the real `refunds.ticket_id` FK + flip `refund_ticket_required` on;
|
||||
wire `invoices.partner_center_id` + `nurse_clawbacks` payout FKs.
|
||||
Reference in New Issue
Block a user