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

10 KiB
Raw Blame History

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.

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 summaryoptionCode + 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-apinotifier 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 expiredstatus='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)