backend phase 13 & frontend phase 6
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user