Files
baya-monorepo/archive/docs/integration/domains/payouts.md
T
2026-08-02 20:01:31 +03:30

6.2 KiB

payouts — weekly nurse settlement

Client seam client/src/services/payouts/ · USE_PAYOUTS_MOCK = true (mock is primary) · 13 server ops Last verified: 2026-07-30 against commit d3ec723 and swagger.v1.json (2026-07-29).

Money going out, weekly, in batches. The nurse's view is read-only; the admin's view is the one place in the platform where an irreversible transfer is triggered by hand.

Endpoints

Method Path Auth Verdict
GET /api/v1/nurse_payouts/earnings [Authorize] wired · paginated — per-booking earnings
GET /api/v1/nurse_payouts/earnings_balance [Authorize] wired — the four-bucket balance
GET /api/v1/nurse_payouts/history [Authorize] wired · paginated
GET /api/v1/nurse_payouts/{id} [Authorize] wired — payout detail
GET /api/v1/nurses/{nurseId}/payable_balance [Authorize] unwired — the nurse reads earnings_balance instead
GET /api/v1/admin_payouts/eligible admin · sensitive wired · paginated
GET /api/v1/admin_payouts/batches admin · sensitive wired · paginated
POST /api/v1/admin_payouts/batches admin · sensitive unwired — generation is automatic (below)
GET /api/v1/admin_payouts/batches/{id} admin · sensitive wired · paginated (page/pageSize)
POST /api/v1/admin_payouts/batches/{id}/process admin · sensitive unwired — the irreversible step; no console button yet
POST /api/v1/admin_payouts/{payoutId}/retry admin · sensitive wired
POST /api/v1/admin_payouts/{payoutId}/mark_failed admin · sensitive unwired
POST /api/v1/webhooks/payouts/{provider} anonymous · webhook server-only — the transferor's reconciliation callback

This domain absorbed the entire wire-level drift between the 2026-07-13 contract freeze and the 2026-07-29 snapshot — one added endpoint and one changed schema, both here:

  • Added (C-6): POST /api/v1/webhooks/payouts/{provider} — the transferor's reconciliation callback.
  • Changed (C-7): GeneratePayoutBatchCommand gained systemInitiated: boolean, alongside the existing periodStart/periodEnd dates. That is the refinement-phase-7 scheduler flag: it distinguishes a batch the recurring job generated from one an admin generated, which is what keeps the "generation is automatic, processing is not" rule auditable.

Phantom — 1

Client call REQ Note
POST /api/v1/admin_payouts/{id}/transfer_reference REQ-036 Deferred. The reference is readable on PayoutDto.transferReference; what is missing is a route to record one manually

Generation is automatic; processing is not

This is a hard platform rule, not a convention:

  • A scheduled job may generate a draft batch. IRecurringJob + RecurringJobSchedulerHostedService do this weekly, in-process (refinement-phase-7).
  • The irreversible process step is always an explicit admin action. No job, no schedule, no retry loop may trigger a real transfer.

Which is why POST batches is unwired (the job does it) and POST batches/{id}/process is unwired (the console has no button yet — that is the gap, and it is deliberate that nothing automated fills it).

The money rules

  • UNIQUE on booking id: one payout per booking, ever. The DB constraint is the authority; the handler does not rely on an if.
  • The net balance is SIGNED and must never be clamped to zero. A nurse with a clawback larger than their eligible earnings has a negative netAmountIrr. Rendering it as 0 tells them they have nothing owing when in fact they owe. The client displays the signed value.
  • Clawback netting is whole-clawback greedy, never partial: a clawback either fits in this batch or waits for the next. See refunds.md.
  • Payout dates are resolved against the bank-holiday calendar server-side. The client never computes one — see admin.md. nurse_payout_interval_days and payout_satna_threshold_irr are config; the threshold selects PAYA vs SATNA.
  • A verified primary IBAN gates payment, not accrual. EligibleNurseEarningsDto.hasVerifiedPrimaryIban is what the console checks; a nurse without one accrues a balance and is not paid. See nurse.md.
  • The IBAN is always masked on every read model, nurse-side and admin-side alike.

A resolved drift

payouts/apis/clientApi.ts states that NursePayoutHistoryDto "carries no failureReason — that field lives on the admin-only PayoutDto … until REQ-025 adds it". The live wire has it: failureReason is on NursePayoutHistoryDto and NursePayoutDetailDto and PayoutDto. The comment predates the delivery; a failed payout can show its reason to the nurse today.

The client also sends Idempotency-Key on process/retry. The server does not read it there — only PaymentsController and CheckoutBnplController do. Harmless (those writes are idempotent by constraint), but the header is decorative on this domain. See ../api-contract.md.

Enums

Vocabulary Values
PayoutStatus pending submitted paid failed
PayoutBatchStatus draft processing partially_failed completed failed
EarningsState pending eligible paid clawback_applied

PayoutStatus and PayoutBatchStatus are verified identical to Entities/Payouts/*.cs and are forward-only through PayoutStatusTransitions. partially_failed is a real batch outcome — some destinations settled, some did not — and drives the single-payout retry; Seams:BankTransfer:FailIban exists to make it testable.

Open REQs

REQ Status Effect
REQ-025 delivered The four-bucket balance, per-booking earnings list and payout detail are all served (including failureReason, above)
REQ-036 deferred No single-payout preview, no holidayShifted flag, no record-transfer-reference route → 1 phantom
REQ-053 open No payout forecast (next batch date + expected eligible amount). The nurse dashboard's forecast line renders nothing on the real path