# Phase 11 — Nurse pay / payouts

**Blocker:** blockers.md § "Nurse pay," and closes §B.6 (real bank-transfer rail) as a side effect — see the
note at the end.
**Depends on:** nothing to start; benefits from [01-admin-rbac.md](01-admin-rbac.md) to actually exercise the
admin-side actions with the seeded `finance` account.

---

Three distinct sub-issues, all independently real.

## 11a. The mock hides four already-real endpoints

`client/src/services/payouts/constants.ts:13` —
`USE_PAYOUTS_MOCK = true`, with a stale comment claiming 3 of 4 nurse reads are server gaps. All four are
real (`NursePayoutsController.cs:27-48`, fully implemented handlers, and `clientApi.ts:162-271` already
correctly calls them). The mock/comment simply predate the server catching up (server phase merged after the
client wrote the mock).

**Fix:** flip `USE_PAYOUTS_MOCK` to `false`. Two secondary defects surface immediately and should land in the
same change: `clientApi.ts:60` hardcodes `failureReason: null` though the wire carries it; and
`previewPayoutBatch` (`clientApi.ts:205-227`) computes money client-side (sums, fabricates a processing
date) — violates the client's own hard rule 18 ("the client never computes money"). The real fix needs a
server-side preview endpoint (already tracked as REQ-036); until that lands, this is a documented gap, not
something to silently patch client-side.

## 11b. No client entry point for the one irreversible action

The real, correct implementation already
exists: `AdminPayoutsController.cs:49-52`, `POST admin_payouts/batches/{id}/process` →
`ExecutePayoutBatchCommand.Handler.cs` (submits to `IBankTransferProvider`, posts the ledger, nets clawbacks,
idempotent). **`PayoutsApi` has no method for it at all** — grepped the whole client, nothing calls `/process`.
The admin UI's scary-looking "Run" button with a typed confirmation
(`admin/payouts/page.tsx:220-235`, `useRunPayoutBatch.ts`) actually calls **Generate**
(`POST admin_payouts/batches`, draft-only, moves no money) — the UX implies it sends money; it doesn't. A
generated batch sits in `draft` forever with no way to advance it.

**Fix:** add `processPayoutBatch(batchId)` to `PayoutsApi` (both `clientApi.ts`/`mockApi.ts`), a
`useProcessPayoutBatch` hook, and a distinct, clearly-separate "Process" action on
`admin/payouts/[batchId]/page.tsx` (gated on `caps.canPayout`). Relabel the existing "Run"/"payout_run" copy
so it's honestly "Generate a draft batch," not "send money." Bundle in the same change: `POST
admin_payouts/{id}/mark_failed` also exists server-side with no client op — add the console action to record
a manually-reconciled bank rejection (this is also the forgotten-features.md item
"no way to confirm a payout succeeded or failed").

*(This is also where blockers.md §B.6's "mock the bank-transfer payment pages for now" mostly resolves
itself: `IBankTransferProvider`/`MockBankTransferProvider` already exist server-side — the "mock" is already
built and DI-registered. What's actually missing is exactly 11a/11b/11c — the client UI in front of that
existing mock. No new bank-transfer mock needs to be invented; de-mocking + adding the process/mark-failed
actions above is the whole of it.)*

## 11c. "Paid" is set at link-time, not bank-confirmation-time

`PayoutRepository.cs:279`, inside `DeriveEarningsState`:
```csharp
if (payout is not null)
    return NurseEarningsState.Paid;
```
`payout` is non-null as soon as `GeneratePayoutBatchCommand` links the booking to a batch
(`GeneratePayoutBatchCommand.Handler.cs:107-112`) — while `NursePayout.Status` still defaults to `Pending`.
The struct carries `Status` already; it's fetched and never read here.

**Fix:** gate on `payout.Status == PayoutStatus.Paid` instead — the definition the codebase already treats as
authoritative elsewhere (`NursePayoutLinkStatusService.cs:22-33`). For the mocked bank rail this'll be set at
`process` time; for a real async rail it should only flip via the reconciliation webhook
(`WebhooksPayoutsController.cs:33-43`).

**Flag:** once fixed, a linked-but-unpaid booking falls into **no** bucket at all in the 4-state summary model
(`pending`/`eligible`/`paid`/`clawback_applied` — none currently represents "batched, awaiting settlement").
Needs a product decision: add a state, or fold it into `eligible`. Separately flagged (needs its own look,
not necessarily the same root cause): the earnings-balance buckets and the raw ledger net don't reconcile on
a live probe — which one is authoritative isn't documented; don't pick one silently.
