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

23 KiB
Raw Blame History

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 · Phase 5 recommended · Unlocks: the money moment finally earns the trust the ledger deserves Before you start, read ../../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 — 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 — 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 (what escrow actually promises — the explainer must not overclaim), bnpl-landscape.md (provider-financed; the agreement is customer ↔ provider), 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, 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; 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. 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 enrichmentnurseAvatarUrl + 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:

  • 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.