Files
baya-monorepo/dev/shared-working-context/backend/handoff/after-backend-phase-13.md
T
2026-07-09 04:09:35 +03:30

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 historyGET 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).