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
+342
View File
@@ -0,0 +1,342 @@
# 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 (D1D5) — 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.