8.3 KiB
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 frombookings.status+dispute_window_ends_at+ the payout link + clawbacks).PayoutStatus= the contract'spending | submitted | paid | failed(NBsubmitted, not "processing").PayoutBatchStatus=draft | processing | partially_failed | completed | failed.NurseEarningsSummary(four buckets + a signednetPayableBalanceIrrthat may be negative),NurseEarningsItem(per-booking, all four states),NursePayoutHistoryItem,NursePayoutBookingLink,NursePayoutBatchContext,NursePayoutDetail,EarningsListParams, and thePayoutsApiseam (all reads, no mutations).
keys.ts—payoutKeyswith 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), aMOCK_SCENARIOtoggle (standard|clawback_heavy) for the negative-balance demo, generous per-readstaleTimes (earnings move weekly),PAYOUTS_PAGE_SIZE.apis/clientApi.ts— real HTTP impl:getNursePayoutHistory→ the liveGET api/v1/nurse_payouts/ history;getNurseEarningsBalance/getNurseEarnings/getNursePayoutDetailtarget 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-onlyuseQuery,keepPreviousDataon the paged lists, detail disabled on an invalid id.index.tsre-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 byPriceBreakdown), one of four visually-distinct state chips, and the state affordance: pending → a display-only dispute-window countdown (reusesCountdownTimer); eligible → awaiting-batch; paid →paid_at+transferReference+ a payout-detail link; clawback_applied → theoriginal − clawback = netexplanation. Every row deep-links to the f8 booking detail.PayoutHistoryRow— net transferred + payout-status chip + period + masked IBAN (last-4,dir="ltr") + transfer ref; afailedpayout shows its reason as a read-only banner (no nurse retry).
Three nurse screens (nurse shell)
/nurse/earnings— balance header + collapsible explainer + aTabsstate 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
earningsnav item + aPaidOutlinedicon;NURSE_EARNINGS/NURSE_EARNINGS_PAYOUTSroutes +nursePayoutDetailPath/nurseBookingDetailPathhelpers; the three components in the@/componentsbarrel; apayoutsi18n namespace (82 keys,en.json+fa.jsonin 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 درآمدها.
- 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. - Eligible → paid — booking 5002 shows eligible / "awaiting the weekly batch"; booking 5003 shows
paid with
paid_at(Shamsi) + atransferReference, links to the payout detail, and appears in payout history; the detail lists the exact booking(s) covered. - Clawback nets the total — booking 5004 shows clawback_applied with the
original − clawback = netexplanation (1,700,000 − 1,700,000 = 0). To see the negative net balance ("owed back"), setMOCK_SCENARIO = 'clawback_heavy'inservices/payouts/constants.ts→ the header renders the explicit owed-back state (magnitude only). - Failed payout — payout 9003 (in history + at
/nurse/earnings/payouts/9003) showsfailure_reasoninvalid_shebaas a read-only banner; no retry control exists for the nurse. - 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. - i18n / RTL / caching —
fa↔entranslates + mirrors; switching state tabs / paging shows separate cache entries (React Query Devtools) and no refetch of already-loaded data. - Gate —
npm run check+npm run test:cipass.
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 thePayoutDto/PayoutBookingLinkDto/PayoutBatchDtoshapes 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 +
failureReasonon 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 maskediban_snapshot; it never edits bank accounts. - On-demand / instant withdrawal, per-nurse payout-frequency settings → DEFERRED product-side (MVP is weekly).