ui phase 6
This commit is contained in:
@@ -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 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.
|
||||
Reference in New Issue
Block a user