4.2 KiB
4.2 KiB
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 whenclawbackAppliedIrr > "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 withhasVerifiedPrimaryIban=false(they won't be paid until they register a verified primary account).POST admin_payouts/batches {periodStart, periodEnd}— open adraftbatch → returns the batch + payouts + theskippednurses (with reasons). No money moves yet.POST admin_payouts/batches/{id}/process— submit to the rail →completed/partially_failed.POST admin_payouts/{payoutId}/retryandPOST admin_payouts/{payoutId}/mark_failed {failureReason}.GET admin_payouts/batches/{id}(header + payouts + linked bookings) andGET admin_payouts/batches?status=.
Contracts
dev/contracts/domains/payouts.md— all 8 endpoints, thePayoutBatchStatus/PayoutStatusenums, 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.jsonrefreshed — the 7 payout paths are in the snapshot.
What is mocked (and how it becomes real)
IBankTransferProvider(new) — the PAYA/SATNA rail.MockBankTransferProvidermoves no money: it returns a deterministictransfer_referenceand settles every instructionpaid, honouring the PAYA/SATNA method the handler picked bypayout_satna_threshold_irr. Config forces failures for testing (Seams:BankTransfer:ForceFailurewhole-batch,Seams:BankTransfer:FailIbanone 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 authoritativeNursePayoutLinkStatusService(paid iff a link ties the booking to apaidpayout); 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_idUNIQUE 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_receivableposting +recoveredstatus. - 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_atguard — exposed asrequire_bnpl_settlement_for_payout(config, default off).