# Frontend Phase 9 — Checkout, card payment & invoice

> **Mission:** turn an accepted booking request into paid, confirmed money on the rails. Build the
> **C6 خلاصه و پرداخت** summary screen (the "✓ پرستار تایید کرد" badge, the reconciling
> service-cost / commission / tax / total breakdown, and the load-bearing escrow trust notice), then
> the **card payment** flow — initiate → mock gateway redirect → return → pending callback →
> succeeded → booking flips to **confirmed** — followed by the **confirmation** screen and a
> downloadable **invoice** with the VAT-on-commission line. This is the family's first real money
> moment in Balinyaar; the breakdown must reconcile to the rial and the escrow copy must build trust,
> because the whole anti-disintermediation thesis rests on payment happening *on-platform*.
>
> **Track:** frontend · **Depends on:** [`frontend-phase-8-b9.md`](./frontend-phase-8-b9.md) (booking
> detail / sessions / EVV) + backend phase **b10** (Payments core — the contract you consume) ·
> **Unlocks:** [`frontend-phase-10-b11.md`](./frontend-phase-10-b11.md) (refunds & cancellation),
> [`frontend-phase-11-b12.md`](./frontend-phase-11-b12.md) (BNPL checkout)
> **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 payment seam of the customer journey. By the end of **f7-b8** the family can send a
booking request and a nurse can accept it; by the end of **f8-b9** there is a booking-detail screen,
session list, and EVV. What's missing is the bridge the wireframe calls **C6**: once the nurse has
accepted (`accepted_awaiting_payment`), the family sees the price, pays by card, and the
`booking_request` converts into a money-bearing `booking` that reaches **confirmed**. Backend **b10**
just shipped the money core (ledger, transactions, webhook idempotency, card capture → confirm →
convert) behind the `IPaymentProvider`/`IWebhookVerifier` seams; this phase is its frontend
counterpart. After this, **f10** can cancel/refund and **f11** can offer BNPL as an alternative to
the full-card path you build here.

**What already exists (do not rebuild) — from prior phases:**

- **The foundation (f0)** — the three actor shells + route groups, the `services/{domain}` + TanStack
  Query caching pattern (copy the `auth` service shape), the contracts→types pattern, the **money/format
  util** in `src/utils/` (`formatIrrToToman`, integer-safe IRR-string parse, Shamsi date display) that
  **every** price in this phase renders through, the shared composite **price-breakdown** and
  **status-chip** components, the i18n namespaces (including `payment`), and the RTL baseline. See
  [`frontend-phase-0.md`](./frontend-phase-0.md).
- **Auth & role routing (f1-b2)** — `AuthContext` with roles; the customer app shell + 5-tab bottom nav.
- **Booking request flow (f7-b8)** — the request form (C4) and **awaiting-acceptance (C5)** status
  tracker. C6 is the *next* node after that tracker's "در انتظار تایید پرستار" step resolves to
  accepted; reuse C5's status-tracker component and the `booking`/`booking_requests` service shapes.
- **Booking detail & sessions (f8-b9)** — the `services/booking` domain (detail/list queries, status
  timeline, the three-amount split already present on the booking payload), the booking status chip,
  and the booking-detail route this phase links *back to* after confirmation. **Reuse this service**;
  do not create a parallel booking service.

> **Out of scope here (DEFERRED):** the **BNPL** method/plan/eligibility/contract screens (D1–D5) — that
> is [`frontend-phase-11-b12.md`](./frontend-phase-11-b12.md); this phase builds **only** the full-card
> path and must leave a clean "یا پرداخت اقساطی" seam on C6/D1 for f11 to attach to. **Cancellation &
> refund** (policy fee disclosure, refund status, BNPL ETA) is
> [`frontend-phase-10-b11.md`](./frontend-phase-10-b11.md). The admin-side refund console is **f15**. The
> مودیان (e-invoicing) registration state is a backend concern — surface it read-only if the contract
> exposes it, never drive it.

## 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) — how you
  work, the gate, the contract/handoff lanes.
- [`../../../client/CLAUDE.md`](../../../client/CLAUDE.md) — the engineering contract (RSC/client boundary,
  `services/{domain}` + Query caching, the toast contract: do **not** toast 401/403/5xx in hooks, only
  domain 4xx; cookies/constants rules; MUI v9 only). Non-negotiable.
- **Invoke the `frontend-designer` skill** — the design/brand contract (palette, the **terracotta**
  financial accent vs **teal** brand, tokens, typography, the `App*` library, the mobile RTL shell, the
  hard UI rules). **All visual work on C6, the redirect/pending/confirmation states, and the invoice goes
  through it.** Do not hand-style money screens off-token.
- **The contract you consume:** [`../../contracts/domains/payments.md`](../../contracts/domains/payments.md)
  (produced by **b10**) — the request/response shapes, routes, status codes, and the **payment status
  enum** for initiate / verify / confirm / get-transaction. If the **invoice** shape lives in a separate
  doc, also read `../../contracts/domains/refunds-invoices.md` (from **b11**) — if that file is not yet
  published, mock the invoice behind the `services/payment` seam and file the gap (see §4 and §8).
  Always cross-check against the published `../../contracts/openapi/README.md` / `swagger.json` snapshot
  for exact casing — **derive types from the contract, never guess shapes.**
- [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md) — **money
  is IRR rials as a string of digits on the wire**; parse with the f0 integer-safe helper, format to Toman
  for display, never do float math; `gross_price_irr = balinyaar_commission_irr + nurse_payout_amount`;
  enums are stable string codes (map to i18n labels, never hardcode a display label off the code).
- [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) — the
  `OperationResult`/`ApiResult` envelope (`clientFetch` already unwraps it), `snake_case` URL segments,
  status codes (`409` = idempotency/state-machine conflict — handle it as "already paid / in progress",
  not an error toast), and the **idempotency key** requirement on the money-path POST.
- **Product truth (read before designing the breakdown/escrow copy):**
  - [`../../../product/business/08-payments-and-escrow.md`](../../../product/business/08-payments-and-escrow.md) —
    merchant-of-record, the three-amount split, **escrow is an internal ledger state, not held cash**,
    VAT applies to **commission only**. This is *why* the C6 copy reads the way it does.
  - [`../../../product/payments/index.md`](../../../product/payments/index.md) — the fintech overview;
    card → PSP → Shaparak → IBAN rails, the تسهیم split, the "PSP received ≠ cash in bank" timing reality
    that justifies a **pending-callback** UI state rather than assuming instant success.
  - [`../../../product/wireframes/index.html`](../../../product/wireframes/index.html) — **C6** is the exact
    screen you implement (the badge, the breakdown rows, the escrow notice, the "ادامه پرداخت ←" CTA);
    confirm the labels and RTL layout against it before building.
- **Code to mirror:** `client/src/services/auth/*` (the `types.ts`/`keys.ts`/`apis/clientApi.ts`/
  `hooks/use*.ts`/`index.ts` shape every domain copies) and the f8 `services/booking` service; the f0
  **price-breakdown** and **status-chip** components; the f0 money util in `src/utils/`.

## 3. Scope — build this

Everything below lives under `client/` (the customer app shell). Build a `services/payment` domain, the
hooks, the C6 + payment-state + confirmation + invoice screens, and the small composites they need —
each visual surface produced via the **frontend-designer** skill, RTL-first, both locales.

### 3.1 `services/payment` domain (the data layer)

Mirror the `auth`/`booking` service shape exactly. Types come from
[`payments.md`](../../contracts/domains/payments.md) — do not invent fields.

- **`types.ts`** — string-literal unions + DTOs derived from the contract. Expect (confirm exact names
  against the published swagger):
  - `PaymentTransactionStatus` = `initiated` | `pending` | `succeeded` | `failed` | `cancelled` (the
    contract's `payment_transactions.status` enum).
  - `BookingStatus` reused from `services/booking` — the value that flips `pending_payment` → `confirmed`.
  - `CheckoutSummary` — the C6 payload: `booking_id`/`booking_request_id`, `nurse` mini-info, the
    **three amounts** (`gross_price_irr`, `balinyaar_commission_irr`, `nurse_payout_amount` as IRR
    digit-strings), a derived/served **service cost** and **tax (VAT) line** (`vat_irr` +
    `vat_rate`), `total_irr` (= `gross_price_irr`), and the escrow flag/copy keys. If the breakdown
    rows aren't all served, compute display rows **only** from served amounts via the f0 integer-safe
    helper — never float-derive tax client-side; if a needed line is missing, request it (§8).
  - `InitiatePaymentRequest` (`booking_id` / `booking_request_id`, `gateway` selector, an
    **idempotency key**) and `InitiatePaymentResult` (`transaction_id`, `redirect_url`,
    `gateway_reference_code`).
  - `VerifyPaymentResult` / `PaymentTransaction` (status, `gateway_reference_code`, amount, the
    `booking_id` it confirmed).
  - `Invoice` — `invoice_number`, issued-at, the line items including the **VAT-on-commission** line,
    `download_url`/`pdf_url` if served (else a client-rendered receipt).
- **`keys.ts`** — a key factory: `paymentKeys.all`, `paymentKeys.summary(bookingRequestId)`,
  `paymentKeys.transaction(transactionId)`, `paymentKeys.invoice(bookingId)`.
- **`apis/clientApi.ts`** — a `PaymentClientApi` namespace wrapping `clientFetch` (one call per
  endpoint). Map to the b10 routes from the contract (illustrative `snake_case` slugs — use the
  published ones):
  - `getCheckoutSummary(bookingRequestId)` → `GET .../payment/get_checkout_summary`.
  - `initiatePayment(req)` → `POST .../payment/initiate_payment` (sends the **idempotency key**).
  - `verifyPayment({ gateway_reference_code, transaction_id })` → `POST .../payment/verify_payment`
    (the **server-side re-check** on gateway return — never trust the redirect query alone).
  - `getTransaction(transactionId)` → `GET .../payment/get_transaction` (the callback-poll target).
  - `getInvoice(bookingId)` → `GET .../payment/get_invoice` (or the refunds-invoices route).
  - **Only** add a `serverApi.ts` if an RSC needs to prefetch the summary; otherwise client-only.
- **`hooks/` — one hook per file:**
  - `useCheckoutSummary(bookingRequestId)` — `useQuery`, `staleTime` short (prices can change before
    pay), keyed `paymentKeys.summary(...)`.
  - `useInitiatePayment()` — `useMutation`; **generates and reuses one idempotency key** for the
    attempt (stable across retries — see §5); on success returns the `redirect_url`.
  - `useVerifyPayment()` — `useMutation`; called on gateway return; on `succeeded`
    **`invalidateQueries`** for the booking detail/list (so the booking flips to confirmed without a
    refetch storm) and for the transaction key.
  - `usePaymentTransaction(transactionId, { enabled })` — `useQuery` for the **pending-callback poll**;
    use `refetchInterval` with **backoff** and **stop on terminal status** (`succeeded`/`failed`/
    `cancelled`) — see §5. Do **not** aggressive-poll.
  - `useInvoice(bookingId)` — `useQuery`, longer `staleTime` (an issued invoice is immutable).
  - `index.ts` re-exports the **hooks only** (per `client/CLAUDE.md` — no `types`/`keys`/`apis` in the
    barrel).

### 3.2 Screens & routes (under the customer shell, inside `[locale]/(private-routes)`)

Decide route segments that read cleanly under the existing role-scoped customer group (e.g. a
`checkout` segment keyed by the booking-request id). No layout above `[locale]`; respect the RSC/client
boundary.

- **C6 · خلاصه و پرداخت (Summary & pay)** — the checkout screen. Composes:
  - the **"✓ پرستار تایید کرد"** acceptance badge (reuse the f0 **status-chip**, success token),
  - the nurse mini-summary (name, service, schedule) pulled from the booking,
  - the **price breakdown** via the f0 **price-breakdown** composite — rows: **هزینه خدمت** (service
    cost, e.g. "۸ ساعت"), **کارمزد بالین‌یار** (platform commission), **مالیات (VAT)** (tax), and the
    bold **مبلغ کل** (total). Every amount rendered through the f0 money util; the visible rows must
    **reconcile to the total** (§5).
  - the **escrow notice** — a distinct trust callout (reuse `AppAlert`/an info surface, teal/info
    token, **not** an error color): the load-bearing copy
    **«مبلغ به‌صورت امانی نزد بالین‌یار می‌ماند و پس از پایان ویزیت آزاد می‌شود»** (i18n key in both
    locales). This copy is product-mandated trust UX — keep it verbatim in `fa`, with a faithful `en`
    translation.
  - the primary CTA **«ادامه پرداخت ←»** (drives `useInitiatePayment` → redirect), plus a disabled-state
    **«یا پرداخت اقساطی»** seam stub that f11 wires to D1 (render it as a clearly-deferred secondary,
    not a dead button — gate behind a `bnplEnabled` flag defaulting off).
- **Card payment states** — a single payment-state surface (page or modal) driving the wireframe's
  documented checkout states **initiating → redirect-to-gateway → pending-callback → succeeded→confirmed
  → failed/retry**:
  - **initiating** — CTA shows a spinner while `useInitiatePayment` runs.
  - **redirect** — on `redirect_url`, navigate to the **mock gateway** (the b10 mock returns a redirect
    URL; in dev this is a local mock-gateway page that immediately returns success — build a tiny
    **mock-gateway return page** under the checkout segment so the round-trip is real without a PSP).
  - **return / pending-callback** — on return, read `gateway_reference_code`/`transaction_id` from the
    query, fire `useVerifyPayment`, and if status is `pending` show a **"در حال تایید پرداخت…"** state
    backed by `usePaymentTransaction` polling with backoff until terminal.
  - **succeeded → confirmed** — on `succeeded`, invalidate booking queries and route to the confirmation
    screen.
  - **failed / retry** — on `failed`/`cancelled`, show a retry affordance (re-initiate generates a
    **new** idempotency key for the new attempt) and a "بازگشت به خلاصه" link.
- **Confirmation screen** — success state: "پرداخت با موفقیت انجام شد", the booking now **confirmed**,
  a summary card, a **«مشاهده رزرو»** link back to the f8 booking-detail route, and a **«دانلود فاکتور»**
  link to the invoice.
- **Invoice view / download** — renders `useInvoice(bookingId)`: header (`invoice_number`, issued
  Shamsi date), line items, and the **VAT line on the commission** explicitly shown (per product rule:
  VAT is on Balinyaar's commission, not the nurse's earnings). If the contract serves a `pdf_url`/
  `download_url`, the download button hits it; otherwise render a clean printable receipt
  (`window.print()` styled view) — no float math, every figure via the money util. Surface the
  مودیان state read-only (e.g. «در انتظار ثبت») **only if** the contract exposes it.

### 3.3 Shared composites (build/extend at the shared level)

- **`PriceBreakdown`** — if f0 stubbed it, finish it here as the shared composite (`src/components/…`)
  with a co-located `*.test.tsx`: props are typed display rows + a total; it formats nothing itself
  beyond receiving already-formatted strings or raw IRR + the money util — keep money formatting in one
  place. A test asserts the rows render and the total equals the sum of the served amounts.
- **`EscrowNotice`** — a small shared callout composite wrapping `AppAlert` with the mandated copy key,
  so f10/f11 reuse the identical trust message. Co-located test asserts it renders the key.
- **`PaymentStatusBadge`** — map `PaymentTransactionStatus` → an i18n label + the f0 status-chip
  variant. Co-located test for each status.
- Keep page-only composition (the confirmation layout, the mock-gateway return page) in the page.

### 3.4 i18n

Add all C6 / payment-state / confirmation / invoice strings to the **`payment`** namespace in **both**
`messages/en.json` and `messages/fa.json`, in sync, RTL-first. Status/enum codes map to label keys —
never hardcode a label off a code. The escrow copy and the breakdown row labels (هزینه خدمت / کارمزد
بالین‌یار / مالیات / مبلغ کل) are keys.

## 4. Mocks & seams in this phase

- **No new cross-cutting seam is owned here.** The PSP/gateway, تسهیم split, and webhook verification
  are **backend** seams (`IPaymentProvider`, `ISettlementSplitProvider`, `IWebhookVerifier`) introduced
  in **b10** — the frontend never talks to a PSP directly; it consumes the b10 contract. Reuse the
  `services/{domain}` mock-`clientApi` seam pattern from **f0**.
- **The frontend-side mock (only if a shape is missing):** if `payments.md` (or `refunds-invoices.md`
  for the **invoice**) is not yet published when you build, implement a **mock `clientApi`** behind the
  `services/payment` seam — real-shaped responses (a `redirect_url` pointing at your local mock-gateway
  return page, a transaction that goes `initiated → pending → succeeded`, an invoice with a VAT line) —
  swap to the real `clientFetch` calls once the contract lands. **Record it** in your frontend report and
  the mock registry, and **append the missing shape** to
  `dev/shared-working-context/frontend/requests/for-backend.md` (operating-rules §6).
- **The dev mock-gateway return page** is a *test harness*, not a product feature: it exists so the
  redirect→return round-trip is exercisable without a PSP. Keep it behind the checkout segment and note
  it as test-only in the report.

## 5. Critical rules you must not get wrong

- **Money is IRR `BIGINT`, no floats.** Every amount on the wire is an **IRR digit-string**; parse it
  with the f0 **integer-safe** helper and format to **Toman** for display through the f0 money util — do
  **no float math** anywhere on the money path, in the DB, in the API, or in the client.
- **The breakdown must reconcile.** `gross = commission + payout`, and the **displayed rows must sum to
  the displayed total** (service cost + platform commission + tax = total) using integer-safe addition —
  never render a breakdown that doesn't add up, and never compute tax with a float rate client-side
  (show the served `vat_irr`; if absent, request it, don't derive it loosely).
- **VAT is on the commission, not the nurse's earnings.** The invoice's VAT line is computed on
  Balinyaar's commission (the taxable supply) — label and place it accordingly; never imply the nurse is
  taxed.
- **The escrow copy is load-bearing trust UX.** Render the mandated notice verbatim («مبلغ به‌صورت امانی
  نزد بالین‌یار می‌ماند و پس از پایان ویزیت آزاد می‌شود») as an info/trust callout, never an error tone,
  on C6 — it is *why* the family pays on-platform.
- **Webhook/callback idempotency is the backend's guarantee — don't fight it.** Send **one stable
  idempotency key** per payment attempt and **reuse it across retries of the same attempt** (a new
  attempt gets a new key). A `409` on initiate/verify means "already in progress / already captured" —
  treat it as a benign state convergence (re-fetch the transaction and continue to confirmation), **not**
  an error toast. The server enforces one succeeded transaction per booking; the UI must never try to
  double-capture.
- **Do not poll aggressively.** The pending-callback state uses `usePaymentTransaction` with
  `refetchInterval` **backoff** (e.g. start ~2s, grow, cap; bounded total attempts) and **stops on any
  terminal status**; never a tight loop. "PSP received ≠ cash in bank," so a pending state is normal —
  reflect it calmly, don't hammer the endpoint.
- **Confirmation flips the booking, by cache invalidation.** On `succeeded`, `invalidateQueries` the
  `services/booking` detail/list keys so the booking shows **confirmed** — do not refetch everything and
  do not duplicate booking state in `services/payment`.
- **Caching & re-renders.** Deliberate `queryKey`/`staleTime`; the summary is short-lived, an issued
  invoice is immutable (long `staleTime`). Use `select`/stable refs so the polling state doesn't re-render
  the whole breakdown.
- **RTL + both locales + tokens.** `fa` default & RTL; every string in both locale files; colours from
  `tokens.css` (terracotta for financial accents, info/teal for the escrow callout — never hardcoded);
  MUI v9 API only; MUI primitives stay MUI, composites live shared.
- **Boundary & fetch discipline.** Fetch only via `clientFetch` through `services/payment`; no raw
  `fetch()`; no toast for 401/403/5xx in hooks (only domain 4xx like a gateway-declined message).

## 6. Definition of Done

The shared [definition-of-done.md](../_shared/definition-of-done.md), plus:

- [ ] **C6** renders the acceptance badge, the reconciling **service cost / commission / tax / total**
      breakdown (all via the f0 money util), and the verbatim **escrow notice**; the «ادامه پرداخت ←»
      CTA initiates payment.
- [ ] The **card flow** drives all five documented states (initiating → redirect → pending-callback →
      succeeded→confirmed → failed/retry) end-to-end against the mock gateway; the **idempotency key** is
      stable per attempt; `409` converges instead of erroring.
- [ ] On success the booking flips to **confirmed** via `invalidateQueries` (verified in React Query
      Devtools — no over-fetch), and the **confirmation** screen links back to booking detail and to the
      invoice.
- [ ] The **invoice** view renders the line items with the **VAT-on-commission** line and downloads
      (served `pdf_url`) or prints a clean receipt; every figure via the money util, no float math.
- [ ] The pending-callback poll uses **backoff** and **stops on terminal status** — no aggressive loop.
- [ ] `PriceBreakdown`, `EscrowNotice`, `PaymentStatusBadge` are shared and each has a co-located
      `*.test.tsx`; the breakdown test asserts rows sum to total.
- [ ] `payment` strings in **both** `en.json`/`fa.json`, in sync; RTL verified; colours from tokens.
- [ ] `npm run check` green; `npm run test:ci` green (shared components touched); `client/CLAUDE.md`
      *Project Structure* updated for the new `services/payment` domain, checkout route segment, and new
      shared components.
- [ ] Types derive from the published contract; any gap is appended to
      `dev/shared-working-context/frontend/requests/for-backend.md` and mocked behind the
      `services/payment` seam meanwhile (recorded in the report + mock registry).

## 7. How to test (what a human can verify after this phase)

Prereq: a booking request that the nurse has **accepted** (`accepted_awaiting_payment`) — create one via
the f7 request flow + nurse accept, or seed it. Run `npm run dev`.

1. **Open C6** from the accepted request (the C5 tracker's "پرداخت و تایید نهایی" step) → the screen
   shows the **«✓ پرستار تایید کرد»** badge, the **breakdown** (هزینه خدمت / کارمزد بالین‌یار / مالیات /
   مبلغ کل) where the rows **sum to the total**, and the **escrow notice** in an info tone. Switch locale
   → `dir` flips, all strings translate, amounts still format as Toman.
2. **Pay (mock redirect)** → tap «ادامه پرداخت ←»: CTA spins (initiating) → you are redirected to the
   **mock gateway** → it returns to the checkout → **verify** runs; if pending you briefly see "در حال
   تایید پرداخت…" (poll with backoff) → it resolves **succeeded** → the **confirmation** screen appears.
3. **Booking flips to confirmed** → follow «مشاهده رزرو» to the f8 booking detail: status is now
   **confirmed** (no full refetch — confirm via React Query Devtools that only the booking keys were
   invalidated).
4. **Download the invoice** → from confirmation tap «دانلود فاکتور»: the invoice shows the
   `invoice_number`, line items, and the **VAT line on the commission**; download (or print) works; every
   figure matches C6 to the rial.
5. **Idempotency / retry** → re-trigger pay on the same attempt (double-tap / refresh on return): no
   double-capture — a `409`/already-succeeded converges to the confirmation, not an error toast. A
   **new** attempt after a simulated failure issues a new idempotency key.
6. **Gate** → `npm run check` and `npm run test:ci` pass; the `PriceBreakdown` test proves rows sum to
   total; the `PaymentStatusBadge` test covers each status.

## 8. Hand off & document (close the phase)

- **Docs:** update `client/CLAUDE.md` *Project Structure* for the new `services/payment` domain, the
  checkout route segment, and the new shared components (`PriceBreakdown` if promoted, `EscrowNotice`,
  `PaymentStatusBadge`). Note the `payment` i18n namespace usage. Don't reintroduce removed scaffolding.
- **Contract consumed:** [`../../contracts/domains/payments.md`](../../contracts/domains/payments.md)
  (b10) for checkout-summary / initiate / verify / get-transaction, and the invoice part of
  `../../contracts/domains/refunds-invoices.md` (b11) **if available**. Types come from the published
  swagger — don't guess. **Append any gap** (missing breakdown line, missing `vat_irr`/`vat_rate`,
  missing `redirect_url`, missing invoice shape, مودیان state field) to
  `dev/shared-working-context/frontend/requests/for-backend.md`.
- **Handoff & report:** append to `dev/shared-working-context/frontend/STATUS.md`; write
  `dev/shared-working-context/reports/frontend-phase-9-report.md` (what was built, **what is now testable
  and exactly how** — the steps in §7, what is mocked client-side (the gateway return harness / any
  unmet shape) and how it swaps to real, contracts consumed, follow-ups for f10/f11). Update
  `dev/shared-working-context/reports/mocks-registry.md` for the `services/payment` mock `clientApi` and
  the dev mock-gateway page (seam, what's faked, config, how to make it real once the contract lands).
- **Memory:** save a `project` memory note for the checkout state machine (initiating → redirect →
  pending-callback → succeeded → failed/retry), the **idempotency-key-per-attempt** rule, the
  **backoff poll, no aggressive loop** decision, and the **escrow-copy-is-verbatim-trust-UX** constraint,
  with a one-line `MEMORY.md` pointer.
