Files
2026-06-28 21:59:59 +03:30

332 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ("~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](../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) — 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`](../../../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
(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`](../../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 (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`](../../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): **دیجی‌پی** (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`](./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 D1D5 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 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`](../../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).
- [ ] **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`](../../../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 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`](../../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.