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