add build development phases

This commit is contained in:
hamid
2026-06-28 21:59:59 +03:30
parent 1df3cd9f64
commit 53a40dc51d
52 changed files with 12379 additions and 0 deletions
@@ -0,0 +1,331 @@
# 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.