# Frontend Phase 12 — Nurse earnings & payout history — report

**Date:** 2026-07-10 · **Track:** frontend · **Depends on:** f8 (nurse booking detail) + the b13 payouts
contract · **Status:** complete (mock-primary) · **Gate:** `npm run check` green · `npm run test:ci` green
(242 tests, +3 suites) · `npm run build` green with `NEXT_PUBLIC_API_URL` set.

This is the **last money-path frontend phase** — it closes the loop on the nurse side ("I did the work,
where is my money?") as a strictly **read-only** surface.

## What was built

### `services/payouts` domain (nurse read; the `auth`-service shape)
- **`types.ts`** — string-literal unions + shapes derived from the b13 contract:
  - `EarningsState` = `pending | eligible | paid | clawback_applied` — a **client display model** (there is no
    single wire enum; it is derived server-side from `bookings.status` + `dispute_window_ends_at` + the payout
    link + clawbacks). `PayoutStatus` = the **contract's** `pending | submitted | paid | failed` (NB `submitted`,
    not "processing"). `PayoutBatchStatus` = `draft | processing | partially_failed | completed | failed`.
  - `NurseEarningsSummary` (four buckets + a **signed** `netPayableBalanceIrr` that may be negative),
    `NurseEarningsItem` (per-booking, all four states), `NursePayoutHistoryItem`, `NursePayoutBookingLink`,
    `NursePayoutBatchContext`, `NursePayoutDetail`, `EarningsListParams`, and the `PayoutsApi` seam (**all reads,
    no mutations**).
- **`keys.ts`** — `payoutKeys` with the **state filter + page baked into the key** (`earningsList(state, page)`,
  `history(page)`, `detail(id)`, `earningsSummary()`) so each tab/page caches independently.
- **`constants.ts`** — `USE_PAYOUTS_MOCK = true` (mock-primary), a `MOCK_SCENARIO` toggle
  (`standard` | `clawback_heavy`) for the negative-balance demo, generous per-read `staleTime`s (earnings move
  weekly), `PAYOUTS_PAGE_SIZE`.
- **`apis/clientApi.ts`** — real HTTP impl: `getNursePayoutHistory` → the **live** `GET api/v1/nurse_payouts/
  history`; `getNurseEarningsBalance` / `getNurseEarnings` / `getNursePayoutDetail` target the proposed
  REQ-025 slugs. **`apis/mockApi.ts`** — the primary impl (self-contained, money-correct fixtures — see below).
  **`apis/index.ts`** — one-line seam selector.
- **`hooks/`** — `useNurseEarningsBalance` / `useNurseEarnings(state, page)` / `useNursePayoutHistory(page)` /
  `useNursePayoutDetail(id)` — all read-only `useQuery`, `keepPreviousData` on the paged lists, detail disabled
  on an invalid id. **`index.ts`** re-exports hooks only.

### Three shared, tested composites (`src/components/`)
- **`EarningsBalanceHeader`** — the net payable balance + four buckets (pending/eligible/paid-lifetime/clawback
  off `--bal-{warning,info,success,error}`). A **negative** net renders an explicit **"owed back"** state
  (error-toned card, the magnitude only — never a bare minus).
- **`EarningsRow`** — the three-amount breakdown framed for the nurse (`gross − commission = your payout`,
  reconciled by `PriceBreakdown`), one of four **visually-distinct** state chips, and the state affordance:
  pending → a **display-only** dispute-window countdown (reuses `CountdownTimer`); eligible → awaiting-batch;
  paid → `paid_at` + `transferReference` + a payout-detail link; clawback_applied → the `original − clawback =
  net` explanation. Every row deep-links to the f8 booking detail.
- **`PayoutHistoryRow`** — net transferred + payout-status chip + period + masked IBAN (last-4, `dir="ltr"`) +
  transfer ref; a `failed` payout shows its reason as a **read-only** banner (no nurse retry).

### Three nurse screens (nurse shell)
- **`/nurse/earnings`** — balance header + collapsible explainer + a `Tabs` state segment (All + the four states)
  → the earnings list (skeleton / empty / error-with-retry) + a prev/next pager.
- **`/nurse/earnings/payouts`** — the payout history list (same state treatments) → payout detail.
- **`/nurse/earnings/payouts/[id]`** — the reconciliation detail: batch window (Shamsi) + status chips, the money
  decomposition (`gross_earnings − clawback_applied = net_amount`, plus the amount transferred), masked IBAN +
  transfer ref, a failure banner, and the list of covered bookings (each deep-linking to `/nurse/visits/[id]`).

### Wiring
- `earnings` nav item + a `PaidOutlined` icon; `NURSE_EARNINGS` / `NURSE_EARNINGS_PAYOUTS` routes +
  `nursePayoutDetailPath` / `nurseBookingDetailPath` helpers; the three components in the `@/components` barrel;
  a `payouts` i18n namespace (82 keys, **`en.json` + `fa.json` in sync**) + `nav.earnings`.

## What is now testable and exactly how (phase §7)
Prereq: `npm run dev` (mock is primary, `USE_PAYOUTS_MOCK=true`), sign in as a nurse, open **درآمدها**.
1. **Pending** — booking 5001 shows under **pending / "in escrow · dispute window open"** with a live countdown
   off `disputeWindowEndsAt`; it counts into the **pending** bucket, never as paid.
2. **Eligible → paid** — booking 5002 shows **eligible / "awaiting the weekly batch"**; booking 5003 shows
   **paid** with `paid_at` (Shamsi) + a `transferReference`, links to the payout detail, and appears in payout
   history; the detail lists the exact booking(s) covered.
3. **Clawback nets the total** — booking 5004 shows **clawback_applied** with the `original − clawback = net`
   explanation (1,700,000 − 1,700,000 = 0). To see the **negative net balance** ("owed back"), set
   `MOCK_SCENARIO = 'clawback_heavy'` in `services/payouts/constants.ts` → the header renders the explicit
   owed-back state (magnitude only).
4. **Failed payout** — payout 9003 (in history + at `/nurse/earnings/payouts/9003`) shows `failure_reason`
   `invalid_sheba` as a read-only banner; **no retry control** exists for the nurse.
5. **Money correctness** — every row satisfies `gross − commission = your payout`; Toman = IRR ÷ 10; no BNPL
   provider commission appears; the amount is identical for a card- vs BNPL-funded booking of the same gross.
6. **i18n / RTL / caching** — `fa`↔`en` translates + mirrors; switching state tabs / paging shows separate
   cache entries (React Query Devtools) and **no refetch** of already-loaded data.
7. **Gate** — `npm run check` + `npm run test:ci` pass.

## What is mocked, and how it swaps
`services/payouts` is **mock-primary** (`payoutsMockApi`) because b13 serves the nurse **only**
`GET api/v1/nurse_payouts/history`. The four-bucket **earnings summary**, the per-booking **earnings list +
money-state**, the **nurse-readable payout detail** (batch context + booking links), and `failureReason` on the
history DTO are contract gaps filed as **REQ-025**. `payoutsClientApi` already maps the live history route 1:1
and targets the proposed slugs for the rest — when REQ-025 lands, the swap is a single `USE_PAYOUTS_MOCK=false`
flip with **no hook/component change**. Recorded in `reports/mocks-registry.md` (`PayoutsApi` row).

The mock fixtures are engineered to be **money-correct** (`gross = commission + payout`; `net = gross − clawback`;
Σ booking-link amounts = `grossEarnings`; the signed net balance computed with BigInt, not clamped) and to
exercise **every** UI state (all four earnings states, all four payout statuses incl. a `failed` one, a negative
net balance). Booking ids 5001–5004 align with the f8 bookings-store seeds so "view booking" deep-links land on
real mock detail screens.

## Contracts consumed
- `dev/contracts/domains/payouts.md` (b13) — `GET api/v1/nurse_payouts/history` (live) + the enum codes
  (`PayoutStatus`, `PayoutBatchStatus`) and the `PayoutDto`/`PayoutBookingLinkDto`/`PayoutBatchDto` shapes the
  nurse-read analogues mirror. `api-conventions.md` (`page`/`pageSize`, envelope) + `money-and-types.md` (IRR
  integer digit-strings, Toman display-only, UTC → Shamsi).

## Follow-ups (deferred, not built here)
- **REQ-025** (this phase's filing) — the three nurse-read endpoints + `failureReason` on the history DTO.
- **Admin payout console** (create/process/retry batch, eligible-earnings preview, clawback write-off queue) →
  DEFERRED to f15.
- **Nurse bank-account add/verify (استعلام شبا) UI** → already the nurse onboarding/profile phase (f2/`/nurse/
  bank`). This phase only *displays* the masked `iban_snapshot`; it never edits bank accounts.
- **On-demand / instant withdrawal**, per-nurse payout-frequency settings → DEFERRED product-side (MVP is weekly).
