backend phase 13 & frontend phase 6

This commit is contained in:
hamid
2026-07-09 04:09:35 +03:30
parent dc64472631
commit de53f9d8a6
97 changed files with 11969 additions and 77 deletions
@@ -12,6 +12,25 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
- **Notes for frontend:** <anything load-bearing>
-->
## backend-phase-13 — Weekly nurse payouts (mocked PAYA/SATNA) — 2026-07-09
- **Shipped:** new `payouts` schema, 3 tables — `NursePayoutBatches` (holiday-shifted period/processing dates),
`NursePayouts` (net-split CHECK, encrypted `iban_snapshot`, forward-only `PayoutStatus`), `NursePayoutBookingLinks`
(**unconditional `UNIQUE(booking_id)`** = one-payout-per-booking-ever). One migration (`NursePayoutEngine`).
`Features/Payouts/*` (compute-eligible / generate-batch / process / retry / mark-failed + admin batch-detail/list +
nurse history; shared `PayoutSettlement` step). `IPayoutRepository`. Controllers `AdminPayouts` (admin, rate-limited)
+ `NursePayouts` (nurse, tenancy-scoped). New seam **`IBankTransferProvider`** (mock PAYA/SATNA). Swapped
`INursePayoutStatus` to the authoritative link-based `NursePayoutLinkStatusService` (deleted the interim one). Added
2 config keys (`payout_satna_threshold_irr`, `require_bnpl_settlement_for_payout`).
- **Contracts:** `dev/contracts/domains/payouts.md` + openapi snapshot refreshed (yes — 7 payout paths).
- **Mocked:** `IBankTransferProvider` → 🟡; `INursePayoutStatus` → 🟢 (real link lookup). Reuse `IHolidayCalendar`,
`IFieldEncryptor`, `IDistributedLock`, `ICacheService`. See reports/mocks-registry.md.
- **Gate:** build clean (0 new code warnings) / tests green (329: 223 foundation + 102 api + 4 identity; +9 payout
unit + 6 payout api). Migration builds; swagger serves all 7 payout paths.
- **Handoff:** backend/handoff/after-backend-phase-13.md
- **Notes for frontend:** f12-b13 = nurse `nurse_payouts/history` (own payouts, masked IBAN, digit-string money) +
admin payout console (`admin_payouts/eligible|batches|batches/{id}|batches/{id}/process|{payoutId}/retry|mark_failed`).
Query params camelCase (`page`/`pageSize`/`status`/`periodStart`/`periodEnd`). Money is a digit string.
## backend-phase-12 — BNPL: provider-financed installments (mocked) — 2026-07-09
- **Shipped:** `payments.BnplTransactions` (1:1 with `payment_transaction`, `UNIQUE(payment_transaction_id)`,
settle-split CHECK, forward-only `BnplStatus` machine); `Features/Bnpl/*` (eligibility/initiate/verify/settle/
@@ -0,0 +1,51 @@
# Handoff — after backend-phase-13 (Weekly nurse payouts)
**The payout engine is live — a nurse's earnings are now real money.** This is the last money-out leg of the
payments arc (b10 ledger → b11 refunds/clawbacks → b12 BNPL → **b13 payouts**). An admin previews eligible
earnings, opens a weekly batch, and submits it to a **mocked PAYA/SATNA bank rail**; each booking is paid in
**exactly one** payout across all batches (a `nurse_payout_booking_links.booking_id` UNIQUE), pending clawbacks are
netted, the verified primary IBAN is snapshotted, and the outbound `nurse_payable → escrow_held` ledger movement is
posted. Everything is **holiday-aware** (a Nowruz-landing batch shifts off bank-closed days). No `payout_released`
boolean exists — paid-ness is derived from a link row + the ledger.
## What the frontend (f12-b13) can now build
- **Nurse earnings / payout history** — `GET nurse_payouts/history` (authenticated nurse, own payouts only,
paginated): `status` (`pending|submitted|paid|failed`), `netAmountIrr`, `grossEarningsIrr`, `clawbackAppliedIrr`,
the **masked** IBAN, `transferReference`, `paidAt`, and the batch window. Money is a **digit string**. Show the
clawback line when `clawbackAppliedIrr > "0"` ("earnings held to recover a prior overpayment").
- **Admin payout console:**
- `GET admin_payouts/eligible?periodStart=&periodEnd=` — dry-run preview per nurse; flags any nurse with
`hasVerifiedPrimaryIban=false` (they won't be paid until they register a verified primary account).
- `POST admin_payouts/batches {periodStart, periodEnd}` — open a `draft` batch → returns the batch + payouts +
the `skipped` nurses (with reasons). No money moves yet.
- `POST admin_payouts/batches/{id}/process` — submit to the rail → `completed` / `partially_failed`.
- `POST admin_payouts/{payoutId}/retry` and `POST admin_payouts/{payoutId}/mark_failed {failureReason}`.
- `GET admin_payouts/batches/{id}` (header + payouts + linked bookings) and `GET admin_payouts/batches?status=`.
## Contracts
- **`dev/contracts/domains/payouts.md`** — all 8 endpoints, the `PayoutBatchStatus`/`PayoutStatus` enums, and the
batch/payout/link/history DTO shapes (IRR digit strings, **masked** IBAN). Query params are **camelCase**
(`page`/`pageSize`/`status`/`periodStart`/`periodEnd`).
- **`dev/contracts/openapi/swagger.v1.json`** refreshed — the 7 payout paths are in the snapshot.
## What is mocked (and how it becomes real)
- **`IBankTransferProvider`** (new) — the PAYA/SATNA rail. `MockBankTransferProvider` moves no money: it returns a
deterministic `transfer_reference` and settles every instruction `paid`, honouring the PAYA/SATNA method the
handler picked by `payout_satna_threshold_irr`. Config forces failures for testing (`Seams:BankTransfer:ForceFailure`
whole-batch, `Seams:BankTransfer:FailIban` one row). Make it real → a Jibit/Vandar/Sadad payout adapter with a
registered source settlement account + the async reconciliation callback (see reports/mocks-registry.md).
- **`INursePayoutStatus`** — b13 shipped the authoritative `NursePayoutLinkStatusService` (paid iff a link ties the
booking to a `paid` payout); the interim dispute-window derivation was deleted. **The b11 refund fork now forks on
the true paid-state** — no refund-side change needed.
## Load-bearing rules (don't regress)
- **One payout per booking, ever** — the `booking_id` UNIQUE is unconditional (not soft-delete-filtered).
- **Eligibility ≠ completed** — needs `dispute_window_ends_at < now`, no active refund, not already linked.
- **Clawback netting recovers *whole* clawbacks up to earnings** (never a negative net, never a partial single row);
the recovery is a real `DEBIT nurse_payable / CREDIT nurse_clawback_receivable` posting + `recovered` status.
- **Process is idempotent** — forward-only `PayoutStatus` + a batch idempotency key + the ledger-exists guard.
## Deferred (flagged, not built)
- The weekly **cron scheduler** — batches are admin-triggered; cadence in `nurse_payout_interval_days` (default 7).
- **On-demand / instant withdrawal**, **per-nurse payout frequency**, **automated clawback recovery beyond netting**.
- The **BNPL `settled_at` guard** — exposed as `require_bnpl_settlement_for_payout` (config, default off).