Files
baya-monorepo/dev/phases/frontend/frontend-phase-11-b12.md
T
2026-06-28 21:59:59 +03:30

26 KiB
Raw Blame History

Frontend Phase 11 — BNPL checkout (installments)

Mission: give the family an alternative to full-card payment at checkout — pay a booking in installments through a provider (دیجی‌پی / اسنپ‌پی / اقساط بالین‌یار). Build the five BNPL screens from the wireframe (D1 method → D2 plan → D3 eligibility → D4 contract/schedule → D5 wallet status), wired to the b12 BNPL endpoints, styled in the financial terracotta language the design system reserves for money/installments. The load-bearing product truth you must encode in the UI: the installment repayment is owned by the provider, not Balinyaar — the provider pays Balinyaar the full amount up-front and bears 100% of customer-default risk, so Balinyaar shows the installment status it is told about; it does not run the schedule. D5 is provider-reported status, never a Balinyaar-managed ledger.

Track: frontend · Depends on: frontend-phase-9-b10.md (checkout & payment) + the b12 BNPL contract (dev/contracts/domains/bnpl.md) · Unlocks: nothing downstream depends on it (BNPL is an alternate checkout branch) Before you start, read ../_shared/agent-operating-rules.md. It is not optional.


1. Context — where this sits

We are at the alternate-checkout branch of the customer money path. By f9 a family can already see the price breakdown (C6 خلاصه و پرداخت) and pay a confirmed booking by card. This phase adds the second exit off C6: instead of paying the full amount on a card, the family chooses اقساط (installments) and is taken through a provider's BNPL flow — pick a provider, pick a plan, pass a credit check, accept a repayment schedule, pay the down-payment — after which the booking confirms exactly as the card path confirms it (the provider has paid Balinyaar in full). The family then tracks repayment from the کیف‌پول (Wallet) tab.

The single rule that shapes every screen: in Balinyaar's books a BNPL order is identical to a card payment that lands net-of-fee in one inbound settlement (product/business/09). The customer's 4-installment (or 3/6/12-month) repayment is decoupled from Balinyaar's escrow/EVV/payout cycle. So the Wallet screen (D5) renders a provider-reported balance and due-date list — not a ledger Balinyaar owns or can settle. The copy must make that ownership clear without scaring the user.

What already exists (do not rebuild) — from prior phases:

  • Foundations (frontend-phase-0.md): the three actor shells, the customer 5-tab bottom nav (خانه · رزروها · بیماران · کیف‌پول/Wallet · پروفایل), the services/{domain} + TanStack Query pattern (a keys.ts factory, apis/clientApi.ts, one-hook-per-file, mutation-invalidates-cache), the money/format util (formatIrrToToman, integer-safe IRR parse, Shamsi date display) in src/utils/, the shared composites (stepper/progress header, status chip, price-breakdown), the i18n namespaces — including a reserved bnpl namespace — and the RTL baseline. Reuse all of it; do not re-derive the data pattern.
  • Checkout & payment (frontend-phase-9-b10.md): the C6 summary-&-pay screen with the commission/tax breakdown and the escrow notice, the card-payment redirect flow, the confirmation screen, and the booking-confirmed state. This phase adds a branch to C6's payment-method step and reuses C6's confirmation/booking-confirmed UI once the down-payment clears — it does not build a second confirmation.
  • Refund & cancellation (frontend-phase-10-b11.md): the cancellation flow and the customer refund-status view, including the BNPL revert ETA copy ("~710 business days, provider-owned"). When a BNPL booking is cancelled, the refund status surface from f10 is what the user sees — do not build a BNPL-specific refund screen here; that path belongs to f10. This phase owns only the forward checkout (D1D4) and the status view (D5).

Branch note: BNPL is gated to bookings above a configurable threshold (e.g. total/duration) — a config flag, not a feature, per scope notes. If the contract exposes an bnplEligible flag on the booking/checkout summary, hide the "اقساط" option when it's false; otherwise show it always and let D3 eligibility be the gate. Do not invent a client-side threshold rule.

2. Required reading (do this first)

  • ../_shared/agent-operating-rules.md and ../_shared/frontend-conventions-checklist.md.
  • client/CLAUDE.md — the engineering contract (RSC/client boundary, layouts, i18n, theme, cookies, the fetch services, anti-patterns). You add a services/bnpl domain and screens under the customer shell; nothing above [locale].
  • Invoke the frontend-designer skill before any visual work. It is the design/brand contract. For this phase it carries the rule that matters most: money/installment surfaces use the terracotta financial accent (--bal-terracotta #d98c6a, the wireframe's "installments/financial" legend colour) — D1D5 are terracotta-accented, distinct from the teal of the core booking flow. Ask it for the installment-plan card, the eligibility-result panel, the repayment-schedule table, and the outstanding-balance Wallet card.
  • Product truth:
    • product/business/09-installments-bnpl.md — the full-upfront / provider-bears-risk / decoupled-repayment model. This is why D5 is provider-reported.
    • product/payments/bnpl-landscape.md — the provider comparison: SnappPay (4 interest-free), Digipay (3/6/12 + 4-installment), Torob Pay (25% down, 6.6%), Balinyaar in-house plan. Use these for the provider/plan copy and fee/down-payment shapes D1/D2 show. Note: settlement timing is not instant and commission is per-contract — never hardcode a fee in the client; render whatever the contract returns.
  • Wireframe: product/wireframes/index.html, Section D (D1D5) — the exact screens, RTL Persian, terracotta financial accent. D1 روش پرداخت, D2 انتخاب طرح اقساط, D3 اعتبارسنجی, D4 تایید طرح و قرارداد, D5 پیگیری اقساط (in Wallet). D5 carries the bottom tab nav (Wallet active); D1D4 are mid-flow (no tab bar).
  • Contract to consume: dev/contracts/domains/bnpl.md (from b12) — the request/response shapes, routes, the BNPL status enum codes, and the eligibility result shape. Plus ../../contracts/conventions/money-and-types.md (IRR-as-string, Toman display, enums-as-codes, UTC + Shamsi) and api-conventions.md (the envelope). Types come from the contract — do not guess shapes.
  • The existing src/services/payment/* from f9 (the checkout service) and src/services/auth/* — the exact service pattern (types.ts / keys.ts / apis/clientApi.ts / hooks/use*.ts / index.ts) your new services/bnpl copies; and the C6 screen you branch from.

3. Scope — build this

A new services/bnpl domain + the five wireframe screens, all under the customer shell, all terracotta-financial. Build the forward checkout (D1D4) and the Wallet status view (D5).

3.1 services/bnpl (the data layer — copy the f0 pattern)

Under src/services/bnpl/:

  • types.ts — derived from bnpl.md. At minimum: BnplProvider (providerCode e.g. digipay / snapppay / balinyaar, display name, supported plans), BnplPlanOption (termMonths 3/6/12 or installmentCount 4, feePercent, downPaymentPercent, monthlyAmountIrr, totalIrr — all IRR amounts as string), BnplEligibilityResult (eligibilityStatus: eligible / not_eligible / ceiling_exceeded, creditCeilingIrr), BnplSchedule (downPaymentIrr, dueDate + amountIrr per installment), BnplTransaction (status state-machine code: eligible / token_issued / verified / settled / reverted / cancelled / failed, installmentCount, outstandingBalanceIrr, installments[] with per-row status and dueDate). All money is the IRR-string type from the money-and-types contract.
  • keys.ts — a query-key factory: bnplKeys.providers(bookingId), bnplKeys.eligibility(bookingId), bnplKeys.transaction(bookingId) / bnplKeys.walletStatus().
  • apis/clientApi.ts wrapping clientFetchgetBnplOptions(bookingId), checkEligibility({ bookingId, nationalId, mobile, consent }), issueToken({ bookingId, providerCode, planSelection }), acceptSchedule({ bookingId, ... }) / pay-down-payment, getBnplTransaction(bookingId), and getWalletInstallments(). Map these to the b12 routes from the contract (POST /checkout/bnpl/eligibility, POST /checkout/bnpl/token, the provider-handoff/verify return, and the read endpoints). If a route or shape is missing from the contract, see §4.
  • hooks/ — one hook per file: useBnplOptions (query), useCheckEligibility (mutation), useIssueBnplToken (mutation), useAcceptBnplSchedule (mutation, invalidates the booking + checkout query so the confirmed booking is not refetched stale), useBnplTransaction (query), useWalletInstallments (query). Deliberate staleTime (eligibility/options are short-lived; wallet-status is moderate). Don't toast 401/403/5xx — only domain 4xx (ineligible, ceiling-exceeded, token-expired) get a message.
  • index.ts barrel.

3.2 D1 · روش پرداخت (Payment method) — the branch off C6

The payment-method chooser the family reaches from C6 (Summary & pay). Shows the payable amount (reuse the f0 price-breakdown / money util — Toman display), then the method options:

  • پرداخت کامل با کارت (full card) — selecting it returns to / continues the f9 card flow (do not rebuild it).
  • Installment providers (terracotta-accented option cards, each with provider branding/label and a one-line plan summary): دیجی‌پی (312 installments), اسنپ‌پی (۴ قسط بدون بهره / 4 interest-free), اقساط بالین‌یار (in-house plan). Render the provider list from useBnplOptions — do not hardcode the provider set or fees; the contract is the source. Primary action: "ادامه با {provider}".

States: options-loading (skeleton), loaded, empty/none-eligible (only card shown), error (retry / fall back to card). If the booking is not BNPL-eligible (§1 branch note), the installment options are hidden and only card shows.

3.3 D2 · انتخاب طرح اقساط (Choose plan)

For the chosen provider, the plan selector. Shows the total amount and the plan options the contract returned — e.g. ۳ ماهه (بدون کارمزد), ۶ ماهه (کارمزد ۴٪), ۱۲ ماهه (کارمزد ۹٪) — each rendering its monthly amount and (where present) down-payment % (پیش‌پرداخت ۲۰٪). A single-select plan card group (terracotta), plus a down-payment indicator. Every amount comes through the money util from the contract's IRR strings — the client computes nothing about money beyond formatting; if the contract gives per-plan monthlyAmountIrr use it, otherwise show only what the contract provides. Primary action: "ادامه". States: loading, loaded, none (no plans for this provider → back to D1).

3.4 D3 · اعتبارسنجی (Credit eligibility)

The provider credit check. Fields: کد ملی (national ID), موبایل (mobile, prefilled from the session), and a consent checkbox — "با استعلام اعتبارسنجی … موافقم" — which gates the submit (no consent → disabled). On submit, call useCheckEligibility. Result panel:

  • approved → "اعتبار شما تایید شد" + the returned credit ceiling (سقف اعتبار, money util). Action: "تایید و ادامه" → D4.
  • not_eligible / ceiling_exceeded → a clear declined panel with a "پرداخت با کارت" fall-back to the f9 card flow. (Ceiling-exceeded copy: the booking total exceeds the available credit.)
  • error/timeout → retry or fall back to card.

National-ID input validation is client-side format only (10 digits) — the real check is the provider's; surface its result, don't pre-judge. Reuse the f0 phone-field for the mobile display.

3.5 D4 · تایید طرح و قرارداد (Schedule & contract)

The repayment schedule + contract acceptance. Renders the repayment table from the contract's schedule: a پیش‌پرداخت (down-payment) — today row, then قسط ۱…N rows each with a Shamsi due date and an amount (money util). A terms/contract acceptance checkbox that gates the final action. Primary action: "تایید نهایی و پرداخت پیش‌پرداخت"useIssueBnplToken / useAcceptBnplSchedule, which performs the provider handoff (redirect or in-app token state — follow whatever the contract specifies, mirroring f9's card redirect handling) and pays the down-payment. On success: the booking confirms — route to the f9 confirmation / booking-confirmed UI (reused, not rebuilt), now reflecting "paid via installments". States: schedule-loading, handoff/redirect in-progress (spinner + "در حال انتقال به {provider}"), success (→ confirmation), declined/expired-token (→ retry or card).

The contract-acceptance copy must reflect the ownership truth: the installment agreement is between the customer and the provider (provider-financed, provider bears risk); Balinyaar is the merchant being paid in full. Get this exact copy from the frontend-designer skill / product docs, in both locales.

3.6 D5 · پیگیری اقساط (Installment status — in Wallet)

The Wallet-tab view of an active installment plan (bottom tab nav, Wallet active). It reads useWalletInstallments and renders a provider-reported status:

  • Outstanding-balance card (مانده بدهی, money util) — terracotta.
  • Next-installment date + an "پرداخت زودهنگام" (early pay) affordance — but early-pay is a provider action, not a Balinyaar transaction: link out / hand off to the provider; do not build a Balinyaar payment for it.
  • Due-date list — one row per installment with a status chip (reuse the f0 status chip): پرداخت‌شده (paid) / سررسید نزدیک (due soon) / آینده (future), each with a Shamsi due date and amount.
  • A short "وضعیت اقساط نزد {provider} ثبت می‌شود" / provider-owned note so the user understands Balinyaar is displaying, not managing, this schedule.

States: no-active-plan (empty Wallet installments section), loading, error (provider status unavailable → "وضعیت اقساط در دسترس نیست"). If the f12 nurse-earnings Wallet content also lands here later, keep D5 a self-contained section under the Wallet route.

3.7 i18n & tokens

Every user-visible string is a key in the bnpl namespace (seeded in f0) in both messages/en.json and messages/fa.json, in sync, fa default & RTL-first. Provider names render from the contract but their surrounding copy is i18n. Colours from tokens.css (the terracotta financial accent via the designer's tokens) — never hardcoded hex in sx.

Out of scope (DEFERRED — do not build here)

  • BNPL refund / revert UI — the cancellation + refund-status surface (incl. the BNPL "~710 business days, provider-owned" ETA) is f10 (frontend-phase-10-b11.md). Don't duplicate it.
  • Admin BNPL revert/cancel console — admin-side BNPL ops live in f15 (frontend-phase-15-b15.md).
  • Multiple-provider routing / tranched settlement — b12 ships one provider mock; treat the provider list as data, but don't build provider-comparison or multi-provider reconciliation UI.
  • Customer per-installment webhook / default handling — there is none on Balinyaar's side; the provider owns it (D5 is read-only status).

4. Mocks & seams in this phase

No new client seam family is introduced here — you reuse the services/{domain} seam pattern from frontend-phase-0.md. The BNPL provider integration itself is mocked on the backend behind IBnplProvider (b12) — the frontend just consumes the b12 endpoints.

  • If the b12 contract is published and live: wire services/bnpl/apis/clientApi.ts to the real routes; no client mock needed.
  • If b12 is not yet merged when you run: build a mock clientApi behind the same services/bnpl seam (per operating-rules §6) that returns deterministic shapes — a provider list, an always-eligible eligibility result with a sample ceiling, a sample 6-month plan + schedule, and a sample D5 status with a paid/due/future mix — so D1D5 are fully exercisable. Record the mock in your report and as a row in reports/mocks-registry.md; it is swapped for the real clientApi (one file) once b12 lands.
  • Contract gaps: any shape the contract doesn't provide that D1D5 need (e.g. per-plan monthlyAmountIrr, the schedule rows, the D5 provider-reported installments[], a bnplEligible flag on the booking) → append a request to dev/shared-working-context/frontend/requests/for-backend.md and mock behind the seam meanwhile. Never edit backend files.

5. Critical rules you must not get wrong

  • The installment repayment is OWNED BY THE PROVIDER. Balinyaar shows status; it does not run the schedule. D5 renders provider-reported balance/due-dates/status — it is not a Balinyaar-managed ledger and Balinyaar never settles a customer installment. "Early pay" hands off to the provider.
  • The provider pays Balinyaar the full amount up-front and bears 100% of customer-default risk — the contract-acceptance copy (D4) and the D5 ownership note must reflect that the installment agreement is customer ↔ provider, interest-free-to-customer, provider-financed. Do not imply Balinyaar lends.
  • Money correctness (verbatim, the payments-track invariants that bind the client): all internal money is IRR BIGINT, no floats anywhere — the client receives IRR as strings and must use the money util (formatIrrToToman / integer-safe parse) for every amount; never Number()-coerce an IRR string or do arithmetic on money in the client. The split is gross = commission + payout; the ledger is append-only and balanced; settlement is reconciled via webhook idempotency; payout is one per booking and dispute-window-gated. The client never computes commission, fee, ceiling, or schedule amounts — it renders whatever the contract returns (commission and BNPL fee are per-contract config, never hardcoded in the UI).
  • A confirmed BNPL booking is, to Balinyaar, a card payment that landed net-of-fee — after the down-payment clears, reuse the f9 booking-confirmed/confirmation UI; do not build a parallel confirmation, and do not show the customer a Balinyaar-side installment ledger.
  • Eligibility & consent are gates. D3 submit is disabled without the consent checkbox; D4 final action is disabled without contract acceptance. Decline / ceiling-exceeded must offer the card fall-back (f9), never a dead end.
  • RSC/client boundary — payment interactions are client components; don't pull next-intl/server or server cookies into them. Caching: set queryKey/staleTime deliberately; the accept-schedule mutation invalidates the booking/checkout queries so the confirmed booking isn't refetched stale and the wallet status reflects the new plan.
  • RTL-first, fa default, both locales in sync; terracotta financial accent from tokens (not teal, not hardcoded hex); MUI v9 primitives reused (no re-implemented Button/Card); shareable composites at the shared level. Keep npm run check green throughout.

6. Definition of Done

The shared definition-of-done.md, plus:

  • services/bnpl exists following the f0 pattern (types from the b12 contract, keys.ts, clientApi, one-hook-per-file, deliberate caching + mutation invalidation).
  • D1D5 are built under the customer shell, terracotta-financial (frontend-designer-driven), with all states (loading / loaded / empty / declined / error) handled; D5 lives under the Wallet tab.
  • The branch off C6 works: choosing اقساط leads into D1D4 and, on a cleared down-payment, routes to the reused f9 confirmation with the booking confirmed; the card fall-back is reachable from decline/ceiling-exceeded/error.
  • D5 is presented as provider-reported status with the ownership note; early-pay hands off to the provider; no Balinyaar-side installment ledger is implied anywhere.
  • Every money value renders through the money util; nothing about money is computed client-side; no hardcoded fee/ceiling/provider set.
  • bnpl strings in both en.json/fa.json, in sync, RTL-correct; colours from tokens.
  • npm run check green; npm run test:ci green for any shared composite you add/touch (e.g. a reusable installment-schedule row/card or plan-option card gets a co-located *.test.tsx).
  • client/CLAUDE.md Project Structure updated if you add a route group/folder for the BNPL screens or the services/bnpl domain.

7. How to test (what a human can verify after this phase)

Run npm run dev, sign in as a customer, reach a confirmed booking's checkout (C6) — using the f9 flow (or the seam mock if b12 isn't merged):

  1. On C6, choose اقساطD1 shows the payable amount + provider options (دیجی‌پی / اسنپ‌پی / اقساط بالین‌یار) loaded from the contract/mock, terracotta-accented. Pick a provider → "ادامه با …".
  2. D2 shows the plan options (e.g. ۳/۶/۱۲ ماهه) with monthly amount + down-payment for the chosen provider; select a plan → "ادامه". Verify amounts render in Toman via the money util.
  3. D3 — enter کد ملی, confirm the prefilled mobile, tick consent (submit stays disabled until you do), submit → mock approves with a credit ceiling → "تایید و ادامه". Then verify the declined path (mock a not_eligible/ceiling_exceeded) shows the panel and the card fall-back.
  4. D4 — the repayment table shows پیش‌پرداخت (today) + قسط rows with Shamsi due dates and amounts; accept the contract (final action disabled until ticked) → "تایید نهایی و پرداخت پیش‌پرداخت" → provider handoff → the booking confirms and you land on the f9 confirmation marked as paid via installments.
  5. Open the کیف‌پول (Wallet) tab → D5 shows the provider-reported outstanding balance, next due date, the per-installment due list with status chips (پرداخت‌شده / سررسید نزدیک / آینده), the ownership note, and an early-pay hand-off (not a Balinyaar payment).
  6. Switch locale to en → all BNPL strings translate, layout flips correctly (RTL ⇄ LTR); dark mode intact.
  7. npm run check and (if a shared composite changed) npm run test:ci pass.

This becomes the "what can be tested" section of your report.

8. Hand off & document (close the phase)

  • Docs: update client/CLAUDE.md Project Structure for the BNPL route(s) and the services/bnpl domain; if you establish a reusable installment-schedule/plan-option composite, note it. If you discover a BNPL business-rule gap while building (e.g. the eligibility/ceiling copy, the threshold flag), record the decision in product/business/09-installments-bnpl.md — don't invent rules; flag uncertain ones in your report.
  • Contract: consume dev/contracts/domains/bnpl.md (b12) for all types/routes — do not guess shapes. Any gap (per-plan monthly amount, schedule rows, D5 installments[], a bnplEligible flag) goes to dev/shared-working-context/frontend/requests/for-backend.md.
  • Handoff & report: append your phase summary to shared-working-context/frontend/STATUS.md; write reports/frontend-phase-11-report.md (the D1D5 screens + services/bnpl built; what is now testable and exactly how — the C6→D1→…→D4→confirm→D5 walkthrough; what is mocked behind the seam and how it swaps to the real b12 clientApi; the contract consumed + any gaps filed; follow-ups for f15 admin BNPL). Add/refresh the BNPL row in reports/mocks-registry.md if you mocked the client seam.
  • Memory: save a project memory note for the non-obvious decision that D5 is provider-reported, not a Balinyaar-managed ledger, and that BNPL confirmation reuses the f9 confirmation — with a MEMORY.md pointer, so a future agent doesn't build a Balinyaar installment ledger or a duplicate confirmation screen.