cocottetech/@platform/codebase/@features/client-area/docs/client-quote-confirm.flow.md

207 lines
10 KiB
Markdown
Raw 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.

# 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 4045 — `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: '<uuid>' }
```
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