# Frontend Phase 11 — BNPL installment checkout (D1–D5) — report

**Track:** frontend · **Depends on:** f9 (checkout & payment) + the b12 BNPL contract · **Date:** 2026-07-10
**Gate:** `npm run check` green · `npm run test:ci` green (223 tests, +9) · `npm run build` green with
`NEXT_PUBLIC_API_URL` set (the build's missing-`.env` prerender failure is pre-existing/environmental and
affects unrelated pages).

## What was built

The **second exit off C6**: instead of paying the full card amount, the family chooses **اقساط** and is taken
through a provider BNPL flow (pick provider → pick plan → credit check → accept schedule → pay down-payment),
after which **the booking confirms exactly as the card path** (the provider paid Balinyaar in full). Repayment
is then tracked from the **کیف‌پول (Wallet)** tab.

### `services/bnpl` (new domain, f0 pattern)
- **`types.ts`** — the b12 enums (`BnplStatus`/`BnplEligibilityStatus`/`ProviderCode`), `BnplProvider` +
  `BnplPlanOption` (per-plan served monthly/down-payment/total IRR strings), `BnplEligibilityResult`,
  `BnplSchedule` + `BnplInstallmentRow`, `BnplOrderStatus`, `WalletInstallmentPlan` (D5 provider-reported),
  the `BnplApi` seam, and helpers (`isTerminalBnplStatus`, `isBnplSettlementSuccess`, `installmentStatusKind`).
  **All money is the IRR digit-string type.**
- **`keys.ts`** — `bnplKeys.options/eligibility/schedule/order/walletInstallments`.
- **`apis/`** — `clientApi.ts` (real, maps b12 `eligibility`/`initiate`/order-by-id 1:1; options/schedule/
  wallet/by-request-order are REQ-022/024 gaps on proposed slugs), `mockApi.ts` (**PRIMARY**), selecting
  `index.ts` (`USE_BNPL_MOCK`).
- **`hooks/`** — `useBnplOptions`/`useCheckEligibility`/`useBnplSchedule`/`useIssueBnplToken`/
  `useAcceptBnplSchedule`(invalidates booking+checkout+wallet)/`useBnplOrder`(bounded backoff poll)/
  `useWalletInstallments`. `invalidations.ts` reuses the f9 `invalidateAfterPaymentSuccess` + the wallet key.

### Screens (customer shell, terracotta financial accent = `--bal-secondary`)
- **D1 روش پرداخت** (`checkout/bnpl/page.tsx` + `MethodStep.tsx`) — payable amount + full-card option
  (returns to the f9 card flow) + provider option cards **from `useBnplOptions`** (never hardcoded) + an
  ownership disclosure at the point of choice. A stateful wizard (`StepperHeader`) drives D1→D4.
- **D2 انتخاب طرح** (`PlanStep.tsx`) — single-select `BnplPlanCard` group (term/fee/served monthly/down-payment).
- **D3 اعتبارسنجی** (`EligibilityStep.tsx`) — کد ملی (10-digit client format) + prefilled موبایل (session) +
  **consent-gated** submit → `useCheckEligibility` → approved (credit ceiling) / declined+ceiling-exceeded
  (declined panel **+ card fall-back**).
- **D4 تایید طرح و قرارداد** (`ScheduleStep.tsx`) — served repayment table (`InstallmentScheduleRow`) +
  the ownership-truth note + a **contract-acceptance-gated** final action → `useIssueBnplToken` handoff.
- **Provider handoff** (`bnpl/gateway/page.tsx` harness → `bnpl/return/page.tsx`) — settle (down-payment
  cleared) → invalidate → **the reused f9 confirmation** (`?method=bnpl` adds «پرداخت‌شده با اقساط»).
- **D5 پیگیری اقساط** (`wallet/page.tsx` → `WalletInstallments.tsx`) — provider-reported outstanding
  balance (terracotta card), next-installment + **early-pay provider hand-off** (opens the provider URL, not
  a Balinyaar payment), per-installment due list with status chips, and the provider-owned ownership note.

### Shared composites (tested)
- **`BnplPlanCard`** — D2 plan option (terracotta selectable).
- **`InstallmentScheduleRow`** — one repayment row, reused by D4 schedule + D5 wallet due list.

### Wiring / changes
- C6 (`checkout/page.tsx`): the «پرداخت اقساطی» button is enabled (`BNPL_ENABLED = true`) and navigates into
  the wizard with `?request_id=`.
- The f9 confirmation renders the installments line on `?method=bnpl` (no parallel confirmation built).
- New `ROUTES.CHECKOUT_BNPL[_GATEWAY|_RETURN]`, the `installments` AppIcon, and the `bnpl` i18n namespace
  (87 keys, both locales in sync, RTL-first, ICU-`number` params for Persian digits).

## What is now testable, and exactly how

Run `npm run dev`, sign in as a customer, reach a **confirmed** booking's checkout (C6) via the f9 flow (the
seam mock closes the loop):
1. **C6 → اقساط → D1**: payable amount + دیجی‌پی/اسنپ‌پی/اقساط بالین‌یار (terracotta). Pick → «ادامه با …».
2. **D2**: plans (۳/۶/۱۲ ماهه or ۴ قسط) with monthly + down-payment; amounts render in Toman via the money util.
3. **D3**: enter کد ملی, tick consent (submit stays disabled until consent) → **mock approves** with a ceiling.
   **Declined path:** a national ID ending in `0` → `not_eligible` (declined panel + card fall-back);
   an order above the ceiling → `ceiling_exceeded`.
4. **D4**: پیش‌پرداخت (امروز) + قسط rows with **Shamsi** due dates; accept the contract (final action disabled
   until ticked) → handoff harness → **the booking confirms**, landing on the f9 confirmation marked اقساطی.
5. **کیف‌پول (Wallet) → D5**: provider-reported outstanding balance + next due + per-installment due list
   (پرداخت‌شده / سررسید نزدیک / آینده), the ownership note, and an **early-pay hand-off** (opens the provider).
6. Switch to `en` → all BNPL strings translate, RTL⇄LTR flips; dark mode intact.

## What is mocked / waiting on a real service

`services/bnpl` is **mock-primary** (`USE_BNPL_MOCK = true`) — see the `BnplApi` row in
[`mocks-registry.md`](./mocks-registry.md). The mock reads the frozen request gross from the f7 store, plays
the provider (eligibility verdict, plan math, token/redirect), and on settle **reuses the f9 conversion
bridge** (`mockInsertConvertedBooking` + `mockMarkBookingRequestConverted`) — a settled BNPL order is a card
payment net-of-fee — then seeds a provider-reported Wallet plan. All money is served IRR digit-strings (the
mock computes plan/schedule with integer BigInt math; components only format). Swap is a one-line flip once
the upstream is real and the REQs land.

## Contracts consumed + gaps filed

Consumed [`dev/contracts/domains/bnpl.md`](../../contracts/domains/bnpl.md) (b12) for all types/routes.
Filed to [`for-backend.md`](../frontend/requests/for-backend.md):
- **REQ-022** — provider/plan options (D1/D2) + repayment schedule (D4); the contract serves neither and
  explicitly does not model the repayment schedule. Also: add `balinyaar` to the `provider_code` enum.
- **REQ-023** — BNPL eligibility should accept the D3 KYC inputs (national ID / mobile / consent).
- **REQ-024** — provider-reported Wallet installment status (D5), a customer-readable `bookingId` on the
  settled order, and a by-request order lookup for the return poll (`GET checkout_bnpl/{id}` is order-id-keyed).

## Adversarial review (multi-agent workflow)

Ran a 6-dimension review (money · ownership · contract · reuse/caching · gates/flow · i18n/RTL/theme) with an
adversarial verify pass. Of the raised findings, **5 were confirmed and all fixed**; the rest were refuted
(mock-allowed, non-reachable, or f9-parity conventions):
1. Return-page **window-expired** branch had a mislabeled CTA + wrong copy → now reuses the f9
   `window_expired_*` + `back_to_request` keys (label matches the destination).
2. Real `getBnplOrder` keyed by request id vs the contract's order-id path → the settle-on-return now reads
   the order **by its own id** (contract-correct); the by-request poll is honestly flagged as a REQ-024 gap.
3. Return-page order poll fired one needless fetch on immediate success → gated on the late-settle case only.
4. Dead i18n keys (`col_*`) deleted; the strongest ownership disclosure (`ownership_note`) is now rendered at D1.
5. D1 provider-glyph text was near-illegible in dark mode → switched to the `--bal-secondary-dark` text token.

## Follow-ups for later phases
- **f15 admin BNPL** — the admin revert/cancel console (b12 `admin_bnpl/*`) is out of scope here.
- **BNPL refund/revert** — the customer cancellation + BNPL revert ETA surface is **f10** (not duplicated here).
- When REQ-022/023/024 land, flip `USE_BNPL_MOCK = false` (one line); if `f12` nurse-earnings Wallet content
  lands, it sits beside the self-contained D5 section under `/wallet`.
