Files
baya-monorepo/dev/shared-working-context/reports/ui-phase-6-report.md
T
2026-07-19 09:49:25 +03:30

17 KiB
Raw Blame History

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-046nurseAvatarUrl/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-049paymentMethod/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.