8.4 KiB
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), theBnplApiseam, 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 b12eligibility/initiate/order-by-id 1:1; options/schedule/ wallet/by-request-order are REQ-022/024 gaps on proposed slugs),mockApi.ts(PRIMARY), selectingindex.ts(USE_BNPL_MOCK).hooks/—useBnplOptions/useCheckEligibility/useBnplSchedule/useIssueBnplToken/useAcceptBnplSchedule(invalidates booking+checkout+wallet)/useBnplOrder(bounded backoff poll)/useWalletInstallments.invalidations.tsreuses the f9invalidateAfterPaymentSuccess+ 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 fromuseBnplOptions(never hardcoded) + an ownership disclosure at the point of choice. A stateful wizard (StepperHeader) drives D1→D4. - D2 انتخاب طرح (
PlanStep.tsx) — single-selectBnplPlanCardgroup (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 →useIssueBnplTokenhandoff. - Provider handoff (
bnpl/gateway/page.tsxharness →bnpl/return/page.tsx) — settle (down-payment cleared) → invalidate → the reused f9 confirmation (?method=bnpladds «پرداختشده با اقساط»). - 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], theinstallmentsAppIcon, and thebnpli18n namespace (87 keys, both locales in sync, RTL-first, ICU-numberparams 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):
- C6 → اقساط → D1: payable amount + دیجیپی/اسنپپی/اقساط بالینیار (terracotta). Pick → «ادامه با …».
- D2: plans (۳/۶/۱۲ ماهه or ۴ قسط) with monthly + down-payment; amounts render in Toman via the money util.
- 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. - 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 اقساطی.
- کیفپول (Wallet) → D5: provider-reported outstanding balance + next due + per-installment due list (پرداختشده / سررسید نزدیک / آینده), the ownership note, and an early-pay hand-off (opens the provider).
- 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. 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 (b12) for all types/routes.
Filed to 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
balinyaarto theprovider_codeenum. - 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
bookingIdon 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):
- Return-page window-expired branch had a mislabeled CTA + wrong copy → now reuses the f9
window_expired_*+back_to_requestkeys (label matches the destination). - Real
getBnplOrderkeyed 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. - Return-page order poll fired one needless fetch on immediate success → gated on the late-settle case only.
- Dead i18n keys (
col_*) deleted; the strongest ownership disclosure (ownership_note) is now rendered at D1. - D1 provider-glyph text was near-illegible in dark mode → switched to the
--bal-secondary-darktext 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); iff12nurse-earnings Wallet content lands, it sits beside the self-contained D5 section under/wallet.