# 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`](./frontend-phase-9-b10.md) (checkout > & payment) + the **b12 BNPL contract** ([`dev/contracts/domains/bnpl.md`](../../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`](../_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](../../../product/business/09-installments-bnpl.md)). 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`](./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`](./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`](./frontend-phase-10-b11.md)): the cancellation flow and the customer refund-status view, including the **BNPL revert ETA** copy ("~7–10 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 (D1–D4) 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](../README.md#scope-notes--deferrals). 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`](../_shared/agent-operating-rules.md) and [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md). - [`client/CLAUDE.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) — D1–D5 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`](../../../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`](../../../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`](../../../product/wireframes/index.html), **Section D (D1–D5)** — the exact screens, RTL Persian, terracotta financial accent. D1 روش پرداخت, D2 انتخاب طرح اقساط, D3 اعتبارسنجی, D4 تایید طرح و قرارداد, D5 پیگیری اقساط (in Wallet). D5 carries the bottom tab nav (Wallet active); D1–D4 are mid-flow (no tab bar). - **Contract to consume:** [`dev/contracts/domains/bnpl.md`](../../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`](../../contracts/conventions/money-and-types.md) (IRR-as-string, Toman display, enums-as-codes, UTC + Shamsi) and [`api-conventions.md`](../../contracts/conventions/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 (D1–D4) 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`](../../contracts/domains/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 `clientFetch` — `getBnplOptions(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): **دیجی‌پی** (3–12 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 "~7–10 business days, provider-owned" ETA) is **f10** ([`frontend-phase-10-b11.md`](./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`](./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`](./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 D1–D5 are fully exercisable. Record the mock in your report and as a row in [`reports/mocks-registry.md`](../../shared-working-context/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 D1–D5 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`](../../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](../_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). - [ ] **D1–D5** 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 D1–D4 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`](../../../product/business/09-installments-bnpl.md) — don't invent rules; flag uncertain ones in your report. - **Contract:** **consume** [`dev/contracts/domains/bnpl.md`](../../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`](../../shared-working-context/frontend/requests/for-backend.md). - **Handoff & report:** append your phase summary to [`shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md); write `reports/frontend-phase-11-report.md` (the D1–D5 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`](../../shared-working-context/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.