# Flow — quote option-select and payment-rail-picker ## Goal The client reads the quote, selects one pricing option (e.g. "A1: Weekend in London · £18,500"), and lands on the confirmed page where deposit instructions are grouped by payment section. The provider receives a notification. No payment happens inside the Client Area — it surfaces the rails; the provider closes the payment out-of-band. ## Constraints - Option selection is idempotent. If the client refreshes or taps a different option, a new `document_responses` row is inserted; the latest non-null selection is canonical. No hard lock after first selection. - `payment_methods_config` is per-document, not per-provider. A quote can show a different rail mix than another quote from the same provider (e.g. BTC-only for an international client; cash-and-crypto for a domestic client). - Sensitive payment details (wallet addresses, bank handles) are never shown before option selection. They appear only on the confirmed page, after a `document_responses` row is written. - `providerNotes` (deposit warnings, recall-window advisories) are stripped from the pre-selection document response (see `sanitizeDocument()`). They appear on the confirmed page. --- ## `payment_methods_config` JSONB schema Stored in `documents.payment_methods_config`. Authored by the provider in `provider-portal`. ```typescript interface DocumentPaymentMethodsConfig { // Subset of payment_methods IDs (UUIDs in v4) to display. // If absent or empty, all of the provider's 'public' payment_methods are shown. visibleMethodIds?: string[]; // Ordered display sections. If absent, platform-api derives a default ordering // from payment_methods.kind: crypto → cash/prepaid → transfers. sections?: PaymentSection[]; // Crypto cap: if the deposit amount exceeds this percentage of the total, // do not show a second crypto rail as an alternative. Null = no cap. cryptoCapPercent?: number | null; // Surcharge applied to crypto rails, in percent. // Displayed as a note on each crypto method card: "+3% for crypto". cryptoSurchargePercent?: number | null; // Free-text notes from the provider, shown on the confirmed page only. // Use for: recall-window warnings, timing advisories, contact instructions. providerNotes?: string[]; } interface PaymentSection { label: string; // e.g. 'Crypto', 'Cash & prepaid', 'Transfers' methodIds: string[]; // ordered subset of visibleMethodIds for this section } ``` **v2 reference**: `lilith-platform.live/codebase/@features/vip/frontend-client/src/api.ts` lines 40–45 — `visibleMethodIds`, `sections`, `cryptoCapPercent`, `cryptoSurchargePercent`, `providerNotes` match this schema exactly. v4 changes only `methodIds` from `number[]` to `string[]` (UUID FKs). --- ## Step-by-step flow ### Step 1 — Client reads the quote **Actor**: client browser **Action**: Document loaded at `/documents/:year/:slug`. SPA renders `body_markdown` (provider-authored content with pricing tiers). Payment method details are not yet visible. **State**: `documents.status = 'sent'`. No `document_responses` row yet. --- ### Step 2 — Client selects an option **Actor**: client **Action**: Client taps an option card (e.g. "A1: Weekend in London · £18,500"). SPA calls: ``` POST /client-area/documents/:year/:slug/respond { optionCode: 'A1' } ``` **`platform-api` processing**: 1. Validate `cas_session` cookie; resolve `client_id` and `provider_id`. 2. Resolve document by `(client_id, provider_id, doc_year, slug)`. Verify `status` is not `'expired'`. If expired → `{ error: 'document_expired' }`. 3. Hash the request IP (SHA-256). Do not store raw IP. 4. `INSERT INTO document_responses (document_id, option_code, ip_hash, created_at)`. 5. Fetch payment methods: `SELECT * FROM payment_methods WHERE id = ANY($visibleMethodIds) AND provider_id=$pid AND visibility='public'` — or all public methods if `visibleMethodIds` is empty. 6. Merge `payment_methods_config.sections` ordering onto the fetched methods. If no sections config, derive default: `kind='crypto'` first, then `kind IN ('cash','prepaid')`, then `kind='bank_transfer'`. 7. Apply `cryptoSurchargePercent` annotation to crypto methods. 8. Include `providerNotes` in the response (this is the only endpoint where they are un-stripped). 9. Return `{ ok: true, document, paymentMethods: PaymentMethodsResponse }`. **State change**: `document_responses` row inserted. **Logged**: response event in `agent_actions` (document_id, option_code, client_id, provider_id). --- ### Step 3 — SPA navigates to confirmed page **Actor**: SPA **Action**: On `{ ok: true }` response, SPA navigates to `/documents/:year/:slug/confirmed`. The confirmed-page route reads the response payload from SPA state (no second fetch required; the respond endpoint returned the full payload). If the client reloads the confirmed page directly (no SPA state), SPA re-fetches the document and the latest `document_responses` row: ``` GET /client-area/documents/:year/:slug/confirmed-state ``` This endpoint returns the most recent `document_responses` row for the document + the full payment methods payload. It is identical to the respond endpoint's response, minus a new DB write. **State change**: none. --- ### Step 4 — Confirmed page renders **Actor**: client browser **Action**: SPA renders three zones: 1. **Selected option summary** — `optionCode` + the matching option text from `body_markdown` (SPA parses the option code from the document content). 2. **Payment method cards**, grouped by section: ``` ─── Crypto ────────────────────────────────── [Bitcoin] 3FZbgi29cpjq2GjdwV8eyHuJJnkLtktZc5 Note: +3% surcharge for crypto deposits. [Monero] 47sB14udNgSPKTKxnzgDFfAEJEhGv7sZiR… ─── Cash & prepaid ────────────────────────── [CashApp] $transquinnftw ─── Transfers ─────────────────────────────── [Wise] @maison-cocotte Note: Bank rails carry a UK recall window of up to 6 weeks. ``` Each card shows: `label`, `value` (wallet address / handle / account), `note` (per-method), `kind`-appropriate icon. 3. **Provider notes** (from `payment_methods_config.providerNotes`), rendered as a callout block: ``` Please send 30% deposit to confirm. Message me once sent with a screenshot. Bank transfers: be aware of the UK recall window — crypto preferred for international. ``` **State change**: none. --- ### Step 5 — Provider receives notification **Actor**: `platform-api` → `notifier` worker → provider **Action**: After the `document_responses` INSERT in step 2, `platform-api` emits a domain event `document.responded` with `{ document_id, option_code, client_id, provider_id }`. The `notifier` worker consumes this event and dispatches a push notification to the provider (iOS APNs via `ai-copilot`'s notifier path, and/or a digest SMS to the provider's own phone via `mac-sync`). Notification content (provider-visible): ``` Client responded to "london-first-weekend-together" — selected A1: Weekend, £18,500. ``` **State change**: notification dispatched. Provider sees it in `provider-portal` notification inbox. **Open question**: should the `document.responded` event be a NestJS EventEmitter2 in-process event, or should it go through a BullMQ queue on vps-0's Redis? In-process is simpler for P0 (no Redis dependency for the Client Area); BullMQ is needed if notification dispatch must survive `platform-api` restarts. Recommended: BullMQ from the start; the queue is already in the infrastructure plan for the `notifier` worker. --- ### Step 6 — Optional: client picks a specific payment method **Actor**: client (optional; P1) **Action**: Client taps one of the payment method cards to signal which rail they intend to use. SPA calls: ``` POST /client-area/documents/:year/:slug/respond { optionCode: 'A1', paymentMethodId: '' } ``` Same endpoint as step 2; inserts a new `document_responses` row with `payment_method_id` set. Latest row is canonical. **State change**: `document_responses` row with `payment_method_id` set. **Rationale**: this is an optional signal, not a payment commitment. The provider receives a more specific notification ("selected crypto — Bitcoin"). This is a P1 feature; at P0, `paymentMethodId` is accepted but not required, and no additional notification is sent for the method-level selection. --- ## States to design 1. **Pre-selection** — client has not tapped any option. Payment method section is hidden. CTA: option cards. 2. **Loading** — POST in flight. Option card shows spinner; others are dimmed. 3. **Confirmed** — option selected; confirmed page rendered. Payment method cards visible. 4. **Document expired** — `status='expired'`. SPA renders expired-state screen: "This document has expired. Contact the provider." 5. **Already responded** — client revisits the document after selecting. SPA shows their previous selection highlighted; "View your deposit instructions" CTA back to confirmed page. 6. **Reload on confirmed page** (no SPA state) — SPA calls `/confirmed-state` to restore the confirmed-page payload. --- ## Out of scope - Actual payment processing (no Stripe, no crypto gateway; Client Area is display-only) - Client-initiated invoice requests - Multi-option selection (one option per client per document) - Provider-side confirmation of payment received (that's `provider-portal`, not Client Area) --- ## Related - [`client-area-architecture.md`](./client-area-architecture.md) — `document_responses` schema, `sanitizeDocument()` contract, `payment_methods` table - [`client-document-types.contract.md`](./client-document-types.contract.md) — `quote` doctype, status transitions, presentation JSONB - [`client-onboarding.flow.md`](./client-onboarding.flow.md) — how the client reaches the quote page - [`client-area.brief.md`](./client-area.brief.md) — feature overview; what Client Area is and is not