cleanup phases 6

This commit is contained in:
hamid
2026-08-02 18:48:32 +03:30
parent e2db97392a
commit 51e86a1e5f
239 changed files with 118 additions and 70 deletions
@@ -0,0 +1,280 @@
# 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.