# UI Phase 6 — Checkout & Money — Report (2026-07-18) ## What was built **Three new shared components** (`src/components/`, each tested): - **`PaymentStateCard/`** — the one terminal/wait-state card (icon/tone/title/body + an actions slot) replacing the four copy-pasted private `MessageCard` (checkout/page.tsx, bnpl/page.tsx) and `StateCard` (checkout/return/page.tsx, bnpl/return/page.tsx) functions. All eight call sites now render through it; the card and BNPL flows can no longer visually drift. - **`BnplProviderLogo/`** — `providerCode` → a bundled SVG when a real asset exists (none licensed yet, registry left empty on purpose) falling back to a *designed* tinted-monogram roundel, replacing the old two-letter text-glyph stand-in (`DG`/`SP`/…) in D1's `MethodStep`. Asset-tolerant: dropping in a real logo later touches only the internal registry map, no call-site changes. - **`EscrowExplainer/`** — wraps the product-mandated `EscrowNotice` (never edited) with an optional «چطور کار می‌کند؟» expander: a 3-step visual (پرداخت ← امانت نزد بالین‌یار ← آزادسازی پس از تایید پایان ویزیت, grounded in `product/payments/escrow-ledger.md`) plus the cancellation/refund implication. Used on checkout and the confirmation receipt. **Minimal foundation extension**: `Money` gained a fourth size, `xl` → MUI `h4`, for the checkout/ confirmation "prominent total" hero figure that phase 1 didn't need. Test added. **C6 checkout** (`bookings/checkout/page.tsx`) — real hierarchy: - Identity moment: `EngagementSummary` now shows the nurse's avatar + a `TrustBadge` (verified/unverified off the new `nurseVerified` field) next to the service/patient/time details. - A prominent `` total sits above the countdown/breakdown — the same served `totalIrr` `PriceBreakdown` reconciles below, never recomputed. - A safe-area-aware sticky pay bar (`StickyActionBar`, reusing phase-4's shell-composed safe-area padding — `layout/` untouched): total + pay CTA co-located, a lock-icon «پرداخت امن از طریق درگاه بانکی» trust line, and the BNPL branch button — **both** CTAs now `disabled={busy}` during initiate (the race the audit flagged: only the card button used to disable). - An explicit «بازگشت به درخواست» text link above the identity card (beyond the shell's header chrome). - Un-baked the arrow: `cta_pay` no longer bakes «←»/«→» into the translated string; the button now uses `endIcon="forward"` (already a registered directional icon — mirrors under RTL automatically). **Confirmation rebuilt as a receipt** (`bookings/checkout/confirmation/page.tsx`): - A copyable, `dir="ltr"` کد پیگیری row with a copy button (clipboard + a "copied" toast) — sourced from `usePaymentOutcome`'s new `trackingCode` (card path) or the settled `BnplOrderStatus.id` (BNPL path, no new field needed). - Shamsi paid-at (`formatShamsiDateTime`), payment method («کارت بانکی» / «اقساطی — {provider}»), and the booking reference — every row conditionally rendered so an unserved real-path field just disappears, never a fabricated value. - `EscrowExplainer` (was a bare `EscrowNotice`) and a "what happens next" 2-step `StatusTimeline` (اطلاع‌رسانی به پرستار ← ویزیت و ثبت ورود). - **Real loading/error states**: the old `{summary ? (...) : null}` (a failed fetch silently erased the paid amount) is now a skeleton → `ErrorState` (with retry) → receipt sequence. **checkout/return staged wait state** — the pending-callback branch's spinner+`PaymentStatusBadge`+title triple is replaced with a 2-node `StatusTimeline` («بازگشت از درگاه ✓» → «در انتظار تایید بانک», reusing the timeline's existing `current`-state animated pulse for the "calm animated indicator" ask) plus an expected-duration caption. The manual «بررسی دوباره» escape hatch and the bounded backoff poll are untouched. The failed/window-expired/invalid-id branches now render through `PaymentStateCard` and no longer double an icon with a redundant status chip (the failed state's `PaymentStatusBadge` was dropped — audit's own complaint about the doubled signal). **Wallet rebuilt as the money hub** (`wallet/`) — `page.tsx` → new `WalletScreen.tsx` (MUI `Tabs`, one shared `CONTENT_MAX_WIDTH`, no local width override — the old `maxWidth: 560` is gone): - **«پرداخت‌ها»** (`WalletPaymentHistory.tsx`) — every card + BNPL payment, newest first, `PaymentStatusBadge` + deep-link to the booking. Card rows come from the new `usePaymentHistory()` (REQ-047); BNPL rows are derived from each settled wallet plan's own down-payment leg. A co-located `useWalletHistoryRows.ts` hook merges the two independent seams once, reused by the receipts tab too. Degrades gracefully: either source failing alone still renders the other's rows. - **«اقساط»** — the unchanged f11 D5 `WalletInstallments` body (own heading/width stripped; it's a section now, not a page). - **«استردادها»** (`WalletRefunds.tsx`) — every refund via the new `useMyRefunds()` (REQ-048), each rendered through the existing `RefundStatusCard` with a link to `bookingRefundStatusPath`. - **«رسیدها»** (`WalletReceipts.tsx`) — invoice deep-links derived client-side from the same merged history rows (succeeded + a known `bookingId`) — no endpoint, no money math, just a filter + `bookingInvoicePath`. - A card-paying customer (the default path) now sees payment history instead of a permanently-empty installments-only tab — the audit's top-severity finding. **BNPL honesty + polish** (`checkout/bnpl/`): - `BnplPlanCard` no longer shows a percent + `LinearProgress` bar for the down payment (a static fact styled as a loading indicator). It now shows three plain Toman rows: پیش‌پرداخت (امروز), قسط ماهانه, and **مجموع بازپرداخت** with the fee delta spelled out («+۴۵۰٬۰۰۰ تومان کارمزد» in `--bal-money-emphasis`) or «بدون سود» for interest-free plans. The delta is `totalIrr − orderAmountIrr` — the exact BigInt difference of two already-served amounts (the same "exact remainder of served amounts" pattern the invoice page uses for its service line), never a computed rate. - `PlanStep`'s «مبلغ کل» header no longer defaults to `plans[0]` before any selection (the silently-morphing number the audit flagged) — it renders only once a plan is selected, **names** the plan («مبلغ کل با طرح {plan}»), and shows the same fee delta. - `EligibilityStep`: the credit-check button now swaps to a spinner + «در حال استعلام اعتبار…» while pending (mirrors C6's `state_initiating` pattern); the prefilled mobile field is `readOnly` (normal contrast, screen-reader-reachable) instead of `disabled`. - `bnpl/return`'s invalid-link state CTA was promising a card payment it couldn't perform (labelled «پرداخت با کارت», navigated to the bookings list) — relabelled «رزروهای من» (`bd_my_bookings`, C6's own invalid-link pattern) so the label matches the destination; no recoverable request id exists at that point to route to an actual card checkout instead. - `checkout/bnpl/gateway/page.tsx` (the dev provider-handoff harness) is now `notFound()`-gated outside `NODE_ENV=development` — it was reachable by direct URL in a production build. The equivalent card-gateway harness was already deleted in refinement-phase-4; this one stays (BNPL is still mock-primary) but is no longer reachable in prod. The dead `ROUTES.CHECKOUT_GATEWAY` constant (pointed at that already-deleted card harness page) is removed, and the one place still referencing it — the payment mock's `initiatePayment` — now returns `redirectUrl: null` (a latent bug: it was building a URL to a page that no longer exists; the checkout page's `!redirectUrl` branch already reads the outcome directly, so behavior is unaffected). **Invoice: fiscal-grade** (`bookings/[id]/invoice/page.tsx`): - Buyer name and a service+visit-date recap are composed **client-side** (a UI join, not money math) from `useCustomerProfile()` and `useBookingDetail(bookingId, 'customer')` — no new fields needed. - Payment method, transaction reference, and a seller fiscal-identity block (legal name / economic code / address) render when `InvoiceDto` serves them (new `paymentMethod`/`transactionReference`/ `sellerFiscalIdentity` fields, REQ-049) — `null` on the real path hides the row rather than fake it. - An A4 print pass: a `@page { size: A4; margin: 16mm }` rule alongside the existing print-visibility rule, and a print-only footer (invoice number + issue date + مودیان reference when present) shown only inside `@media print`. The existing print mechanics (visibility-scoped area, `insetInlineStart` anchoring, dark→light token flip) are untouched. - Deleted the stale comment claiming fa `common.brand` reads «بلینیار» — it actually reads «بالین یار» (plain space); the brand-spelling unification itself stays phase 12's, per this phase's scope note. **Money-display sweep**: `PriceBreakdown` rows now render through `` (every row carries «تومان», not just the total — the exact Toman/Rial ambiguity the audit flagged); `InstallmentScheduleRow`'s `hideUnit` was dropped so every installment amount carries the unit too. ## What is now testable (and exactly how) 1. Login as a seeded customer with an accepted request → `/fa/bookings/checkout?request_id=…` at 375px: the nurse's avatar + verified badge sit above a large total figure; the sticky bar (total + «پرداخت») pins above the bottom nav. Tap «پرداخت» → both CTAs (card + BNPL) disable, label swaps to «در حال شروع…». 2. Complete the mock capture round-trip → `checkout/return` shows «بازگشت از درگاه ✓» → «در انتظار تایید بانک» with an animated current-node pulse and a duration hint — no bare spinner+chip stack. 3. Land on the confirmation: کد پیگیری renders LTR with a working copy-to-clipboard (toast confirms); Shamsi paid-at, method, booking reference, and the escrow line are present; tap «چطور کار می‌کند؟» → the 3-step explainer expands. Block the network and reload → skeleton then a retryable error, never a silently-missing amount. 4. Open «کیف‌پول» → four tabs. «پرداخت‌ها» lists the just-made payment linking to the booking; «اقساط» is the unchanged installment tracker; «استردادها» shows a `RefundStatusCard` after cancelling a paid booking; «رسیدها» links to the invoice. All four read at the same content width as checkout/invoice. 5. Back on checkout, tap «پرداخت اقساطی» → provider rows show a tinted monogram (no two-letter glyph); plan cards show پیش‌پرداخت/قسط ماهانه/مجموع بازپرداخت in Toman with the fee delta on fee plans and no `LinearProgress`; selecting a plan names the header total; the eligibility check shows a spinner + label change while pending; an invalid BNPL return link offers «رزروهای من», not a dead "pay with card" promise. 6. `/fa/bookings/checkout/bnpl/gateway` in a production build (`npm run build && npm start`) → 404; `npm run dev` → still reachable. Open a paid booking's invoice → buyer/service/visit-date/reference rows present (mock); print preview shows an A4 page with a footer; from dark mode, the print dialog shows paper colors. 7. `npm run check` is green (`tsc` + `eslint`, zero errors) and `npm run test:ci` is green — **104/104 test suites, 447/447 tests** (3 new suites: `PaymentStateCard`, `BnplProviderLogo`, `EscrowExplainer`; existing `Money`/`PriceBreakdown`/`BnplPlanCard` suites extended for the new size/behavior). 8. Repeat 1–5 on `/en` (LTR) and in dark mode: no clipped RTL/LTR islands, tokens resolve in both schemes, «تومان» (fa) / "Toman" (en) on every amount including breakdown rows and installment amounts. ## What is mocked / waiting on a real service No new seams — everything stays behind the existing `PaymentApi`/`BnplApi`/`RefundsApi` seams (`USE_PAYMENT_MOCK=false`/`USE_BNPL_MOCK=true`/`USE_REFUNDS_MOCK=true`, all unchanged). Extended, not replaced: - `PaymentApi.getCheckoutSummary`/`getPaymentOutcome` gained mock-only `nurseAvatarUrl`/`nurseVerified`/ `trackingCode`/`paidAt`; a new `PaymentApi.getPaymentHistory` reads the mock's transaction list. - `PaymentApi.getInvoice`'s mock-issued invoices gained `paymentMethod`/`transactionReference`/ `sellerFiscalIdentity`. - `RefundsApi` gained `getMyRefunds`, reading the same in-memory refund store `getRefundByBooking` uses. - The BNPL provider-handoff harness page is now env-gated (see above) — not a seam change. See `mocks-registry.md`'s updated `PaymentApi`/`RefundsApi`/BNPL-harness rows for the exact deltas and the "make it real" steps (REQ-046/047/048/049). ## Contracts - Consumed: `services/payment` (b10/b11), `services/bnpl` (b12), `services/refunds` (b11) — all unchanged contract-wise; the mock/real seam split is untouched. - Requested (`dev/shared-working-context/frontend/requests/for-backend.md`, next free numbers 046–049): - **REQ-046** — `nurseAvatarUrl`/`nurseVerified` on `CheckoutSummaryDto` (the C6 identity moment) + `trackingCode`/`paidAt` on `PaymentOutcomeDto` (the confirmation receipt). Extends REQ-016/017. - **REQ-047** — a customer payment-transactions list (`GET bookings/payment_history` proposed) for the wallet «پرداخت‌ها» tab. - **REQ-048** — a customer "all my refunds" list (`GET refunds/my` proposed) for the wallet «استردادها» tab — confirmed b11 truly has only by-booking/by-id customer reads. Extends REQ-021. - **REQ-049** — `paymentMethod`/`transactionReference`/`sellerFiscalIdentity` on `InvoiceDto` for the fiscal-grade invoice. Extends REQ-018. ## Docs updated - `client/CLAUDE.md` "Project Structure": the checkout/return/confirmation/bnpl subtree (identity moment, sticky bar, staged wait state, receipt fields, provider-logo/eligibility/plan-naming polish, the gateway harness env-gate), the invoice line (fiscal fields + A4 print), the wallet line (4-tab hub replacing the installments-only shell), the `Money`/`PriceBreakdown`/`EscrowNotice`/`InstallmentScheduleRow`/ `BnplPlanCard` component lines, and three new component entries (`PaymentStateCard`, `BnplProviderLogo`, `EscrowExplainer`). The `services/payment`/`services/refunds`/`services/bnpl` domain-summary lines note the new hooks/DTO fields. ## Follow-ups for later phases - **Buyer name on the invoice is a live client-side join** (`useCustomerProfile()`), not a snapshot on `InvoiceDto` — correct for MVP but not strictly immutable (a later profile-name change would reflect on an old invoice). If invoice immutability becomes a real concern, a future REQ should ask for a server-snapshotted `buyerName` at issue time. - **`variantName()`'s best-effort `variantSnapshotJson` parse is now duplicated a third time** (`BookingDetailView.tsx`, the review page, and this phase's invoice page) — REQ-045 (typed `variantSnapshot`) would let all three collapse to one read; not extracted to a shared helper this phase to avoid touching files outside this phase's lane speculatively. - **Wallet «پرداخت‌ها»/«رسیدها» merge two independent seams client-side** (card `PaymentApi` history + BNPL `WalletInstallmentPlan` down-payment rows) rather than one unified "customer transactions" read — kept the domains separate per existing architecture; if a future phase adds a true unified ledger read, the merge in `useWalletHistoryRows.ts` can retire. - The BNPL card's fee-delta math (`totalIrr − orderAmountIrr`) needs the order amount threaded down through `PlanStep`/`BnplPlanCard` — fine today (both wizard steps already have it in scope) but worth folding into a single served `feeIrr` field per plan if REQ-022 (options/schedule) ever lands, so the client stops doing even this exact-remainder subtraction. - Real gateway/Shaparak logos near the pay CTA are still DEFERRED (no licensed assets) — `secure_gateway_notice` (lock icon + text) is the interim trust signal, per the phase's own scope note. ## Memory Saved a `project`-type memory (`ui_phase_6_checkout_and_money.md`, indexed in `MEMORY.md`) covering: the `PaymentStateCard`/`BnplProviderLogo`/`EscrowExplainer` extraction decisions, the provider-logo (monogram roundel, no brand colors) and money-width (one `CONTENT_MAX_WIDTH`) decisions, the fee-delta-as-exact- remainder pattern, the wallet history/receipts merge-two-seams-client-side decision, the sticky-bar-vs- bottom-nav layering (reuses `StickyActionBar`'s existing safe-area composition, never re-implemented), the print-stylesheet `@page` addition, and REQ-046..049 for whichever backend phase next touches b10/b11/b12.