# UI Phase 6 — Checkout & money

> **Mission:** money UI is where trust is won or lost — and today the checkout total is a `subtitle2` row
> buried inside a breakdown card, the payment "confirmation" has no reference code a user could quote in a
> dispute, the top-level «کیف‌پول» tab is permanently empty for every card payer, and BNPL plans hide the
> numbers people decide with. Restructure checkout around "who am I paying for / how much", turn the
> confirmation into a real screenshot-worthy receipt, fill the wallet into the customer money hub, and make
> the BNPL comparison honest — without touching one rial of the money pipeline itself.
>
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
> [Phase 5](ui-phase-5-booking-lifecycle.md) recommended · **Unlocks:** the money moment finally earns the trust the ledger deserves
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the `frontend-designer` skill — both are mandatory.**

## 1. Context — where this sits

Balinyaar's pitch is "pay on-platform because your money sits in escrow until the visit is confirmed." The
checkout → confirmation → wallet → invoice chain is where a family decides whether to believe that. The
plumbing is excellent; the presentation treats the payment moment like any other form. Diagnosed current
state (all verified in code):

1. **The total is visually a footnote.** C6's page h1 is a plain `variant="h6"`
   (`…/bookings/checkout/page.tsx:159`); the total — the most important number on screen — is a `subtitle2`
   row inside `PriceBreakdown` (`components/PriceBreakdown/PriceBreakdown.tsx:62-63`); the pay CTA is the
   last element of a scroll column (`page.tsx:202-225`), not sticky, not paired with the amount.
2. **A live race between two payment paths.** During pay-initiate, `busy` (`page.tsx:148`) disables only the
   card CTA (`:207`); the «پرداخت اقساطی» button (`:214-224`) stays tappable mid-redirect.
3. **The confirmation is not a receipt.** `checkout/confirmation/page.tsx:51-113` renders only total +
   variant + nurse name — no کد پیگیری, date-time, method, or booking number — and the amount panel is
   `{summary ? … : null}` with no loading/error fallback: a failed fetch silently erases the paid amount.
   The wire can't serve a receipt yet (`PaymentOutcomeDto`, `services/payment/types.ts:104-110`).
4. **«کیف‌پول» is an empty promise.** `wallet/page.tsx` renders only `WalletInstallments` — provider-reported
   BNPL plans. For card payers (the default; BNPL is mock-gated) the tab shows «طرح اقساط فعالی ندارید»
   forever, wasting 1 of 5 bottom-nav slots — and it self-constrains to `maxWidth: 560`
   (`WalletInstallments.tsx:24`) while checkout/confirmation/invoice run at the shell's 800.
5. **BNPL hides the deciding numbers.** `BnplPlanCard.tsx:77-98` renders the down payment as a percent +
   `LinearProgress` bar — a static fact styled as a loading indicator — with no Toman figure and no total
   repayment cost; `PlanStep.tsx`'s «مبلغ کل» header silently swaps on plan tap (`plans.find(…) ?? plans[0]`);
   providers are picked from two-letter text glyphs; `EligibilityStep.tsx:163` runs a multi-second credit
   check behind a bare disabled button; `bnpl/return/page.tsx:100-104`'s invalid-link CTA says
   «پرداخت با کارت» but navigates to the bookings list.
6. **Spinner-stacked waits, forked terminal cards.** `checkout/return/page.tsx:139-147` shows title +
   `CircularProgress` + a `pending` chip — three redundant signals at the flow's most anxious moment — and
   its `StateCard` / C6's `MessageCard` are copy-pasted four times across the card & BNPL flows.
7. **Currency-unit ambiguity.** Breakdown rows and installment amounts render bare grouped numbers; only the
   total carries «تومان» (`PriceBreakdown.tsx:53` vs `:63`, `InstallmentScheduleRow.tsx:57`) — the classic
   Toman/Rial second-guess, on wire amounts that are Rials displayed as Toman.

**What already exists (do not rebuild):**

- The money pipeline: `utils/money.ts` (BigInt, `formatIrrToToman`, `parseIrr`), IRR digit-strings on the
  wire, Toman display-only, and `PriceBreakdown`'s dev reconciliation guard (`PriceBreakdown.tsx:35-42`).
- The full C6 → return → confirmation flow and the 4-step BNPL wizard: idempotency-key-per-attempt,
  benign-409 convergence, bounded outcome polling, every state (skeleton/error/empty/expired) covered.
- `EscrowNotice` (product-mandated verbatim copy), `CountdownTimer` (server-frozen, LTR clock),
  `RefundStatusCard` + `RefundEtaBanner`, `StatusChip`, `TrustBadge`, `PaymentStatusBadge`; the invoice's
  print mechanics — visibility-scoped print area, `insetInlineStart` anchoring, dark→light token flip so
  dark-mode users print on paper colors (`invoice/page.tsx:98-113`).
- Phase 0's theme/`AppIcon` work, phase 1's primitives (`<Money>`, PageHeader, state views, StatusTimeline),
  phase 2's shells. Consume them; extend a foundation file minimally (noted in your report) — never fork a copy.

## 2. Required reading (do this first)

- [audit/checkout-money.md](audit/checkout-money.md) — the 18-problem inventory with file/line evidence, the
  8 opportunities this scope is drawn from, and the keep-list §5 repeats.
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
  the design contract (two-layer tokens, `App*` wrappers, icon registry). Invoke it.
- Code (paths abbreviate `client/src/app/[locale]/(private-routes)/(customer)` as `…`):
  `…/bookings/checkout/page.tsx` (+ `return/`, `confirmation/`), `…/bookings/checkout/bnpl/page.tsx` (+ the
  four step files, `return/`, `gateway/`), `…/wallet/` (both files), `…/bookings/[id]/invoice/page.tsx`;
  components `PriceBreakdown`, `BnplPlanCard`, `InstallmentScheduleRow`, `EscrowNotice`, `RefundStatusCard`;
  services `payment` (types + hooks + both `PaymentApi` impls), `bnpl`, `refunds`; `components/config.ts`
  (`CONTENT_MAX_WIDTH`) and `src/layout/config.ts` (bottom-bar dimensions, for the sticky bar).
- Product truth: [escrow-ledger.md](../../../product/payments/escrow-ledger.md) (what escrow actually
  promises — the explainer must not overclaim),
  [bnpl-landscape.md](../../../product/payments/bnpl-landscape.md) (provider-financed; the agreement is
  customer ↔ provider), [iranian-payment-reality.md](../../../product/payments/iranian-payment-reality.md)
  (gateway redirects, receipt culture, Toman/Rial). Plus `client/CLAUDE.md` "Golden rules" + Project Structure.

## 3. Scope — build this

### 3.1 C6 checkout: real hierarchy + sticky pay bar

- **Prominent total.** Lift the total out of the breakdown into an unmissable figure near the top (an
  `h4`-weight `<Money>` with «تومان»); replace the `h6`-as-h1 with the phase-1 PageHeader treatment.
  `PriceBreakdown` keeps its total row — the page-level figure is the same served `totalIrr`, never recomputed.
- **Sticky pay bar.** A page-level bar pinned above the customer bottom nav: total + pay CTA, always
  co-located on mobile. Respect `env(safe-area-inset-bottom)` and the bottom-bar height from
  `src/layout/config.ts` — do **not** edit `layout/` (phase 2 owns it); this is page composition.
- **Identity moment.** Give `EngagementSummary` the nurse's avatar + verified `TrustBadge` so "who am I
  paying for" is answered at the moment of payment. `CheckoutSummaryDto` serves neither field — file the REQ
  (§4) and serve them from the payment mock meanwhile.
- **Kill the race:** `disabled={busy}` on the BNPL branch button too — both CTAs freeze during initiate.
- **Back affordance.** The payable state gets an explicit «بازگشت به درخواست» text link — a user under a
  ticking countdown needs an in-page path back to the request, beyond phase 2's header chrome.
- **Trust line at the CTA:** lock icon + «پرداخت امن از طریق درگاه بانکی» caption under the pay bar so the
  gateway redirect is expected, not alarming. Real gateway/Shaparak logos are (DEFERRED — until licensed
  assets exist; never fake a bank's mark).
- **Un-bake the arrows.** `payment.cta_pay` embeds «←»/«→» in the translated string (fa/en.json:577) — move
  the arrow to an `endIcon` slot (register a direction-aware chevron in `AppIcon/config.ts` if phase 0/1 didn't).

### 3.2 Confirmation as a receipt

- Rebuild `…/checkout/confirmation/page.tsx` as a screenshot-worthy receipt card (Iranian users screenshot
  receipts — design for that): **کد پیگیری** in a copyable `dir="ltr"` row with a copy button, Shamsi payment
  date-time, method («کارت بانکی» / «اقساطی — {provider}»), booking reference, the paid total as a `<Money>`
  hero, and the escrow reassurance via the `EscrowNotice` component — never rewritten copy.
- **No silent vanish:** the summary fetch gets real loading (skeleton) and error (retry) states. Add a
  compact "what happens next" 2-step strip (اطلاع‌رسانی به پرستار ← ویزیت و ثبت ورود) via the phase-1
  StatusTimeline idiom, extending trust past the payment.
- The wire serves none of کد پیگیری/paidAt/method today — file the REQ (§4), render from the mock now, and
  degrade gracefully on the real path (hide the row rather than show a fake code). Web-Share/save-image
  actions are (DEFERRED — the copyable code is the contract).

### 3.3 Wallet as the money hub

- Restructure `…/wallet/` into segmented sections (MUI Tabs or a segmented control — one pattern, both
  locales): **پرداخت‌ها** (payment history: every card/BNPL transaction with amount, Shamsi date,
  `PaymentStatusBadge`, deep-link to the booking — needs the customer payment-transactions list REQ, §4;
  build mock-tolerant behind `services/payment`), **اقساط** (the existing `WalletInstallments` content,
  semantics unchanged — provider-reported, never a Balinyaar ledger), **استردادها** (reuse `RefundStatusCard`
  entries; `services/refunds` has by-booking/status reads but no "all my refunds" list — REQ if confirmed
  missing, §4), **رسیدها** (invoice deep-links derived client-side from succeeded payments — no endpoint).
  Each section keeps four-state discipline with money-hub-specific empty copy («هنوز پرداختی نداشته‌اید» —
  not the installments placeholder).
- **Normalize money-surface width:** drop the wallet's local `maxWidth: 560`; all money surfaces (checkout,
  confirmation, wallet, invoice) read at one width system — the shell's `CONTENT_MAX_WIDTH`. If a narrower
  measure is deliberately wanted, make it a named constant in `components/config.ts` applied to **all four**.

### 3.4 BNPL honesty + polish

- **Plan cards decide with Toman, not percent.** Each `BnplPlanCard` shows: پیش‌پرداخت (امروز) in Toman,
  قسط ماهانه, and **مجموع بازپرداخت** with the fee delta vs interest-free explicit («+۴۵۰٬۰۰۰ تومان کارمزد»
  in terracotta / «بدون کارمزد» neutral). Replace the `LinearProgress` down-payment bar with a plain labelled
  amount row — a static fact must not look like loading. All figures are **served** by the plan DTO; if one
  isn't, REQ it — never compute percent × total client-side.
- **PlanStep's swapping total explained.** The «مبلغ کل» header names the plan it reflects
  («مبلغ کل با طرح {plan}») and shows the fee delta when it changes — no silently morphing number.
- **Provider logos strategy (decide and document):** build a small `BnplProviderLogo` registry component —
  `providerCode` → bundled SVG when an asset exists, falling back to a *designed* neutral chip (full provider
  name + tinted monogram, not the current two-letter glyph). Asset-tolerant: real logos drop in without
  touching call-sites.
- **Eligibility gets feedback.** During the credit check the button swaps to «در حال استعلام اعتبار…» with a
  spinner (mirror C6's `state_initiating` pattern); the prefilled mobile field becomes `readOnly`
  presentation (normal contrast), not `disabled`.
- **Fix the mislabelled CTA** on `bnpl/return`'s invalid-link state: it navigates to the bookings list, so
  label it «رزروهای من» (reuse C6's `bd_my_bookings` invalid-link pattern) — or route to an actual card
  checkout if a `request_id` is recoverable. Label and destination must agree.
- **Guard the dev harness.** `…/checkout/bnpl/gateway/page.tsx` is a test harness reachable in production —
  env-gate it with `notFound()` outside development, mirroring the card-gateway harness refinement phase 4
  deleted. Also remove the dead `CHECKOUT_GATEWAY` constant in `src/constants/routes.ts:24` (verify unreferenced).

### 3.5 Invoice: fiscal grade

- Extend `…/bookings/[id]/invoice/page.tsx` toward a document a family can file: buyer name, service
  description + visit date(s) (composable client-side from the booking detail read — a UI join, not money
  math), booking + transaction references, payment method, and a seller fiscal-identity block (legal name,
  economic code, address). `InvoiceDto` serves none of the fiscal fields — REQ (§4); render what the client
  can compose now, reserve labelled slots for the rest.
- Add an A4 print stylesheet: `@page` margins, a document footer (invoice number + issue date + مودیان
  reference when present), type sized for paper. **Keep** the existing print mechanics untouched.
- Delete the stale comment at `invoice/page.tsx:148-149` (it claims fa `common.brand` reads «بلینیار»; the
  catalog actually has «بالین یار»). The brand-spelling unification itself is (DEFERRED →
  [phase 12](ui-phase-12-copy-motion-and-polish.md), which owns the catalog sweep).

### 3.6 Designed wait states + one terminal-state component

- Replace `checkout/return`'s pending spinner+chip+title triple with a staged 2-node progress:
  «بازگشت از درگاه ✓» → «در انتظار تایید بانک» (active, calm animated indicator) + expected-duration copy
  («معمولاً کمتر از یک دقیقه طول می‌کشد»). Keep the manual «بررسی دوباره» escape hatch and the bounded poll.
- Extract the four copy-pasted terminal cards (`MessageCard` in `checkout/page.tsx:255-286` +
  `bnpl/page.tsx`, `StateCard` in `checkout/return/page.tsx:151+` + `bnpl/return/page.tsx`) into **one**
  shared `src/components/PaymentStateCard/` (co-located test, barrel export) covering
  icon/tone/title/body/actions — the card and BNPL flows can no longer drift.

### 3.7 Money-display sweep

- Adopt the phase-1 `<Money>` primitive on every money row in this area — `PriceBreakdown` rows **and**
  total, confirmation, `MethodStep`, `PlanStep`, `EligibilityStep`, `WalletInstallments`, `BnplPlanCard`,
  `InstallmentScheduleRow` — so **every** amount carries «تومان» and identical typography. Bare grouped
  numbers on a Rial-wire/Toman-display product are the exact ambiguity users second-guess. If `<Money>` lacks
  a needed variant (e.g. strike-through for fee comparison), extend the phase-1 component minimally + its
  test, and note it in your report.
- An expandable «چطور کار می‌کند؟» escrow explainer **around** `EscrowNotice` — a 3-step visual
  (پرداخت ← امانت نزد بالین‌یار ← آزادسازی پس از تایید پایان ویزیت) plus the cancellation/refund implication,
  available from checkout and confirmation. The mandated `EscrowNotice` sentence is **untouchable** — the
  explainer wraps it, never edits it. Ground the steps in
  [escrow-ledger.md](../../../product/payments/escrow-ledger.md); never promise timing the ledger doesn't guarantee.

## 4. Mocks & seams in this phase

**No new mocks or seams.** Everything stays behind the existing `services/payment`, `services/bnpl`, and
`services/refunds` seams (`USE_PAYMENT_MOCK` / `USE_BNPL_MOCK` unchanged); the UI is mock-tolerant — real-path
gaps degrade gracefully, never faked data.

Backend gaps become REQ entries appended to
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
REQ-001…038 are taken and parallel UI phases are also filing — **check the tracker at execution time and take
the next free numbers.** File (verify each gap against the current types first):

1. **Checkout/receipt enrichment** — `nurseAvatarUrl` + `nurseVerified` on `CheckoutSummaryDto` (§3.1);
   tracking/reference code, `paidAt`, and payment method on the payment outcome or a confirm-read for the
   §3.2 receipt (`PaymentOutcomeDto` today: `types.ts:104-110`; extends REQ-016/017).
2. **Customer payment-transactions list** — card+BNPL transactions (amount, status, date, bookingId) for the
   wallet «پرداخت‌ها» section (§3.3); receipts derive from it client-side.
3. **Customer refunds list** — an "all my refunds" read for «استردادها» (§3.3) if `services/refunds` truly
   has only by-booking/status reads (extends REQ-021).
4. **Invoice fiscal fields** — buyer name, payment method + transaction reference, seller fiscal identity
   (legal name, economic code, address) on `InvoiceDto` (§3.5; extends REQ-018).

## 5. Critical rules you must not get wrong

- **`EscrowNotice` copy is product-mandated — never edit it.** Wrap and explain around it; verbatim, one
  shared component.
- **Money values are served IRR digit-strings, formatted via the BigInt util. The UI never computes a
  figure** — no percent × total, no client-side VAT, no float anywhere on the money path. `PriceBreakdown`'s
  dev reconciliation guard stays.
- **Idempotency-key-per-attempt and benign-409 convergence stay exactly as implemented** (`page.tsx:54-57`,
  `:137-143`) — a 409 on initiate routes to the outcome read, never a toast.
- **`CountdownTimer` semantics:** server-frozen deadline, LTR-forced tabular clock — restyle, never recompute.
- **BNPL honesty architecture stays:** D5 installments are provider-reported (never a Balinyaar ledger),
  ownership notes at point-of-choice and contract step, consent checkboxes gate credit check and contract,
  every decline path offers the card fallback. `RefundEtaBanner`'s honest 7–10-day BNPL window is untouched.
- **Terracotta is the single money accent** — pay CTA, BNPL selection, outstanding balance, breakdown total.
  Never as body-text color; teal/neutral carries everything else. **Invoice print mechanics + dark→light
  print flip are do-not-regress** (`invoice/page.tsx:98-113,128-136`).
- Design-contract non-negotiables that bite here: every string in **both** `en.json`/`fa.json`; tokens, never
  hexes; logical/RTL-safe props only (`dir="ltr"` islands for codes and clocks); both color schemes verified;
  MUI v9 API only; co-located tests for shared components; `clientFetch`/cookie rules untouched; **frontend
  lane only** — never edit `server/`.

## 6. Definition of Done

On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):

- [ ] `npm run check` green; `npm run test:ci` green for every touched shared component (`PriceBreakdown`,
      `BnplPlanCard`, `InstallmentScheduleRow`, `PaymentStateCard`, `BnplProviderLogo`); `en.json`/`fa.json` in sync.
- [ ] C6 answers "who + how much" without scrolling at 375px: identity block with avatar + TrustBadge,
      prominent total, safe-area-aware sticky pay bar; **both** CTAs disabled during initiate; back link.
- [ ] Confirmation renders a receipt: copyable LTR کد پیگیری, Shamsi date-time, method, booking reference,
      escrow line — with real loading/error states (mock shows all fields; real path hides unserved rows).
- [ ] Wallet has the four sections with four-state coverage each; a card-paying user sees payment history
      (mock) instead of an installments-only empty tab; money surfaces share one width system.
- [ ] BNPL plan cards show down-payment/monthly/total-repayment in Toman with the fee delta explicit; no
      `LinearProgress` as a static fact; PlanStep's total names its plan; eligibility shows in-progress
      feedback; the `bnpl/return` invalid-link CTA label matches its destination.
- [ ] `bnpl/gateway` returns 404 outside development; dead `CHECKOUT_GATEWAY` constant removed.
- [ ] Every amount in the area carries «تومان» via `<Money>`; `PriceBreakdown` still reconciles (dev guard
      fires on a deliberate mismatch). Invoice prints as an A4 document (footer, paper colors from dark mode).
- [ ] REQs filed with the next free numbers; visual verification on all four axes (`/fa` + `/en` × light +
      dark), mobile **and** desktop, for checkout, confirmation, wallet, BNPL wizard, invoice.

## 7. How to test (what a human can verify after this phase)

1. Login as a seeded customer with an accepted request → `/fa/bookings/checkout?request_id=…` at 375px:
   total figure at top, sticky pay bar above the bottom nav with total + «پرداخت»; nurse avatar + TrustBadge
   on the summary; tap pay → both CTAs disable, label swaps to «در حال شروع…».
2. Complete the mock gateway round-trip → the return page shows the staged 2-node wait
   («بازگشت از درگاه ✓ → در انتظار تایید بانک») with duration copy — no bare spinner+chip stack.
3. Confirmation: کد پیگیری renders LTR with a working copy button; Shamsi date-time, method, booking
   reference, escrow line present; reload with the network blocked → skeleton then error+retry, never a
   silently missing amount.
4. Open «کیف‌پول» → four sections; پرداخت‌ها lists the payment just made (mock) linking to the booking;
   استردادها shows `RefundStatusCard` entries after cancelling a paid booking; رسیدها links to the invoice.
5. Back on checkout, tap «پرداخت اقساطی» → provider rows show designed logos/chips (no two-letter glyphs);
   plan cards show پیش‌پرداخت/قسط ماهانه/مجموع بازپرداخت in Toman with «+… تومان کارمزد» on fee plans, no
   progress bar; selecting a plan names the header total; the eligibility check shows progress while pending.
6. `/fa/bookings/checkout/bnpl/gateway` in a production build → 404; in dev it still works. Open a paid
   booking's invoice → buyer/service/reference rows present (mock); print preview shows an A4 document with
   footer; triggered from dark mode → paper colors.
7. Repeat 1–5 on `/en` (LTR) and in dark mode: no clipped RTL/LTR islands, no unreadable tokens, «تومان»
   (fa) / unit label (en) on every amount.

## 8. Hand off & document (close the phase)

- Update `client/CLAUDE.md` (Project Structure) for new components/route changes (`PaymentStateCard`,
  `BnplProviderLogo`, wallet sections); note any minimal extension made to a phase-0/1 foundation file.
- Write the report at `dev/shared-working-context/reports/ui-phase-6-report.md`: what shipped per §3 item,
  the provider-logo and money-width decisions, four-axes screenshots, and the exact REQ numbers filed.
- Save a memory note per operating-rules §8: checkout/receipt/wallet/BNPL end state, REQ numbers, and gotchas
  (sticky-bar vs bottom-nav layering, print stylesheet interactions) the next phase should know.
