Files
baya-monorepo/dev/post-phase/ui/ui-phase-6-checkout-and-money.md
T
2026-07-17 13:22:04 +03:30

281 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 02](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 710-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 15 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.