73 lines
4.5 KiB
Markdown
73 lines
4.5 KiB
Markdown
# 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.
|