ui phase 6

This commit is contained in:
hamid
2026-07-19 09:49:25 +03:30
parent 4c70d8e424
commit a438edeeaa
54 changed files with 1766 additions and 475 deletions
@@ -0,0 +1,208 @@
# 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 `<Money size="xl">` 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 `<Money>` (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 15 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 046049):
- **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.