SRS: Stage 1 — Checkout Flow — Steps 0–5 (UC 2.1–2.6)
| Field | Value |
|---|---|
| BA in Charge | Huyen Dinh |
| Date Created | 2026-07-23 |
| Version | v19 |
| Last Updated | 2026-08-24 |
| Document References | RFQ_ Website and Dashboard Implementation V7.pdf (§Step 0–§Step 5)RFQ_ Stack Trading Prop Tech V7.pdf (§/system/status, §/calculate-cart, §/execute-checkout)Zapier Integration V7.pdf (§3.1.9 checkout_ui_routing, §3.1.10 Payment_Method_Config, §Tables A–J) |
⚠️ CR Priority (CR wins over all other sources):
- [CHR-6] adds
zip_code(VARCHAR, nullable) to Users table + newcountry_zip_requirementsreference table;GET /system/statusreturnszip_requirementsmap.- [CHR-16] overrides RFQ V7 §3.3 (Pass Rate Aggregator cron) —
cached_pass_rateis now computed viaSUM(sims_passed)/(SUM(sims_passed)+SUM(sims_failed))from the append-onlyFirm_Sim_Funnel_Snapshotstable, populated daily by Zapier Flow 29 →POST /financial/daily-snap(no custom Node.js cron job needed).
UC Index
| UC_ID | Use Case Name | Business Description |
|---|---|---|
| UC_2.1.1 | System Status API | On checkout page load, frontend calls GET /system/status once to retrieve geo-routing, pricing, cohort status, and gate flags that govern all subsequent steps. |
| UC_2.1.2 | Geo-Based Compliance UI Variants (Flow A–J) | System routes the user to a compliance UI variant (Flow A through J) based on their country and region, determined by the Cloudflare geo-IP headers. |
| UC_2.1.3 | Gate 1 — Waitlist | WhenGlobal_Var_Allow_New_Signups == FALSE, the user is redirected to a dedicated Waitlist Lead Capture page (retains standard Marketing Header/Footer). |
| UC_2.1.4 | Gate 2 — Geoblock | When the user's IP matches a sanctioned country, the checkout is replaced by a hard-stop 403 block page. |
| UC_2.1.5 | Pricing Engine | Frontend parses cohort and pricing data from the /system/status response to dynamically populate the pricing display throughout the checkout. |
| UC_2.2 | Step 1: Asset Class Selection | User selects their preferred asset class (Futures or Forex) as the first configuration step. |
| UC_2.3 | Step 2: Capital Allocation Selection | User selects one of three Evaluation Package tiers (Associate, Accelerated Associate, Advanced Associate), each with distinct pricing, notional capital, and career ladder entry points. |
| UC_2.4 | Step 3: Platform Selection | User selects their preferred trading platform from a dynamically loaded list filtered by chosen asset class. |
| UC_2.5 | Step 4: Market Data Selection | Futures users select their market data feed subscriptions (CME pre-selected and locked; NYMEX/CBOT/COMEX optional). |
| UC_2.6.1 | Step 5: PII Capture & Compliance | User fills in personal information (Name, Email, Billing Address, Country/State, Shirt Size), compliance checkboxes (flow-dependent), and triggers tax calculation. |
| UC_2.6.2 | Step 5: Lead Capture & Cart Abandonment | Two-phase capture: Email field onBlur → POST /capture-lead creates/updates a Guest record + fires Cart_Abandonment webhook. [Next] click → UPSERT full PII into the same record, navigate to Step 6. No Auth0 account exists at either phase. |
Step 0: System Initialization & Access Gates
UC_2.1.1 — System Status API
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.1.1 |
| Use Case | System Status API |
| Description | On checkout page load, the frontend calls GET /system/status exactly once. The response payload is stored in global checkout state and used by all subsequent steps to determine geo-routing, gate status, pricing, and cohort membership. |
| Zapier Flow | — |
| Zapier Table | Table C (Global_Var_Allow_New_Signups, Founder_Cohort_Open, Founder_Cohort_Limit)Table J (Founder pricing / Standard pricing) |
| 3rd Party | Cloudflare (geo-IP detection) |
2. Trigger
The user navigates to the dedicated checkout page URL for the first time in the session.
Note: Checkout is implemented as a dedicated page, not a modal or overlay on the marketing page. Four architectural requirements govern this decision: (1) Same-Tab Routing (2) Cart Abandonment Deep Link (3) Attention Ratio — once inside the checkout funnel, all off-page distractions are removed. (4) No Standard Marketing Header/Footer — the standard marketing header and footer are not rendered on this route, except for Gate 1 (Waitlist, Ref: UC_2.1.3): when triggered, the checkout page is unmounted and the user is redirected to a separate dedicated Waitlist page, which does render the standard Marketing Header and Footer.
3. Pre-conditions
- User is on the checkout page.
- No prior GET /system/status response exists in the current session.
4. Post-conditions
- The full response payload (JSON below) is stored in the global checkout state.
- Gate 1 and Gate 2 conditions are evaluated immediately.
Response Payload Schema:
{
"Global_Var_Allow_New_Signups": boolean,
"is_founder_cohort": boolean,
"pricing_tiers": { // object — keyed by product_id
"EVAL_L1": float, // raw price, no currency symbol (e.g. 650.00)
"EVAL_L2": float,
"EVAL_L5": float
},
"geo_blocked": boolean,
"geo_country": string,
"geo_region": string,
"required_flow": string,
"methods": [
{
"id": string,
"label": string,
"explanatory_text": string,
"cta_text": string,
"icon_tags": [string]
}
],
"is_launch_phase": boolean,
"historical_pass_rate": float,
"zip_requirements": { // object — keyed by country_iso2, built from country_zip_requirements table
"US": boolean,
"CA": boolean,
"IE": boolean
// ... one entry per country in country_zip_requirements
}
}Note:
current_price(float) andfounder_count(int) have been removed from this response.
Field Notes:
| Field | Meaning |
|---|---|
is_founder_cohort | Value ofGlobal_Var_Founder_Cohort_Open (Table C) — global switch toggled manually by Ops. TRUE = all new signups at this moment receive Founder status and Founder pricing; FALSE = Standard pricing applies. Not a per-user flag — same value for every visitor at any given moment. |
pricing_tiers | Object containing raw float prices for all 3 tiers (EVAL_L1, EVAL_L2, EVAL_L5), built by the backend at Step 3e based on is_founder_cohort:- TRUE → fetch the Founder Price column from Zapier Table J for each tier.- FALSE → fetch the Challenge Price column from Zapier Table J for each tier.Frontend reads this object at Step 0 to populate the 3 pricing cards at Step 2. Actual values are managed centrally in Zapier Table J — not hardcoded in backend or frontend. |
required_flow | Compliance UI variant ID (FLOW_A–FLOW_J) the frontend renders at Step 5. Full routing table: see UC_2.1.2. |
methods | Array of payment methods available for the visitor'sgeo_country. See the detailed field table below. |
is_launch_phase / historical_pass_rate | Set in Step 3f. Consumed byUC_2.1.2 Flow B (UK/Australia) and Flow C (EU/EEA) to show the pass-rate disclosure at Step 5. |
zip_requirements | Object built server-side from thecountry_zip_requirements table (see schema below) — keyed by country_iso2, value = requires_zip. Frontend stores this map in global checkout state on page load and reads it at Step 5 (UC_2.6.1) to decide whether the ZIP / Postal Code field is required or optional for the selected Country. |
country_zip_requirements Table Schema:
| Column | Type | Notes |
|---|---|---|
country_iso2 | CHAR(2), PRIMARY KEY | ISO 3166-1 alpha-2 country code. |
requires_zip | BOOLEAN, NOT NULL | TRUE = ZIP / Postal Code is required for this country at Step 5. FALSE = optional. |
Seed data source: The initial dataset for this table can be sourced from standard ISO 3166-1 postal code requirement datasets publicly available on GitHub or via services such as GeoNames, rather than manually compiled per country.
methods[] detail — rendered at Step 6 (Payment Method selection):
| Field | Type | Meaning | Where rendered |
|---|---|---|---|
id | string | =method_id in the DB. Enum: CC, CRYPTO, APPLE_PAY, GOOGLE_PAY, DUSUPAY, T365. | Selects the execution environment (NMI Collect.js / Triple-A modal / hosted-page redirect). |
label | string | Display name (e.g. "Credit or Debit Card"). | Next to the radio button, Step 6. |
explanatory_text | string | Descriptive copy (fees, conditions). | Info text block below the payment list, Step 6. |
cta_text | string | Action button text (e.g. "Complete Purchase"). Max 25 characters. | Gold action button, Step 6. |
icon_tags | [string] | Icon keys (e.g."pix", "gcash") mapped to SVG assets. Aggregator corporate logos (Dusupay, T365) are specifically excluded — only the label text is shown. | Inline with the label, Step 6. |
5. Basic Flow
- User navigates to the checkout page.
- Frontend sends GET /system/status (Cloudflare headers
CF-IPCountry,CF-Regionare included automatically). - Backend processes, in order:
-
Step 3a — Geo-IP Gate: Extracts geo-IP from
CF-IPCountry/CF-Region. QueriesCompliance_geo_restrictions.- If match → sets
geo_blocked = trueand returns the payload immediately (skips Steps 3b–3e). Frontend then renders Gate 2 (UC_2.1.4) and stops — no further checkout UI is rendered. - If no match → continues to Step 3b.
Table:
Compliance_geo_restrictionsColumn Type Notes idUUID (PK) countryVARCHAR ISO 3166-1 alpha-2 or standard string regionVARCHAR 'All'= entire country blockedspecific subdivision = only that region blocked.Query logic:WHERE country = CF-IPCountry AND (region = 'All' OR region = CF-Region)— either row matching triggers the block.banned_reasonVARCHAR Regulatory framework or policy justification created_atTIMESTAMP updated_atTIMESTAMP - If match → sets
-
Step 3b - Waitlist Gate: Reads
Global_Var_Allow_New_Signupsfrom Zapier Table C.- If
Global_Var_Allow_New_Signups == FALSE-> backend returns the payload immediately (skips Steps 3c-3f). Frontend redirects (client-side navigation, same tab) to the dedicated Waitlist page (UC_2.1.3). Checkout UI is unmounted — no further checkout UI rendered. - If
Global_Var_Allow_New_Signups == TRUE-> continues to Step 3c.
- If
-
Step 3c — UI Routing: Queries
checkout_ui_routingfor a row matchinggeo_country(+geo_regionwhere applicable).- If match → sets
required_flowto that row's value (e.g.FLOW_B,FLOW_C). - If no match → defaults
required_flow = 'FLOW_A'(Override Model — see UC_2.1.2 §3 Routing Table). - Table:
checkout_ui_routing
Column Type Notes idUUID (PK) countryVARCHAR ISO 3166-1 standard name regionVARCHAR 'All'= applies to entire country (default)specific subdivision = overrides the'All'row for that region only.Match logic (in order):(1) row wherecountry = user_country AND region = user_region→ use thatrequired_flow;(2) else row wherecountry = user_country AND region = 'All'→ use thatrequired_flow;(3) else → defaultFLOW_A.required_flowVARCHAR e.g. 'Flow B','Flow C'justificationVARCHAR Internal reference created_atTIMESTAMP updated_atTIMESTAMP - If match → sets
-
Step 3d — Gateway Filtering & Localization: Queries
Payment_Method_Configtwice and merges results into onemethodsarray:- Rows where
target_country = 'GLOBAL'ANDis_active = TRUE→ payment methods offered to every country (CC, Apple Pay, Google Pay, Crypto).GLOBALis a literal value stored in the table, not a computed default. - Rows where
target_country = geo_countryANDis_active = TRUE→ country-specific rails (e.g.target_country = 'ZA'→ Dusupay Mobile Money/Bank). - Backend exclusion rule: if
geo_country == 'IN', removeCC,APPLE_PAY, andGOOGLE_PAYfrom the merged array. India users receive only Crypto (Triple-A).
Table:
Payment_Method_ConfigColumn Type Notes idUUID (PK) target_countryString 'BR','CO','MX','PH','SG','JP','IN','ZA','KE','GB','FR','PL','NL','GLOBAL','AE'method_idString 'CC','CRYPTO','APPLE_PAY','GOOGLE_PAY','DUSUPAY','T365'gateway_routerString 'NMI','TRIPLE_A','DUSUPAY','T365'labelString explanatory_textString cta_textString Action button text (e.g. 'Complete Purchase'). Max 25 characters — enforced at both DB constraint level (VARCHAR(25)) and API validation.icon_tags[String] Icon keys for frontend asset mapping is_activeBoolean Ops toggle per row - Rows where
-
Step 3e — Cohort & Pricing Check: Fetches
is_founder_cohort(=Global_Var_Founder_Cohort_Open) from Zapier Table C — pricing switch only:is_founder_cohort == TRUE→ buildpricing_tiersfrom the Founder Price column (Table J) for EVAL_L1, EVAL_L2, EVAL_L5.is_founder_cohort == FALSE→ buildpricing_tiersfrom the Challenge Price column (Table J) for EVAL_L1, EVAL_L2, EVAL_L5.
Table: Zapier Table C (Global Variables) — flat key-value config, not a row-based DB table.
Variable Value Description Global_Var_Allow_New_Signupsboolean Master Throttling Switch — read atStep 3b (Waitlist Gate). TRUE→ checkout proceeds.FALSE→ backend returns immediately. User is redirected to the dedicated Waitlist page.Global_Var_Founder_Cohort_Openboolean Master Cohort switch — read atStep 3e. TRUE→ Founder pricing.FALSE→ Standard pricing. -
Step 3f — Launch Phase & Pass Rate (consumed by UC_2.1.2 Flow B and Flow C):- Reads
system_launch_datefromPlatform_Configuration(row id = 1).- Sets
is_launch_phase = TRUEifCurrentDate <= system_launch_date + 90 days, elseFALSE. - Reads the
cached_pass_ratecolumn and returns it in the response ashistorical_pass_rate(rename only, same value — no re-calculation here; the value is pre-computed daily, see note below). - Used by Flow B (UK/Australia) and Flow C (EU/EEA) for the pass-rate disclosure at Step 5 — all other flows ignore them.
Two separate mechanisms are involved:
- Mechanism 1 — Zapier Flow 29 (write-only): runs daily at 5:05 PM EST (5 min after market close) and fires a webhook to
POST /financial/daily-snap. Each run counts the number of users who transitioned to'PASSED'or'FAILED'status that day, and appends one new row to theFirm_Sim_Funnel_Snapshotstable (columnssims_passed,sims_failed) - see schema below. Existing rows are never updated or deleted (append-only historical ledger). This mechanism never touchesPlatform_Configuration. - Mechanism 2 — Backend cron "Pass Rate Aggregator" (query corrected): a separate Node.js cron job, still required in the backend. Corrected formula:
cached_pass_rate = SUM(sims_passed) / (SUM(sims_passed) + SUM(sims_failed)), computed across all rows ofFirm_Sim_Funnel_Snapshots. Writes the resulting float back toPlatform_Configuration(row id = 1, columncached_pass_rate). This avoids recalculating the ratio on every/system/statuscall.
Table:
Platform_Configuration— single-row global config, avoids aggregate queries on every checkout page load.Column Type Notes idInteger (PK) Locked to 1system_launch_dateDate cached_pass_rateFloat Firm_Sim_Funnel_SnapshotsColumn Type Notes idPK dayDate, Indexed timeTime Informational — exact timestamp the daily job ran. conversions_newInteger New sim payments processed that day. conversions_resetsInteger Sim reset payments processed that day. active_simsInteger Total users currently in an active sim state. sims_passedInteger Total users who successfully cleared the challenge that day. sims_failedInteger Total users who hit liquidation that day. pass_percentDecimal Per-day value: sims_passed / (sims_passed + sims_failed). Not the same as cumulativecached_pass_rateabove.avg_pass_fail_durationDecimal Average days elapsed from sim start to pass/fail. - Sets
-
- Backend returns JSON response (only reached if Step 3a did not trigger an early return due to
geo_blocked = true). - Frontend stores full response in global checkout state.
6. Exceptional Flow
-
[If GET /system/status fails — network error or timeout]
- Ref: TE-SYS-01. Checkout UI is not rendered. User may retry once network is restored.
-
[If GET /system/status fails — 5xx server error]
- Ref: FP-03. Full-page error screen replaces checkout UI.
-
[If geo headers are missing, empty, or return "XX"]
- Backend defaults to
geo_country = 'US',required_flow = 'FLOW_A'. User is NOT blocked.
- Backend defaults to
-
[If
checkout_ui_routingquery returns 0 rows]- Backend defaults to
required_flow = 'FLOW_A'. User is NOT blocked.
- Backend defaults to
7. Business Rules
BR_2.1.1.1: Single Call Per Session
GET /system/status is called exactly once when the checkout page first loads. Step navigation between Steps 1–5 does NOT trigger a new call. A full page refresh (F5) triggers a new call to re-hydrate global checkout state (pricing, geo, flow routing, etc.) — this does NOT mean the user's in-progress step/data is discarded; see BR_2.1.1.3 for the per-step reload/recovery behavior.
BR_2.1.1.2: Response Persistence
The full API response payload is stored in global checkout state. All subsequent steps read from this cached state — no re-fetching.
BR_2.1.1.3: Checkout Routing & Session Recovery Matrix (All Steps)
URL Structure: The checkout flow utilizes query parameters on a single path to define the current step (e.g. /checkout?step=selectassetclass, /checkout?step=payment). This enables deep-linking and cart-abandonment recovery (Klaviyo) to drop the user at their exact abandonment point.
Table 1 — Trigger: Page Reload (F5)
| Step | Behavior |
|---|---|
| Step 1-5 (before Email) | Stay on current step + keep data. |
| Step 5 (after Email onBlur, Guest created) | Stay on current step + keep data. Do NOT re-callPOST /capture-lead. |
| Step 6 (Payment) | On reload, the frontend immediately routes to the correct final state based on the backend outcome — no intermediate OV-05 processing overlay is re-rendered on reload:- If payment already succeeded: Frontend routes directly to Step 7 (Claim Account screen). User also receives a confirmation email (Ref: SES-01).- If payment failed (or no payment was attempted): Step 6 renders with the split-panel layout. If email is currently locked (5-Failure Email Lock, Ref: BR_2.8.1.2), Ref: OV-03 renders on top; otherwise normal split-panel, no overlay.Rationale: Payment is a single, continuous backend flow — there is no persistent mid-payment state to resume on reload. Backend idempotency is enforced via provider_event_id on the webhook (Ref: UC_2.8.1 §5 step 6.0). NMI transactions are normally fast (< 30 s), making the F5 window extremely narrow. |
| Step 7.1 (Claim Account) | Stay on claim screen + keep entered phone number. JWT token in URL remains valid if within 48h. If SIM provisioning fails in the background at this point, no indication is shown (Ref:BR_2.8.3.5). |
| Step 7.3 (Provisioning) | Check account status.Active_SIM → redirect to Auth0 login (Ref: UC_2.8.4 §5). Guest → keep showing provisioning/waiting state indefinitely (no frontend timeout). Failure handling is delegated to the backend auto-retry mechanism (Ref: UC_2.8.2 §Step 3 Exception); on exhaustion, overlay OV-07 renders (Ref: UC_2.8.4 §6 Exceptional Flow). |
Table 2 — Trigger: URL Link Click (Deep-Link)
| Step | Behavior |
|---|---|
| Step 1-5 (before Email) | Local storage exists → Resume at exact step. Local storage lost → Restart at Step 1. Email re-entry triggers Duplicate Account Check normally. |
| Step 5 (after Email onBlur, Guest created) | Local storage exists → Resume at exact step. Local storage lost → Restart at Step 1. |
| Step 6 (Payment) | Local storage exists → Resume at Step 6 (same direct-routing logic as Table 1 above applies). Local storage lost → Restart at Step 1. |
| Step 7.1 (Claim Account) | URL has valid JWT → Lands on Step 7.1 with validtransaction_id. URL has no/expired token → Fallback to Step 1. |
| Step 7.3 (Provisioning) | Reached via raw URL without token → Fallback to Step 1 → progress to Step 5 → re-check status. |
Table 3 — Trigger: Back from Marketing Page (Exit)
- If local browser storage still has the in-progress cart state: frontend restores it, and the next click on the [Checkout] button re-opens the checkout overlay directly at the step the user was on when they left — not back at Step 1. If that step is Step 6, the same direct-routing logic as Table 1 applies.
- If local storage is unavailable (cache cleared, new device): the checkout overlay restarts from Step 1, and the user re-types their email at Step 5 — including fields already captured before (name, billing address, etc.) — Step 5 displays the corresponding message.
BR_2.1.1.4: UTM Capture — Global, Root Layout (per CR-12)
The frontend reads current UTM values (utm_source, utm_medium, utm_campaign, utm_term, utm_content) from localStorage (written by the CR-12 root-layout script, not re-parsed from the URL at Step 0). This applies on every branch — including when the Waitlist Gate (UC_2.1.3, Step 3b) intercepts the flow — so email + UTM can still be attached to the direct API payload sent to ActiveCampaign/Klaviyo.
Attribution rule — last-touch, NOT first-seen wins: If the URL at any page load/route change contains UTM params, the root-layout script overwrites the existing localStorage values (last-touch attribution). If the URL has no UTM params, localStorage is left untouched (not cleared, not nulled).
All downstream consumers read the current localStorage value at the moment of their own action (not a value cached once at Step 0): Step 5 (UC_2.6.1), POST /capture-lead (UC_2.6.2), POST /execute-checkout at Step 6, and the Waitlist direct API calls to ActiveCampaign + Klaviyo (UC_2.1.3).
UC_2.1.2 — Geo-Based Compliance UI Variants (Flow A–J)
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.1.2 |
| Use Case | Geo-Based Compliance UI Variants (Flow A–J) |
| Description | Based onrequired_flow from /system/status, the frontend renders flow-specific compliance content (text disclosures, mandatory checkboxes) at Step 5 only. Step 0 itself renders no flow-specific UI. |
| Zapier Flow | — |
| Zapier Table | — |
| 3rd Party | — |
2. Trigger
required_flow (stored in global checkout state at Step 0) is read when Step 5 renders.
3. Routing Table
Sourced from checkout_ui_routing PostgreSQL table (Override Model: unmatched countries default to Flow A).
| Flow | Countries / Regions | UI Behavior at Step 5 |
|---|---|---|
| Flow A | USA + all unmatched countries (default) | 2 standard checkboxes only |
| Flow B | UK, Australia | 2 standard checkboxes + pass-rate disclosure (content varies onis_launch_phase) |
| Flow C | EU/EEA (Austria, Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Iceland, Ireland, Italy, Latvia, Liechtenstein, Lithuania, Luxembourg, Malta, Netherlands, Norway, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden) | 2 standard checkboxes + pass-rate disclosure (content varies onis_launch_phase) + 1 EU withdrawal waiver checkbox (3 total) |
| Flow D | Canada — Quebec region only | Entire checkout UI rendered in French — including all labels, placeholders, button text, error messages, policy text, popups, banners, and toasts triggered from any checkout screen (Steps 1–7). Standard 2 checkboxes (in French). |
| Flow E | United Arab Emirates | 2 standard checkboxes + UAE non-regulation disclaimer |
| Flow F | Sanctioned countries (seeCompliance_geo_restrictions table) | Hard block — Gate 2 (UC_2.1.4). Step 5 is never reached |
| Flow G | India | 2 standard checkboxes. |
| Flow H | Philippines | 2 standard checkboxes only. |
| Flow I | Brazil | 2 standard checkboxes only. |
| Flow J | South Africa | 2 standard checkboxes only. |
4. Standard Compliance Checkboxes (All Flows Except F)
Checkbox 1 — Commercial Acknowledgment:
"I acknowledge that I am purchasing a skills assessment software evaluation for commercial purposes to secure an independent contractor agreement with a US-domiciled C-Corporation, and I am not opening a retail financial, brokerage, or investment account."
Checkbox 2 — Age & Terms of Service:
"By clicking 'Complete Purchase', I confirm that I am at least 18 years of age and agree to the Terms of Service for the Data Processing and Performance Evaluation Service (Associate Track)."
Both: mandatory, default unchecked, displayed at Step 5 only.
Checkbox 2 — "Terms of Service" Click Behavior: The words "Terms of Service" inside the Checkbox 2 label are a clickable hyperlink. Clicking it opens a popup/modal (does not navigate away from Step 5, does not close/reset the checkout flow) displaying the Terms of Service content. Content is identical to the public /terms page (Ref: UC_29) — same legal text, same numbered sections, same "Last Updated" timestamp. This supersedes the prior distinct-document framing in UC_29 BR-29-03/A-10 (see changelog note there). Closing the popup (via close button or overlay click) returns the user to Step 5 with checkbox state and all other form data unchanged.
5. Flow B — Pass-Rate Disclosure (UK & Australia)
Static text block at Step 5, above standard checkboxes. Same text for UK and Australia.
-
If
is_launch_phase == TRUE:"This is a newly launched proprietary trading evaluation program. Historical pass-rate and success data is currently unavailable."
-
If
is_launch_phase == FALSE:"Historically, only [historical_pass_rate]% of participants successfully pass the evaluation to become authorized traders."
6. Flow C — EU/EEA Additional Checkbox
Checkbox 3 — EU 14-Day Withdrawal Waiver:
"I expressly consent to the immediate commencement of the digital evaluation service and waive my 14-day right of withdrawal under EU consumer protection law."
Default: unchecked. Mandatory. Positioned between standard checkboxes and the Next button.
7. Flow D — Quebec Localization
Entire checkout UI (Steps 1–7) renders in French. All labels, placeholders, button text, and ad copy within the checkout are localized to French.
8. Flow E — UAE Disclaimer
Static text block at Step 5, above standard checkboxes:
"Stack Trading is a U.S.-domiciled entity and is not licensed, registered, or regulated by the Dubai Financial Services Authority (DFSA) or the Abu Dhabi Global Market (ADGM)."
9. Hypothetical Performance Disclaimer (All Flows)
Mandatory legally-required static text block at Step 5 for all flows:
"HYPOTHETICAL OR SIMULATED PERFORMANCE RESULTS HAVE CERTAIN LIMITATIONS. UNLIKE AN ACTUAL PERFORMANCE RECORD, SIMULATED RESULTS DO NOT REPRESENT ACTUAL TRADING. ALSO, SINCE THE TRADES HAVE NOT ACTUALLY BEEN EXECUTED, THE RESULTS MAY HAVE UNDER-OR-OVER COMPENSATED FOR THE IMPACT, IF ANY, OF CERTAIN MARKET FACTORS, SUCH AS LACK OF LIQUIDITY. SIMULATED TRADING PROGRAMS IN GENERAL ARE ALSO SUBJECT TO THE FACT THAT THEY ARE DESIGNED WITH THE BENEFIT OF HINDSIGHT. NO REPRESENTATION IS BEING MADE THAT ANY ACCOUNT WILL OR IS LIKELY TO ACHIEVE PROFITS OR LOSSES SIMILAR TO THOSE SHOWN."
Static, always visible, non-collapsible. No user interaction.
7. Business Rules
BR_2.1.2.1: Flow-to-UI Mapping is Frontend-Hardcoded
The mapping of required_flow → specific UI content at Step 5 is hardcoded in the frontend. FE reads required_flow from global checkout state and renders the corresponding variant via a fixed enum switch. Changing what a flow displays requires a frontend code change. See §3 Routing Table for the full mapping.
BR_2.1.2.2: Country-to-Flow Assignment is Dynamic (DB-Driven)
Which country maps to which flow is managed via the checkout_ui_routing PostgreSQL table — configurable by Ops without a code deploy. Adding a new country rule only requires inserting a row into this table; it does not require a FE change.
UC_2.1.3 — Gate 1: Waitlist
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.1.3 |
| Use Case | Gate 1 — Waitlist |
| Description | WhenGlobal_Var_Allow_New_Signups == FALSE in the /system/status response, the user is redirected (client-side navigation, same tab) to a dedicated Waitlist page — rendered with the standard Marketing Header and Footer, replacing the prior overlay-on-checkout-page design. The user selects their primary market and enters email to join the waitlist. |
| Zapier Flow | — |
| Zapier Table | Table C (Global_Var_Allow_New_Signups flag) |
| 3rd Party | ActiveCampaign + Klaviyo (frontend makes direct, parallel API calls to BOTH, sending captured email + UTM parameters — no Zapier orchestration for either). |
References/Wireframe/Stage 1/Checkout Flow - Waitlist.png Waitlist — empty state
References/Wireframe/Stage 1/Join waitlist (choosing asset).png Waitlist — dropdown open
References/Wireframe/Stage 1/Join waitlist (Processing).png Waitlist — submitting
References/Wireframe/Stage 1/Join waitlist (succeed).png Waitlist — success state
References/Wireframe/Stage 1/Join waitlist - Invalid email.png Waitlist — validation error
References/Wireframe/Stage 1/Checkout Flow - Fail due to error.png Waitlist — CRM API submit failed
2. Trigger
Global_Var_Allow_New_Signups == FALSE detected in the GET /system/status response.
3. Pre-conditions
- User is on the checkout page.
- GET /system/status returns
Global_Var_Allow_New_Signups == FALSE.
4. Post-conditions
- Frontend makes direct API calls to BOTH Klaviyo and ActiveCampaign, sending the captured email and UTM parameters (read from
localStorageat submit time, per CR-12 / BR_2.1.1.4) to each — no Zapier orchestration for either call. - Page content replaced by success state. Ref: OV-04.
- Scope boundary: System responsibility ends at firing both direct API calls. Klaviyo automatically triggers the
Waitlist_Welcomedrip (Ref: KLA-22) — out of system; the marketing team manages that sequence's content/cadence. ActiveCampaign creates the CRM record and attaches identity to behavioral tracking — it does not send automated emails.
5. Basic Flow
- GET /system/status returns
Global_Var_Allow_New_Signups == FALSE. - Frontend redirects (client-side navigation, same tab) to the dedicated Waitlist page. The standard Marketing Header and Footer render on this page.
- User selects primary market (Futures or Forex).
- User enters email.
- User clicks [Join Waitlist].
- Button switches to "Processing..." (disabled). Frontend reads current UTM values from
localStorage(per CR-12) and makes direct API calls to BOTH Klaviyo and ActiveCampaign, sending lead data (primary market + email) and those UTM parameters to each. - On success → page content replaced by success state. Ref: OV-04.
- User clicks [Return to Homepage] → navigated to homepage.
6. Exceptional Flow
-
[If CRM API fails on submit]
- Ref: BN-02. [Join Waitlist] button remains available for retry.
- Form data is NOT cleared.
7. Business Rules
BR_2.1.3.1: Global Scope
Global_Var_Allow_New_Signups == FALSE is a global toggle — redirects ALL countries to the Waitlist page simultaneously.
BR_2.1.3.2: No Interrupt Logic
If Global_Var_Allow_New_Signups changes to FALSE while a user is mid-checkout (started when Global_Var_Allow_New_Signups == TRUE), the user completes the ENTIRE checkout flow without interruption. Frontend does NOT re-check after initial page load.
BR_2.1.3.3: CRM-Side Deduplication (No Information Leakage)
No custom backend check or database query is built to verify waitlist status. ActiveCampaign and Klaviyo handle identity resolution and deduplication natively. If the submitted email already exists, the CRM silently deduplicates or updates the record — the frontend still transitions to the success UI state (Ref: OV-04) regardless, to prevent information leakage (i.e., never reveal whether an email is already registered). This applies identically to both the ActiveCampaign and Klaviyo direct API calls — both receive the same email + UTM payload.
8. Screen Description
Page Content Header: (rendered below the standard Marketing Header — Ref: UC_1.1.1)
| Component | Content |
|---|---|
| Title | "Be First on the Waitlist" |
| Body | "Enrollment is currently paused. Join the waitlist to secure your spot and you will be notified when we re-open." |
Form Fields:
| No. | Field Name | Field Type | Validation Rule / Behaviour |
|---|---|---|---|
| 1 | Primary Market | Dropdown (Single-selection) | Display rule:- Placeholder: "Select primary market".- Options: Futures, Forex.Validation:- Required. Ref: CR-03. |
| 2 | Textbox | Display rule:- Placeholder: "Enter email".Validation:- Required. Ref: CR-09 §9.1. | |
| 3 | Join Waitlist | Button (Primary) | Behaviour:- On click: validates all fields. If invalid → show inline errors, do NOT submit.- If valid → button switches to "Processing..." (disabled). Frontend reads UTM from localStorage (per CR-12) and makes direct API calls to BOTH Klaviyo and ActiveCampaign, sending the captured email and those UTM parameters.- CRM API fail → button reverts to "Join Waitlist" (enabled). Ref: BN-02. Form data preserved.- CRM API success → Ref: OV-04. |
UC_2.1.4 — Gate 2: Geoblock
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.1.4 |
| Use Case | Gate 2 — Geoblock (Flow F) |
| Description | When GET /system/status returns HTTP 403 (user is in a sanctioned region), the entire checkout UI is replaced by a hard-stop block page. The user cannot proceed. |
| Zapier Flow | — |
| Zapier Table | — (source:Compliance_geo_restrictions PostgreSQL table) |
| 3rd Party | Cloudflare (geo-IP detection) |
References/Wireframe/Stage 1/Flow F Gate 2 - Geoblock (Absolute).png Gate 2 — Geoblock full-page block
2. Trigger
GET /system/status returns HTTP 403 — geo_blocked == true because CF-IPCountry/CF-Region matches a row in Compliance_geo_restrictions.
3. Business Rules
BR_2.1.4.1: Hard Stop — Full Page Replacement
HTTP 403 = absolute hard stop. The geoblock state replaces the ENTIRE checkout UI — no header, nav, footer, step indicators, or appeal link. Only the block message is shown (message content includes a support contact email per FP-02).
4. Screen Description
Full-page 403 block — checkout UI fully replaced. Screen content defined at Ref: FP-02.
UC_2.1.5 — Pricing Engine
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.1.5 |
| Use Case | Pricing Engine |
| Description | Frontend parses the /system/status response to dynamically populate pricing at Step 2 for all 3 Evaluation Package tiers, applying Founder or Standard pricing based on cohort status. |
| Zapier Flow | — |
| Zapier Table | Table C:Global_Var_Founder_Cohort_OpenTable J: Founder Prices, Standard Challenge Prices per tier |
| 3rd Party | — |
2. Business Rules
BR_2.1.5.1: Founder Cohort Detection
Frontend reads is_founder_cohort from the /system/status response:
TRUE→ display prices from the Founder Price column (Table J) for each tier.FALSE→ display prices from the Challenge Price column (Table J) for each tier.
Prices are not hardcoded in the frontend — always read from the pricing_tiers object in the /system/status response. Actual values are managed centrally in Zapier Table J.
BR_2.1.5.2: Race Condition — Stale Pricing
Triggered at Step 6 (Pay click) when the server-side price/tax re-check (Ref: UC_2.8.1 §5 step 6.1.a) detects either of the following:
- Founder cohort sold out: Founder cohort slot filled up between page load and Pay click, reverting to Standard pricing.
- Tax rate changed: Tax rate changed server-side between
POST /calculate-cart(Step 5) andPOST /execute-checkout(Step 6), causing the total to differ.
In either case, the backend returns PRICE_CHANGED — no charge is created, no promo reservation occurs. Frontend dismisses Ref: OV-05 and surfaces Ref: OV-08.
[Refresh now] behavior: Clicking [Refresh now] on OV-08 closes the overlay and refreshes the Order Summary with the updated price/tax. No full page reload. The user may then click [Pay] again with the updated total.
BR_2.1.5.3: Price Data Source
Founder Prices and Standard Challenge Prices for all 3 tiers are stored in Zapier Table J. Backend reads the appropriate column and packs the values into the pricing_tiers object in the /system/status response at Step 3e. Frontend renders the 3 pricing cards at Step 2 from this object — no additional API call required.
(Source: Zapier Integration V7.pdf §3.7)
| Track | Challenge Price | Futures Reset Price | Forex Reset Price | Extension Fee | Founder Price | Founder Reset Fee |
|---|---|---|---|---|---|---|
| Associate Track (Level 1) | $650 | $375 | $325 | $150 | $499 | $325 |
| Accelerated Track (Level 2) | $1,250 | $725 | $625 | $275 | $1,049 | $600 |
| Advanced Track (Level 5) | $7,000 | $3,800 | $3,500 | $1,500 | $5,599 | $3,250 |
Note: Data above reflects current Ops configuration snapshot (source: Zapier Table J). All pricing values may be changed by Ops at any time without a code deploy.
Step 1: Asset Class Selection
UC_2.2 — Asset Class Selection
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.2 |
| Use Case | Step 1: Asset Class Selection |
| Description | User selects their preferred trading asset class —Futures (via CME) or Forex (Currency pairs). |
| Zapier Flow | — |
| Zapier Table | — |
| 3rd Party | — |
References/Wireframe/Stage 1/Checkout Flow - Step 1.png Asset Class Selection
2. Trigger
User passes Gate 1 and Gate 2 at Step 0.
3. Pre-conditions
Global_Var_Allow_New_Signups == TRUEgeo_blocked == FALSE- /system/status response stored in global checkout state.
4. Post-conditions
asset_classstored in session state.- User proceeds to Step 2.
5. Basic Flow
- Step 1 renders with 2 large selection cards: Futures and Forex.
- User clicks one card.
- Selection stored in session state.
- User clicks [Next] to proceed.
6. Business Rules
BR_2.2.1: Fixed Options
2 asset class options (Futures, Forex) are fixed in the frontend — not driven by API. Both always displayed.
BR_2.2.2: Session Persistence
Asset class selection is stored in session state for the duration of the checkout flow. Persistence and reload/recovery behavior across a full page refresh is governed centrally by BR_2.1.1.3 (Step 0) — a refresh does NOT return the user to Step 1 as long as local storage cart state is present; see that rule for the exact per-step matrix.
BR_2.2.3: Asset Class Change — Progress Bar & Step Count Impact
Progress bar step count is dynamic — total number of steps differs by asset class:
- Futures: 7 steps — Step 1 → 2 → 3 → 4 (Market Data) → 5 → 6 → 7
- Forex: 6 steps — Step 1 → 2 → 3 → 5 → 6 → 7 (Step 4 omitted from flow)
This applies on initial selection at Step 1 and when navigating back to Step 1 to change selection. Progress bar re-renders to reflect the new total step count immediately on selection.
If user navigates back to Step 1 and changes asset class selection:
- Futures → Forex: Step 3 (Platform) selection is reset. Step 4 (Market Data) is removed from the flow; progress bar updates to show 6 steps total.
- Forex → Futures: Step 3 (Platform) selection is reset. Step 4 (Market Data) is added back to the flow; progress bar updates to show 7 steps total.
Step 5 (PII & Compliance) data is NOT reset on asset class change. Data is only fully cleared on full page refresh — see BR_2.2.2.
7. Screen Description
| No. | Field Name | Field Type | Validation Rule / Behaviour |
|---|---|---|---|
| 1 | Futures | Radio Group | Display rule:- Title: "Futures"- Subtitle: "via CME"- Default: unselected.Behaviour:- On click: stores asset_class = 'FUTURES'. Deselects Forex if previously selected.- Impact (if switching from Forex): Ref: BR_2.2.3. |
| 2 | Forex | Radio Group | Display rule:- Title: "Forex"- Subtitle: "Currency pairs"- Default: unselected.Behaviour:- On click: stores asset_class = 'FOREX'. Deselects Futures if previously selected.- Impact (if switching from Futures): Ref: BR_2.2.3. |
| 3 | Next | Button (Primary) | Behaviour:Disabled until one asset class is selected. On click when enabled: navigates to Step 2. |
Step 2: Capital Allocation Selection
UC_2.3 — Capital Allocation Selection
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.3 |
| Use Case | Step 2: Capital Allocation Selection |
| Description | User selects one of three Evaluation Package tiers. Each tier has distinct pricing, notional capital, career ladder entry, and live stop loss parameters. |
| Zapier Flow | — |
| Zapier Table | Table C:daily_loss_ratioTable J: Founder pricing, Standard pricing per tier |
| 3rd Party | — |
References/Wireframe/Stage 1/Step 2 future standard price.png Capital Allocation — Futures, Standard price
References/Wireframe/Stage 1/Step 2 future founder price.png Capital Allocation — Futures, Founder price
References/Wireframe/Stage 1/Step 2 forex standard price.png Capital Allocation — Forex, Standard price
References/Wireframe/Stage 1/Step 2 forex founder price.png Capital Allocation — Forex, Founder price
References/Wireframe/Stage 1/Step 2 tooltip.png Capital Allocation — Live Stop Loss tooltip
2. Trigger
User clicks [Next] at Step 1 with an asset class selected.
3. Pre-conditions
asset_classin session state.- /system/status pricing data in global checkout state.
4. Post-conditions
product_id(EVAL_L1, EVAL_L2, or EVAL_L5) stored in session state.- User proceeds to Step 3.
5. Evaluation Package Data
| Parameter | Advanced Associate Track | Accelerated Associate Track | Associate Track | Source (Zapier / Doc) |
|---|---|---|---|---|
| product_id | EVAL_L5 | EVAL_L2 | EVAL_L1 | Fixed (no Zapier table to config) |
| Standard Price (Challenge Price) | $7,000 | $1,250 | $650 | Table J (Challenge Price) |
| Founder Price | $5,599 | $1,049 | $499 | Table J (Founder Price) |
| Status Ribbon | Recommended for Pros | Best Value | Foundation | Fixed (no Zapier table to config) |
| Eval Requirement — Futures | 7 ES | 16 Micros | 8 Micros | Table B (Futures Configuration Matrix, Max_Contracts_ES_Equiv) |
| Eval Requirement — Forex | $150,000 Notional | $50,000 Notional | $25,000 Notional | Table A (Forex Configuration Matrix, Max_Forex_Notional) |
| Evaluation Target / Stop | 14% Target / 7.5% Stop (Dynamic), 60 days (Fixed) | 14% Target / 7.5% Stop (Dynamic), 60 days (Fixed) | 14% Target / 7.5% Stop (Dynamic), 60 days (Fixed) | Table C (SIM_target_percent / SIM_stop_percent) [CHR-49] — 60-day window is a separate hardcoded literal (Today + 60 Days), no Table C field |
| Career Entry Level | Level 5 | Level 2 | Level 1 | Fixed (no Zapier table to config) |
| Distance to W2 | 1 Promotion Away | 4 Promotions Away | 5 Promotions Away | Fixed (= 6 − Career Entry Levelcareer ladder has 6 levels total; Level 6 = W2/international retainer eligibility) |
| Live Capital Allocation — Futures | 7 ES | 16 Micros | 8 Micros | Table B (Futures Configuration Matrix, Max_Contracts_ES_Equiv) |
| Live Capital Allocation — Forex | $150,000 Notional | $50,000 Notional | $25,000 Notional | Table A (Forex Configuration Matrix, Max_Forex_Notional) |
| Live Stop Loss | $10,500 | $2,500 | $1,250 | Table A/B (Market_Loss_When_Stopped) |
| Live Profit Target | $14,100 | $3,750 | $1,875 | Table A/B (Profit_Target) |
Values above are a current snapshot — Tables A, B, C, and J are Ops-editable without a code deploy; frontend/backend must fetch live from
/system/status, not hardcode.Daily Loss Limit (
Daily_Loss_Ratio × max_drawdown) is a backend/ops metric — it is not displayed in the Step 2 checkout UI.Daily_Loss_Ratiois read from Table C at runtime.
6. Business Rules
BR_2.3.1: Pricing Mode — Standard vs Founder
Frontend reads is_founder_cohort from /system/status.
TRUE→ Founder mode: each card renders the original Standard Price with strikethrough (~~$7,000~~) followed by the Founder Price at full size ($5,599). The "one-time" label is NOT shown.FALSE→ Standard mode: each card renders only the Standard Price followed by the "one-time" label (e.g.,$7,000 one-time). No strikethrough.
BR_2.3.2: Card Selection
Clicking a card body to select that tier. Only one card can be selected at a time; selecting a new card deselects the previous one. The card's selected state is reflected visually on the card.
The [Next] button at the bottom of the page is a separate navigation control — it is disabled until a card is selected (see §7 Screen Description).
7. Screen Description
| No. | Field Name | Field Type | Validation Rule / Behaviour |
|---|---|---|---|
| 1 | Pricing Card (×3) — Top section | Radio Group (Single-selection) | Display rule:- 3 cards, left-to-right: Advanced Associate Track · Accelerated Associate Track · Associate Track.- Each card displays in order: Status Ribbon, Track name, Price block, Evaluation Requirements box, [Select Track] button.- Price block (Standard mode — is_founder_cohort == FALSE): Standard Price + "one-time" label.- Price block (Founder mode — is_founder_cohort == TRUE): Original Standard Price rendered with strikethrough + Founder Price at full size. No "one-time" label.- Evaluation Requirements box: Futures asset class → Eval Requirement — Futures from §5 (contract units). Forex asset class → Eval Requirement — Forex from §5 (Notional USD). Row 2: "{SIM_target_percent}% Target / {SIM_stop_percent}% Stop, 60 days" — target/stop % read live from Table C (current: 14%/7.5%), rendered dynamically not as a static string; 60 days remains a fixed literal. Same for both asset classes. [CHR-49]- No card pre-selected by default. Tier → product_id: Advanced Associate Track = EVAL_L5 · Accelerated Associate Track = EVAL_L2 · Associate Track = EVAL_L1.Behaviour:- On click anywhere on the card (including [Select Track] button): selects that tier, stores product_id in session state. Deselects previously selected card. Ref: BR_2.3.2. |
| 2 | Pricing Card (×3) — Bottom section | Static Display | Display rule:- Always visible below the [Select Track] button on each card. Not collapsible.- Header: "Live Account & Career Path (Upon Passing)".- Fields displayed (top to bottom): - Career Ladder Entry Upon Passing - Distance to W2 (or international retainer) Offer: 1 / 4 / 5 Promotions Away - Live Capital Allocation: Futures → contract units; Forex → Notional USD - Live Stop Loss — accompanied by "Firm takes 100% of the risk" and a tooltip icon ⓘ (see Row 3) - Live Profit Target |
| 3 | Live Stop Loss — Tooltip ⓘ | Tooltip | Display rule:- Icon ⓘ appears inline next to the Live Stop Loss label.- Behaviour:On hover → shows tooltip: "If you pass the evaluation, the firm backs your account with this exact amount of real capital at risk. We absorb the losses so you can focus on execution." |
| 4 | [Select Track] | Button (Secondary) | Behaviour:Per-card selection button. Clicking selects the card (equivalent to clicking the card body). Ref:BR_2.3.2. |
| 5 | [Back] | Button (Secondary) | Behaviour:On click → navigates back to Step 1. |
| 6 | [Next] | Button (Primary) | Validation:Required. Disabled until one card is selected.Behaviour:On click when enabled → navigates to Step 3. |
Step 3: Platform Selection
UC_2.4 — Platform Selection
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.4 |
| Use Case | Step 3: Platform Selection |
| Description | User selects their preferred trading platform from a dynamically loaded list filtered by the asset class chosen at Step 1. |
| Zapier Flow | — |
| Zapier Table | Table I (Platform Registry — maps platform names to gateway routing) |
| 3rd Party | — |
References/Wireframe/Stage 1/Checkout Flow - Step 3 (Future).png Platform Selection — Futures
References/Wireframe/Stage 1/Checkout Flow - Step 3 (Forex).png Platform Selection — Forex
References/Wireframe/Stage 1/Checkout Flow - Step 3 - No platforms available.png Platform Selection — No platforms available
2. Trigger
User clicks [Next] at Step 2 with a package selected.
3. Pre-conditions
asset_classin session state.
4. Post-conditions
platformstored in session state.- User proceeds to Step 4 (Futures) or Step 5 (Forex — Step 4 skipped).
5. Platform Registry (Zapier Table I)
Platform availability is controlled by the
Is_Activeflag in Zapier Table I. Ops can enable or disable any platform at any time without a code deploy.
| Platform_Name | Asset_Class | Gateway_Router | Is_Active | Risk_Group_Template |
|---|---|---|---|---|
| TradeSea | Futures | Rithmic | TRUE | Sim_Rithmic_Default |
| Quantower | Futures | Rithmic | TRUE | Sim_Rithmic_Default |
| ATAS | Futures | Rithmic | TRUE | Sim_Rithmic_Default |
| MotiveWave | Futures | Rithmic | TRUE | Sim_Rithmic_Default |
| Sierra Chart | Futures | Rithmic | TRUE | Sim_Rithmic_Default |
| MetaTrader 5 | Forex | MT5 | TRUE | Sim_MT5_Default |
| TradingView | Forex | TraderEvolution | TRUE | Sim_TV_Default |
Note: Data above reflects current Ops configuration snapshot (source: Zapier Table I).
Is_ActiveandRisk_Group_Templatemay be changed by Ops at any time without a code deploy — frontend must fetch this table dynamically (via GET /public/platform-options), not hardcode it.TradeSeareplacesNinjaTrader 8, same asset class and gateway — source table snapshot still listsNinjaTrader 8, superseded by CR.
6. Basic Flow
- Step 3 renders.
- Frontend calls GET
/public/platform-options?asset_class=[asset_class]. - Platform options render as logo-driven selection tiles.
- User clicks a tile → stored in session state.
- User clicks [Next].
7. Exceptional Flow
-
[If GET /public/platform-options returns empty array]
- Ref: OV-01.
-
[If GET /public/platform-options returns HTTP 500 or network timeout]
- Full-page error: FP-03.
8. Business Rules
BR_2.4.1: Dynamic List — No Hardcoding
Platform options fetched from GET /public/platform-options?asset_class=[asset_class].
Response structure returns only an array of Platform_Name strings (e.g. { "platforms": ["TradeSea", "Quantower"] }) — no logo/icon field exists in the response schema.
Platform logos are static frontend assets; the frontend is responsible for mapping each returned Platform_Name string to its corresponding local logo asset (no backend-provided icon URL).
BR_2.4.2: Pre-selection with 1 Result
1 platform returned → auto pre-select (highlighted). User must still click [Next] — no auto-advance.
BR_2.4.3: Navigation — Back from Later Steps
Changing platform by navigating back from Step 5 → Market Data (Step 4) is NOT reset (independent). Step 5 data is NOT reset.
9. Screen Description
| No. | Field Name | Field Type | Validation Rule / Behaviour |
|---|---|---|---|
| 1 | Platform Tiles | Radio Group (Single-selection) | Display rule:- Logo-driven selection tiles.- API returns only Platform_Name strings (no logo field in response) — frontend maps each Platform_Name to a local static logo asset.- Default: unselected (or pre-selected if only 1 option returned, per BR_2.4.2).- Ref: BR_2.4.1.Behaviour:- On click: selects tile, deselects previous. Stores platform = [Platform_Name]. |
| 2 | [Back] | Button (Secondary) | Behaviour:On click → navigates back to Step 2. |
| 3 | [Next] | Button (Primary) | Behaviour:Disabled until a platform tile is selected. On click when enabled: navigates to Step 4 (Futures) or Step 5 (Forex). |
Step 4: Market Data Selection
UC_2.5 — Market Data Selection (Futures Only)
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.5 |
| Use Case | Step 4: Market Data Selection |
| Description | Futures users select market data feed subscriptions. CME is pre-selected and locked. NYMEX, CBOT, and COMEX are optional. Step is skipped for Forex users. |
| Zapier Flow | — |
| Zapier Table | Table C (Global Variables) — product list and per-feed prices are read from Table C, not hardcoded. (Source: RFQ_ Stack Trading Prop Tech V7.pdf §3.2, Market Data Products API) |
| 3rd Party | — |
References/Wireframe/Stage 1/Checkout Flow - Step 4.png Market Data Selection (Futures only)
2. Trigger
User clicks [Next] at Step 3 AND asset_class == 'FUTURES'.
3. Pre-conditions
asset_class == 'FUTURES'- GET /public/market-data-products returns a non-empty array.
4. Post-conditions
addon_ids[]array stored in session state.- User proceeds to Step 5.
5. Basic Flow
- Step 4 renders.
- Frontend calls GET
/public/market-data-products. Backend fetches the product list and per-feed prices from Zapier Table C (Global Variables) — prices are never hardcoded in backend code (see BR_2.5.5). - Renders selection grid of toggle switches.
- CME: pre-checked and locked (cannot be toggled OFF).
- User toggles optional feeds (NYMEX, CBOT, COMEX) as desired.
- All selected feeds display:
"Cost: $0.00 (Covered by Stack Trading)". - User clicks [Next].
6. Exceptional Flow
- [If GET /public/market-data-products returns 0 products]
- Full-page error: FP-03. Treated as system error.
7. Business Rules
BR_2.5.1: CME Locked ON
CME is pre-checked and locked (locked ON) by default — cannot be unchecked by the user.
BR_2.5.2: Optional Feed Defaults
NYMEX, CBOT, COMEX default to unchecked (OFF). User actively toggles to ON.
BR_2.5.3: Cost Display
All selected options display "Cost: $0.00 (Covered by Stack Trading)". Downstream billing (Level 1–2 pay; Level 3+ reimbursed) is handled by Zapier Flow 22 (Monthly Data Fee Billing) — not surfaced here. (Source: Zapier Integration V7.pdf, Flow 22)
BR_2.5.4: Navigation — Back from Later Steps
If the user proceeds past Step 4 and later navigates back to Step 4 (e.g. Back button, or editing a prior step), the previously selected addon_ids[] toggle states are preserved and re-rendered as-is (CME still locked ON; any optional feeds the user had toggled ON remain ON). Selections are not reset to default.
BR_2.5.5: Product & Price Source — Table C (No Hardcoding)
GET /public/market-data-products MUST read the product list (id, name) and per-feed price (e.g. NYMEX_Bundle) from Zapier Table C (Global Variables) at request time. Backend code must not hardcode product IDs or prices — Table C is the single source of truth so Ops can change pricing without a deploy. This rule also applies to the authenticated dashboard variant GET /market-data-products (see Market Data Management, Dashboard Settings), which reads from the same Table C but additionally filters out feeds the user already owns. (Source: RFQ_ Stack Trading Prop Tech V7.pdf §3.2, Market Data Products / Public Market Data API — "Do not hardcode prices"; client confirmation 2026-07-31.)
8. Screen Description
| No. | Field Name | Field Type | Validation Rule / Behaviour |
|---|---|---|---|
| 1 | CME Feed | Toggle/Switch | Display rule:- ON label: "CME Level 2". Default: ON (pre-selected as mandatory). "Most Popular" badge shown.- Behaviour:Always ON, non-toggleable. Always included in addon_ids[]. |
| 2 | Optional Feeds (NYMEX / CBOT / COMEX) | Toggle/Switch | Display rule:- 3 separate toggles.- ON label per feed: "NYMEX" / "CBOT" / "COMEX".- Default: OFF for all.- Cost display: "Cost: $0.00 (Covered by Stack Trading)".Behaviour:- Toggle ON: adds feed to addon_ids[]. Toggle OFF: removes from addon_ids[]. |
| 3 | Market Data Lifecycle Disclosure | Label | Display rule:- Static text block below feed grid. Always visible. Non-collapsible.- Line 1: "Associate Track Evaluation: The Firm pays 100% of data costs."- Line 2: "Level 1 and 2: The Trader pays (Standard Exchange Professional Data rates apply)."- Line 3: "Level 3: The Trader is fully reimbursed for all Base CME market data costs incurred during Levels 1 and 2."- Line 4: "Level 3 to Level 24: The Firm covers 100% of Base CME data costs." |
| 4 | [Back] | Button (Secondary) | Behaviour:On click → navigates back to Step 3. |
| 5 | [Next] | Button (Primary) | Behaviour:Always enabled (CME always selected by default). On click: stores finaladdon_ids[] in session state. Navigates to Step 5. |
Step 5: PII Capture, Compliance & Cart Abandonment
UC_2.6.1 — PII Capture & Compliance
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.6.1 |
| Use Case | Step 5: PII Capture & Compliance |
| Description | User fills in personal information, flow-dependent compliance checkboxes render, a Sanctions pre-check runs immediately on Country/State selection, and tax calculation runs once on [Next] click (with the Sanctions Gate re-checked server-side). No price/tax display at this step (seeBR_2.6.1.5). |
| Zapier Flow | — |
| Zapier Table | — |
| 3rd Party | Quaderno (sales tax calculation via POST /calculate-cart)Everflow JavaScript SDK (affiliate click tracking — initialized on page load, storeseverflow_id as first-party cookie) |
References/Wireframe/Stage 1/Step 5 Flow A.png PII & Compliance — Flow A (Standard — US & most regions)
References/Wireframe/Stage 1/Step 5 Flow B_ Data available.png PII & Compliance — Flow B (UK/AU, pass rate data available)
References/Wireframe/Stage 1/Step 5 Flow B_ Launch state (Data unavailable).png PII & Compliance — Flow B (UK/AU, launch state)
References/Wireframe/Stage 1/Step 5 Flow C Data available.png PII & Compliance — Flow C (EU/EEA, data available)
References/Wireframe/Stage 1/Step 5 Flow C Launch state (Data unavailable).png PII & Compliance — Flow C (EU/EEA, launch state)
References/Wireframe/Stage 1/Step 5 Flow D.png PII & Compliance — Flow D (Canada)
References/Wireframe/Stage 1/Step 5 Flow E.png PII & Compliance — Flow E (UAE)
References/Wireframe/Stage 1/Step 5 Flow G.png PII & Compliance — Flow G (India)
2. Trigger
User clicks [Next] at Step 4 (Futures) or Step 3 (Forex).
3. Pre-conditions
- Session state:
asset_class,product_id,platform,addon_ids[].- For the Forex path (Step 4 skipped):
addon_idsmust be present as an empty array[].
- For the Forex path (Step 4 skipped):
required_flowavailable in global checkout state.
4. Post-conditions
- All PII data validated.
- POST /calculate-cart returned HTTP 200 (Sanctions Gate passed).
- All required compliance checkboxes checked.
- User can click [Next] to proceed to Step 6.
5. Basic Flow
-
Step 5 renders with all PII input fields. Flow-dependent compliance UI renders simultaneously per UC_2.1.2 (
required_flow). -
Current UTM values are read from
localStorage(per CR-12 / BR_2.1.1.4) — not re-parsed from the URL here. -
User fills all fields. On Email field
onBlur→ triggers UC_2.6.2 Phase 1 (Capture Lead), independent of the flow below. -
User selects Country. State/Province/Region and ZIP / Postal Code fields are all visible on initial Step 5 load. On Country selection, frontend re-evaluates both fields: if the selected country has no subdivisions → hide State/Region; if
zip_requirements[billing_country] = false→ hide ZIP. Otherwise the field stays visible. -
Sanctions pre-check (Ref: BR_2.6.1.1): on Country selection and on State/Region selection, the system immediately checks whether the selected value is a blocked jurisdiction.
- Blocked → the warning defined in IN-01 displays immediately, and Region/ZIP fields are disabled per BR_2.6.1.9.
- Not blocked → no warning; user continues filling the form.
-
User checks all required compliance checkboxes and fills the remaining fields.
-
User clicks [Next]. Frontend calls POST /calculate-cart (Ref: BR_2.6.1.1):
- Input:
product_id,addon_ids,billing_country,billing_region,user_ip,user_id,promo_code(NULL at Step 5),zip_code(included only whenzip_requirements[billing_country] = true; omitted otherwise).
While this call is in flight, frontend displays the text "Calculating regional taxes..." directly below the State/Region dropdown, and [Next] is disabled (Ref: BR_2.6.1.2).
Backend processes, in order (5 steps):
Step Name Logic 1 Sanctions Gate Full request payload: product_id,addon_ids,billing_country,billing_region,zip_code(included only whenzip_requirements[billing_country] = true; omitted otherwise),user_ip,user_id,promo_code(NULL at Step 5).- Validatebilling_country+billing_regionagainstCompliance_geo_restrictions(same table/query logic as UC_2.1.1 §5 Step 3a).- If a match is found wherecountry = billing_countryAND (region = 'All'ORregion = billing_region) → return HTTP 403 immediately. See IN-01 for the exact user-facing message.- Steps 2–5 are skipped when this returns HTTP 403.2 Pricing Engine - If product_idisEVAL_L1/EVAL_L2/EVAL_L5: query Table J (Evaluation and Reset Pricing) for the base price.- IfGlobal_Var_Founder_Cohort_Open == TRUE(Table C), the backend queries the Founder Price column in Table J instead of the Standard Price column — this sets a lowerbase_pricedirectly (not a discount on top of Standard Price). See UC_2.1.5.- Returning user detection[CHR-38]: if the entered email matches an existing Users record withstatus IN ('Failed', 'Terminated'), the engine must additionally branch ondays_since_failurevs.Discount_Code_Duration_Days(Table C) andis_founder/is_professionalto selectlocked_reset_price/locked_rebuy_price/Retry_Discount-adjusted price instead of full Challenge Price — kept in sync with the Dashboard Failure Modal and Klaviyo email pricing. Not yet fully elaborated in this UC — full branch logic documented inReferences/CR/2026-08-02_CR38_returning-user-checkout-pricing/CR_summary.md(Ref: [CHR-38]) and in the Failure/Recovery research docs; needs a dedicated BR write-up here before dev handoff.- Otherwise, fetch prices forproduct_id+addon_ids.-promo_code= NULL at Step 5 →discount_amount = 0.- During the evaluation phase, alladdon_idsare $0 ("Covered by Stack Trading").- Math:Final_Amount = (base + addons) - discount_amount3 Location Check - If billing_countryis missing, query the Users table viauser_idto retrieve it.- Not applicable at Step 5 —billing_countryis always supplied by the user's own form input here.- This step only triggers for the authenticated Dashboard reuse of this endpoint (see BR_2.6.1.1).4 Tax Engine - Ping the third-partyQuaderno API with user_ip,billing_country,billing_region,zip_code(included only whenzip_requirements[billing_country] = true; omitted otherwise), and the running amount computed in Step 2 (base_price − discount_amount).- Quaderno checks whether the buyer's jurisdiction has crossed a tax threshold (e.g. EU VAT, APAC GST) or qualifies for a domestic exemption. If not,tax_amount = 0; otherwise Quaderno returns the applicabletax_amount.5 Return - Return { base_price, discount_amount, tax_amount, total_price }to the frontend, wheretotal_price = base_price − discount_amount + tax_amount.- At Step 5 this is saved silently to session state (see BR_2.6.1.5) — nothing is rendered.- At Step 6 the same shape populates the Order Summary. - Input:
-
If POST /calculate-cart returns HTTP 200: the "Calculating regional taxes..." text disappears. Triggers UC_2.6.2 Phase 2 (full PII UPSERT), then navigates to Step 6. No price/tax is shown at this step (see BR_2.6.1.5).
6. Exceptional Flow
-
[Selected Country or State/Region is a blocked jurisdiction]
- The warning defined in IN-01 displays immediately on selection.
- Placement rules (mutually exclusive):
region = 'All'matched (entire country blocked) → warning displays under the Country field; State/Region and ZIP fields are disabled (Ref: BR_2.6.1.9).region = [specific region]matched → warning displays under the State / Province / Region field; ZIP field is disabled (Ref: BR_2.6.1.9).
- [Next] remains disabled.
- Warning auto-clears, and disabled fields re-enable, the next time a valid (non-restricted) Country/State combination is selected.
-
[If POST /calculate-cart returns HTTP 5xx]
- Full-page error: FP-03. Full-page error screen replaces checkout UI.
7. Business Rules
BR_2.6.1.1: Sanctions Pre-Check & Tax Calculation Timing [CHR-47]
Two independent mechanisms — replaces the prior debounced-per-field-settle design entirely:
- Sanctions pre-check (immediate): on every Country selection AND every State/Region selection, the system immediately checks whether the selected value is a blocked jurisdiction (same restricted-jurisdiction data as the Sanctions Gate below).
- Blocked → the IN-01 warning displays immediately. State/Region and/or ZIP fields are disabled per placement rules — Ref: BR_2.6.1.9.
- Not blocked → no warning; fields stay enabled.
- Does not apply to Email or Confirm Email.
- Tax calculation call on [Next] click:
POST /calculate-cartfires only when the user clicks [Next] (Ref: §5 Basic Flow step 7) — not on any field'sonChange/onBlur. Email and Confirm Email field changes never trigger this call (Ref: BR_2.6.1.7, unchanged). While the call is in flight, "Calculating regional taxes..." displays and [Next] is disabled (Ref: BR_2.6.1.2). The Sanctions Gate inside this call (Ref: §5 Basic Flow step 7 table, Step 1) remains the authoritative server-side check.
BR_2.6.1.2: Next Button Gate
[Next] is disabled until ALL conditions are simultaneously met:
- All required fields have valid data.
- No unresolved Sanctions pre-check match is active (Ref: BR_2.6.1.1 mechanism 1) — the currently selected Country/State combination is not a blocked jurisdiction.
- All required compliance checkboxes are checked.
- Confirm Email is filled and matches the Email field (Ref: BR_2.6.1.7).
On click, if conditions 1–4 above are met, [Next] triggers POST /calculate-cart (Ref: BR_2.6.1.1 mechanism 2) and stays disabled while that call is in flight ("Calculating regional taxes..." showing). Only on HTTP 200 does navigation to Step 6 actually proceed (Ref: §5 Basic Flow step 8).
BR_2.6.1.3: Everflow Cookie Capture
Everflow first-party cookie & S2S Attribution Fallback:
On Step 5 page initialization, the frontend runs the following two paths in parallel — both are mandatory and always execute on every page load, regardless of SDK status:
- Path 1 — Everflow SDK: Frontend initializes the Everflow JavaScript SDK (installed via NPM). Using the SDK helper methods, frontend captures the Affiliate ID and Offer ID from the URL parameters, executes the click client-side, and stores the generated
transaction_idas a first-party cookie. At Step 6,everflow_idis read from this cookie viagetTransactionId()and appended as a hidden payload parameter toPOST /execute-checkout. - Path 2 — Direct affiliate cookie: Frontend parses the raw affiliate URL parameters (
affid,oid,ef_tok) directly from the browser URL and writes them into a first-party cookie namedst_affiliate_data. This write does not route through the Everflow SDK and is therefore not blocked by ad blockers.
Everflow SDK failure behavior — S2S Fallback:
If the Everflow SDK fails to load (e.g. blocked by an ad blocker or network error), the failure is silent — Step 5 rendering is NOT blocked. The following S2S fallback sequence applies:
- Frontend appends
everflow_sdk_blocked: true(boolean flag) to the hidden payload sent toPOST /execute-checkout. - Frontend passes the
st_affiliate_datacookie content toPOST /execute-checkoutalongside the other payload parameters. - Backend — On-the-fly Transaction Generation: If the Node.js middleware receives
everflow_id = nullbutst_affiliate_datais present, it invokes Everflow's Server-to-Server (S2S) Click API directly from the server to generate atransaction_idbefore processing the conversion postback.
BR_2.6.1.4: Data Persistence on Back Navigation
Step 5 data is preserved across back navigation (same general rule as BR_2.2.3). Reload/refresh behavior at Step 5 is governed centrally by BR_2.1.1.3 (Step 0) — data is kept, not cleared, as long as local storage cart state is present.
BR_2.6.1.5: No Price Display at Step 5
Step 5 does not render base_price, tax_amount, total_price, or any other line item from the POST /calculate-cart response. The response is held in session state only; the Order Summary is first rendered at Step 6.
BR_2.6.1.6: ZIP / Postal Code Dynamic Validation
The ZIP / Postal Code field is visible by default on Step 5 load. Its required/optional state and visibility after Country selection are driven entirely by zip_requirements, read from the global checkout state populated by UC_2.1.1 GET /system/status.
On Country onChange: requires_zip = true → field stays visible and is required; requires_zip = false → field is hidden from the UI.
Only a Country change re-evaluates this rule — a State/Region change does not. billing_address and zip_code are both stored in session state and forwarded to the payment gateway at POST /execute-checkout for AVS (Address Verification System) check; there is no secondary billing address support — only the address collected at Step 5 is used for payment verification.
BR_2.6.1.7: Confirm Email — Frontend-Only Match Validation [CHR-18]
The Confirm Email field has no database column and is never included in any API payload (POST /capture-lead, POST /execute-checkout, or any other endpoint).
Sequencing:
- Email field
onBlur→ runs format validation, then Company Email Domain Block (Ref: BR_2.6.1.10) — a blocked domain halts here (Ref: IN-11). Phase 0 (Blacklist Check + Duplicate Account Check, Ref: UC_2.6.2 §5 Phase 0 — Blacklist Check & Duplicate Account Check) only runs if this gate is passed. Inline error displays immediately if any gate fails. - Confirm Email field
onBlur→ validates format, checks Company Email Domain Block (Ref: BR_2.6.1.10), then compares against Email:- 2.1. Match: No error. If Phase 0 has already passed for the current Email value, Phase 1 (
POST /capture-lead) fires. - 2.2. Invalid format: Ref: IN-07. Phase 1 does not fire.
- 2.3. Blocked domain: Ref: IN-11. Phase 1 does not fire. Checked independently of the Email field's own result — Ref: BR_2.6.1.10.
- 2.4. Valid format, not blocked, but mismatch: Ref: IN-05. Phase 1 does not fire.
- 2.1. Match: No error. If Phase 0 has already passed for the current Email value, Phase 1 (
POST /capture-leadfires only when Phase 0 has passed AND Confirm Email matches (step 2.1).
Editing Email after Confirm Email already matched it:
Confirm Email must be re-validated against the new Email value:
- If it no longer matches → Ref: IN-05 renders under Confirm Email: "Emails do not match" (same as step 2.3).
- If it still matches → since the Email value changed, Email must go through Phase 0 again (Ref: UC_2.6.2 §5 Phase 0 — Blacklist Check & Duplicate Account Check):
- New Email matches an existing DB record → follow the Duplicate Account Check matrix (Ref: UC_2.6.2 §5, Duplicate Record table) — e.g. UPSERT onto the existing Guest record where applicable, or block per the matrix.
- New Email has no existing record → proceed with the normal Phase 0 → Phase 1 flow (new Guest record).
Re-matching Confirm Email against an Email value that has NOT changed does not re-fire POST /capture-lead — Phase 1 only fires again when the Email value itself has actually changed.
BR_2.6.1.8: Invalid Field Data Display on Reload / Navigation
Applies to the 4 free-text fields at Step 5 with a format or length rule: Full Name, Email, Billing Address, ZIP / Postal Code.
At the moment checkout state is re-rendered — full page refresh (F5, per BR_2.1.1.3 Table 1), backward navigation ([Back]), or forward navigation ([Next]) — if a field holds previously-entered data that fails its format/length rule, the invalid data is displayed with an inline error message and red border.
Explicit exclusion — NULL/empty fields: if a field is NULL or empty (never filled, or cleared by the user before leaving the step), this rule does not apply and no error is shown automatically on reload/navigation.
BR_2.6.1.9: Field Disable on Sanctions Pre-Check Match
When the Sanctions pre-check (Ref: BR_2.6.1.1 mechanism 1) matches the selected Country or State/Region against a blocked jurisdiction:
- Country match (
region = 'All'): State/Province/Region field (if visible) AND ZIP / Postal Code field (if visible) both become disabled. - State/Region match (
region = [specific region]): ZIP / Postal Code field (if visible) becomes disabled. State/Region itself stays enabled (user may pick a different region).
Fields re-enable automatically the moment the user changes the Country (or Region, for the region-match case) to a value that is not a blocked jurisdiction. Disabling does not clear any value the user had already entered into ZIP / Region — it is restored (still visible, greyed out) if the user re-selects the same blocked value again.
BR_2.6.1.10: Company Email Domain Block (Email & Confirm Email)
Domain check: value's domain is @stacktrading.com → Block.
Email field onBlur — check order:
- Format validation (Ref: CR-09 §9.1) — fails here, halts.
- Company Email Domain Block: If domain is
@stacktrading.com→ HALT. Phase 0 (Blacklist Check / Duplicate Account Check, Ref: UC_2.6.2 §5) only runs if this gate is passed.
Confirm Email field onBlur — check order (extends BR_2.6.1.7):
- Format validation (IN-07 on failure).
- Company Email Domain Block: If domain is
@stacktrading.com→ HALT (IN-11). Match comparison against Email only runs if this gate is passed. Checked independently of the Email field's own result — covers the case where a user populates Confirm Email (paste/autofill) before the Email field has ever been blurred.
On block (either field): Inline error IN-11 renders beneath the field that triggered it. [Next] stays disabled — hard stop, same pattern as IN-02/IN-03, Ref: BR_2.6.1.2 condition 1. No Guest row is written for a blocked Email value.
8. Screen Description
| No. | Field Name | Field Type | Validation Rule / Behaviour |
|---|---|---|---|
| 1 | Full Name | Textbox | Display rule:- Placeholder: "Enter full name".Validation:- Required. Ref: CR-02.Behaviour:- Ref: CR-02. DB column is full_name- Invalid on reload/navigation: Ref: BR_2.6.1.8. |
| 2 | Textbox | Display rule:- Placeholder: "Enter email".Validation:- Required. Ref: CR-09 §9.1.Behaviour:- On onBlur (field loses focus after user finishes typing) → runs format validation, then Company Email Domain Block (Ref: BR_2.6.1.10), then — only if the domain is not blocked — triggers UC_2.6.2 Phase 0 (Blacklist Check + Duplicate Account Check). No visible UI change on pass. Fires independently of Country/State/checkbox state. Ref: UC_2.6.2.- Invalid on reload/navigation: Ref: BR_2.6.1.8. | |
| 2b | Confirm Email | Textbox | Display rule:- Placeholder: "Enter confirm email".Validation:- Required. Must be a valid email format (Ref: CR-09 §9.1).- Must not be a blocked company domain (Ref: BR_2.6.1.10).- Must match the Email field (Row 2) exactly.Behaviour:- Ref: BR_2.6.1.7, BR_2.6.1.10. |
| 3 | Billing Address | Textbox | Display rule:- Placeholder: "Enter billing address".Validation:- Required. Ref: CR-02.Behaviour:- Not used as input to the Sanctions Gate check (see BR_2.6.1.1 — only Country + State/Region are evaluated).- Accepted even when the billing address is in a different country from the Cloudflare-detected geo country.- Invalid on reload/navigation: Ref: BR_2.6.1.8. |
| 3b | ZIP / Postal Code | Textbox | Display rule:- Placeholder: "Enter ZIP / postal code".- Visible by default on Step 5 load (alongside Country and State/Region). On Country onChange, frontend reads zip_requirements from global checkout state (loaded via GET /system/status, see UC_2.1.1). requires_zip = false → Hide the field from the UI; requires_zip = true → Field stays visible.Validation:- requires_zip = true → Field is required. Max 20 characters (Ref: CR-02).Behaviour:- Disabled when the selected Country/Region is a blocked jurisdiction. Ref: BR_2.6.1.9.- Not tied to any field-triggered API call — the tax calculation call fires only once, on [Next] click. Ref: BR_2.6.1.1 mechanism 2, §5 Basic Flow step 7.- Accepts alphanumeric characters and spaces. No integer-only validation.- Stored in session state alongside billing_address. Passed to POST /execute-checkout payload for AVS check at the NMI gateway. Ref: BR_2.6.1.6.- Invalid on reload/navigation: Ref: BR_2.6.1.8. |
| 4 | Country | Dropdown (Single-selection) | Display rule:- Placeholder: "Select country".Validation:- Required. Ref: CR-03.Behaviour:- On selection → clears the State / Province / Region field selection (if any) and repopulates its option list with the new Country's subdivisions. If the selected country has no subdivisions → hide the State/Region field entirely. Also re-evaluates zip_requirements[billing_country]: false → hide ZIP / Postal Code field (Row 3b); true → ZIP stays visible and required. A subsequent State/Region change does NOT re-evaluate ZIP visibility.- On selection → immediately checks whether the selected Country is a blocked jurisdiction. Blocked (region = 'All') → IN-01 displays under this field immediately; State/Region and ZIP become disabled (Ref: BR_2.6.1.9). Ref: BR_2.6.1.1 mechanism 1.- Does NOT call POST /calculate-cart by itself — that call fires only once, on [Next] click (Ref: BR_2.6.1.1 mechanism 2). |
| 5 | State / Province / Region | Dropdown (Single-selection) | Display rule:- Placeholder: "Select state / region".- Visible by default on Step 5 load. On Country selection: if the selected country has no subdivisions → hide this field entirely; otherwise repopulate dropdown options and keep visible.Validation:- Required when visible. Ref: CR-03.Behaviour:- On selection → immediately checks whether the selected Region is a blocked jurisdiction. Blocked → IN-01 displays under this field immediately; ZIP becomes disabled (Ref: BR_2.6.1.9). Ref: BR_2.6.1.1 mechanism 1.- Does NOT call POST /calculate-cart by itself — that call fires only once, on [Next] click (Ref: BR_2.6.1.1 mechanism 2). |
| 6 | Shirt size | Dropdown (Single-selection) | Display rule:- Placeholder: "Select shirt size".- Options: S, M, L, XL, XXL.Validation:- Required. Ref: CR-03. |
| 7 | Compliance Checkbox 1 — Commercial Acknowledgment | Checkbox | Display rule:- Default: unchecked.- Label: Ref: UC_2.1.2 §4 Checkbox 1Validation:- Ref: BR_2.6.1.2. Mandatory — [Next] disabled if unchecked. |
| 8 | Compliance Checkbox 2 — Age & ToS | Checkbox | Display rule:- Default: unchecked.- Label: Ref: UC_2.1.2 §4 Checkbox 2Validation:- Ref: BR_2.6.1.2. Mandatory — [Next] disabled if unchecked. |
| 9 | Compliance Checkbox 3 — EU Waiver (Flow C only) | Checkbox | Display rule:- Rendered only if required_flow == 'FLOW_C'. Default: unchecked.- Label: Ref: UC_2.1.2 §6 Flow CValidation:- Ref: BR_2.6.1.2. Mandatory when visible — [Next] disabled if unchecked. |
| 10 | Flow B Disclosure Text (Flow B only) | Label | Display rule:- Rendered only if required_flow == 'FLOW_B'. Static, non-interactive.- Text: Ref: UC_2.1.2 §5 Flow B per is_launch_phase. |
| 11 | Flow C Disclosure Text (Flow C only) | Label | Display rule:- Rendered only if required_flow == 'FLOW_C'. Static, non-interactive.- Text: Ref: UC_2.1.2 §6 Flow C per is_launch_phase. |
| 12 | Flow E UAE Disclaimer (Flow E only) | Label | Display rule:- Rendered only if required_flow == 'FLOW_E'. Static, non-interactive.- Text: Ref: UC_2.1.2 §8 Flow E. |
| 13 | Hypothetical Performance Disclaimer | Label | Display rule:- All flows. Static block, non-collapsible.- Text: Ref: UC_2.1.2 §9 Hypothetical Disclaimer. |
| 14 | Sanctions Gate Warning (conditional) | Label | Display rule:- Rendered when the Sanctions pre-check matches (Ref: BR_2.6.1.1 mechanism 1) or, in the rare defense-in-depth case, when POST /calculate-cart's Sanctions Gate matches on [Next] click. Inline red warning.- Text: Ref: IN-01. |
| 15 | [Back] | Button (Secondary) | Behaviour:On click → navigates back to Step 4 (Futures) or Step 3 (Forex — Step 4 does not exist in the Forex flow). |
| 16 | [Next] | Button (Primary) | Display rule:- Disabled until ALL conditions in BR_2.6.1.2 are met.Behaviour:- On click when enabled → triggers UC_2.6.2 Phase 2 (full-PII UPSERT into the existing Guest record created at Email onBlur — no new Guest record is created here). Navigates to Step 6. |
UC_2.6.2 — Lead Capture & Cart Abandonment (Three-Phase)
1. Overview
| Field | Content |
|---|---|
| ID | UC_2.6.2 |
| Use Case | Step 5: Lead Capture & Cart Abandonment |
| Description | Three sequential phases write to the same Guest record.Phase 0 fires on Email field onBlur: runs Blacklist Check and Duplicate Account Check. If either gate blocks, the flow stops with an inline warning.Phase 1 fires only after both Phase 0 gates pass: captures a minimal lead via POST /capture-lead and fires a Cart_Abandonment webhook to Zapier/Klaviyo. Phase 2 fires on [Next] click: the same record is UPSERTed with the full PII collected across Step 5, and the user proceeds to Step 6. |
| Zapier Flow | Cart_Abandonment (outbound webhook event → Zapier bus → Klaviyo), fired at Phase 1 only. |
| Zapier Table | — |
| 3rd Party | Klaviyo (lifecycle email nurture — triggered via Zapier) |
2. Trigger
- Phase 0: Email field
onBlurat Step 5 — runs Blacklist Check and Duplicate Account Check. If either gate blocks → flow stops (see Phase 0 in §5 Basic Flow). - Phase 1: Both Phase 0 gates pass, AND Confirm Email is filled, valid, and matches Email (Ref: BR_2.6.1.7) →
POST /capture-leadfires async (background, no UI change). - Phase 2: User clicks [Next] at Step 5 with all BR_2.6.1.2 conditions satisfied.
3. Pre-conditions
- Phase 0: Email field contains a value in valid email format.
- Phase 1: Both Phase 0 gates (Blacklist Check and Duplicate Account Check) have passed, AND Confirm Email is filled, valid, and matches Email.
- Phase 2: All BR_2.6.1.2 (Next Button Gate) conditions are met.
4. Post-conditions
- Phase 0 — blocked (Company Email Domain Block, Blacklist, or Duplicate Account Check): No Users row is written. [Next] is disabled and an inline warning is shown beneath the Email field (or Confirm Email field, if the block is on that field's own domain check — Ref: BR_2.6.1.10) (see Phase 0 in §5 Basic Flow).
- Phase 1 — succeeded: A row in the Users table with
status = 'Guest'exists for this email — newly created on the user's first-ever visit, or updated in place if a Guest row for this email already existed (e.g. re-entry, see BR_2.6.2.3). Cart_Abandonment webhook fired to Zapier automation bus. - Phase 2: The same Guest row is updated with full PII and session data.
user_idreturned and stored in session state. User navigated to Step 6.
5. Basic Flow
Confirm Email Gate — Confirm Email onBlur:
- Check Confirm Email: On Confirm Email field
onBlur:- 3a. Invalid format: If the value is not a valid email format → display Ref: IN-07 ("Please enter a valid email address") and HALT.
- 3b. Mismatch: If the value is a valid format but does not match the Email field → display Ref: IN-05 ("Emails do not match") and HALT. (see BR_2.6.1.7).
Phase 1 Trigger:
- Capture Lead: Once Email has passed gates (1) and (2), AND Confirm Email has passed gate (3), silently call
POST /capture-leadin the background (Phase 1). This fires from whichever field'sonBlurcompletes the full condition set last.
Phase 0 — Blacklist Check & Duplicate Account Check:
Company Email Domain Block runs first, immediately after format validation passes on Email onBlur (Ref: BR_2.6.1.10). If the email's domain is @stacktrading.com → HALT immediately — no Users row is written, [Next] is disabled, inline warning IN-11 renders beneath the Email field. Blacklist Check and Duplicate Account Check do not run.
The two checks below run on Email onBlur only when the domain is not blocked, and act as the primary gate before any capture-lead call fires.
Blacklist Check runs next. Query the blacklist table (schema below) WHERE email = payload.email AND is_active = TRUE. A match HALTs the flow — no Users row is written, [Next] is disabled, and inline warning Ref: IN-02 renders beneath the Email field. [CHR-17]
blacklist Table Schema:
| Column | Type | Notes |
|---|---|---|
id | SERIAL PRIMARY KEY | |
email | VARCHAR | Indexed for fast lookup at Step 5 onBlur. |
reason | TEXT | Internal note, never shown to user. |
added_by | VARCHAR | Admin/ops identifier. |
added_at | TIMESTAMP | |
is_active | BOOLEAN | Soft-delete flag; onlyTRUE rows block. |
Duplicate Account Check runs only after the Blacklist Check clears, and evaluates the existing Users record (if any) for this email against the four branches below:
| Existing record state | Outcome |
|---|---|
status = 'Active_SIM' OR status = 'Active_DMA' | HALT. [Next] disabled. Inline warning Ref:IN-03. |
status = 'Guest' AND provider_event_id IS NOT NULL (paid, not yet claimed — covers both provisioning "still in-progress" and "failed") | HALT. [Next] disabled. Inline warning Ref:IN-04. |
status = 'Failed' OR status = 'Terminated' | Allow. Proceeds to the normal UPSERT (Phase 1) exactly like a fresh entrant. |
status = 'Guest' AND provider_event_id IS NULL (never paid) | Allow. Proceeds to the normal UPSERT (Phase 1). |
If no existing record matches at all, the check also allows the flow through (first-ever visit with this email).
Phase 1 — Capture Lead (Confirm Email onBlur, once all gates pass):
-
Once Phase 0 gates (1)–(2) and the Confirm Email Gate (3) have all passed → proceed to Phase 1 (Capture Lead).
-
Frontend calls
POST /capture-lead(fires async, in the background — no spinner, no UI change while pending), with input:email(required),full_name(optional),utm_source(optional — read fromlocalStorageper CR-12 / BR_2.1.1.4; not re-parsed from the URL here),abandoned_step(optional).abandoned_step: Records the furthest step the user has ever reached for this row — monotonic, never regresses on re-entry. See BR_2.6.2.10. All possible values the backend can insert:
Value Trigger point "Step 5: PII & Details"Email onBlurat Step 5"Step 6: Payment"User clicks[Next] from Step 5 → arrives at Step 6 (Checkout & Payment) "Step 7: Order Processing"User clicks[Complete Purchase] on Step 6 → payment submitted "Step 7: Claim Account"User reaches the Provisioning Gate (Claim Account — Phase 2 of Step 7) "Completed"POST /claim-account returns session token successfully (Step 7 Phase 2 complete) [CHR-14]
'Completed'state — Klaviyo Kill Signal: WhenPOST /claim-accountreturns HTTP 200, the backend setsabandoned_step = 'Completed'and fires a completion event to the Zapier automation bus to cancel the abandonment nurture sequence for this user.[CHR-14] -
UPSERT (keyed by email) on the core Users table, runs server-side inside
POST /capture-lead:- First-ever visit with this email → no existing row matches → backend INSERTs a new row with
status = 'Guest', using theabandoned_stepvalue passed up in the request. - Re-entry with an email already captured before (see BR_2.6.2.3) → an existing Guest row matches → backend UPDATEs that same row instead of creating a duplicate (
abandoned_stepfollows the monotonic rule — see BR_2.6.2.10). - There is no separate "Capture Lead" table — this always writes to the same Users row (see BR_2.6.2.5).
- "Pending Order" is only the conceptual label ActiveCampaign/Klaviyo uses to track this record; it is not a DB status value.
- Backend fires outbound webhook event
Cart_Abandonment→ Zapier automation bus.
- First-ever visit with this email → no existing row matches → backend INSERTs a new row with
-
Zapier triggers a Klaviyo lifecycle email nurture campaign (see BR_2.6.2.7 for scope boundary).
-
Backend returns HTTP 200 OK. No visible UI change — no spinner, no banner. Public endpoint, no Auth required.
Phase 2 — Full PII UPSERT (Next click):
- User clicks [Next] at Step 5 (all BR_2.6.1.2 conditions met).
- System performs an UPSERT on the same Guest record keyed by email:
status = 'Guest'- All PII fields (name, email, billing address, zip_code, country, region, shirt size).
- System returns
user_id→ stored in session state. (transaction_iddoes not exist yet at this point — it is only generated later, at Step 6 checkout/payment execution) - Frontend navigates user to Step 6.
5a. Exceptional Flow
- [Phase 1 — POST /capture-lead returns HTTP 5xx or times out]
- Silent failure — no error UI shown (background lead-capture call, not a blocking step). Does not disable or delay [Next]; Phase 2 still runs its own UPSERT independently.
- [Phase 0 — Company Email Domain Block, Blacklist Check, or Duplicate Account Check blocks the user]
- [Next] stays disabled until the user changes the blocked field (Email and/or Confirm Email) to a value that clears all checks.
- [User closes the browser or navigates away before clicking [Next], after Phase 0 & Phase 1 already ran]
- See BR_2.6.2.6 / BR_2.1.1.3 — no login gate exists for this; the existing Guest record is simply updated (UPSERT) the next time the user re-enters an email on Step 5.
6. Business Rules
BR_2.6.2.1: Trigger — Three Sequential Phases
Phase 0 (Gates) is triggered by Email field onBlur. Phase 1 (Guest record creation/update + Cart_Abandonment webhook) requires BOTH Phase 0 to have passed AND Confirm Email to have been filled, validated, and matched against Email — it is not triggered by [Next]. See BR_2.6.1.7 for the full Confirm Email sequencing and the re-fire rule when Email is edited after a match.
Phase 2 ([Next] click) performs an additional UPSERT of full PII onto the same record and does not re-fire the Cart_Abandonment webhook.
Edge case — [Next] clicked without the Email or Confirm Email field ever losing focus (e.g. users fill in all other fields first, then click "Next" without blurring either field): Phase 0 and Phase 1 have not run, so no Guest row exists yet for this email. Frontend must run Phase 0 gates, the Confirm Email match check, and Phase 1 (§5) first — synchronously, before Phase 2's UPSERT — so the Cart_Abandonment webhook still fires and the row exists before Phase 2 updates it.
BR_2.6.2.2: Email onBlur — Debounce
Frontend starts a 500ms debounce timer on onBlur. If the user refocuses the email field within 500ms (e.g. to fix a typo) and blurs again, the pending call is cancelled and the timer resets. POST /capture-lead only fires after the email field has been blurred and remained unfocused for the full 500ms.
BR_2.6.2.3: Email onBlur — Change-Detection
POST /capture-lead only fires if the email that just passed the debounce timer is different from the last email successfully submitted. If the user blurs the same email again (e.g. accidentally refocuses and blurs without changing the value), no duplicate API call is made.
BR_2.6.2.4: Email Change — Old Guest Row Handling
When a user changes their email address and the new email passes both the debounce timer (BR_2.6.2.2) and the change-detection check (BR_2.6.2.3), a new Guest row is created for the new email. The old email’s Guest row is not deleted immediately — it is cleaned up by the existing 30-day cron job defined in BR_2.6.2.8.
BR_2.6.2.5: Guest Record Schema — No Separate Table
There is no dedicated “Capture Lead” table. The email is written directly to the core Users (PostgreSQL) table with status = 'Guest'. The full Users table schema (all columns) is documented at Step 7.
BR_2.6.2.6: Re-entry Handling — No Login Gate
There is no Auth0 account for a Guest record at any point before Step 7 is completed, so a “login” cannot occur for these users. Re-entry (whether via reload, back navigation, or a deep-link) is instead governed centrally by BR_2.1.1.3 (Step 0).
BR_2.6.2.7: Klaviyo Scope Boundary
Sotatek’s responsibility ends at firing the Cart_Abandonment webhook (Phase 1) to the Zapier automation bus. Klaviyo email campaign is orchestrated by Zapier — out of scope for Sotatek implementation.
BR_2.6.2.8: Guest Record Auto-Delete [CHR-19]
A background cron job runs periodically and hard-deletes records matching BOTH conditions:
status = 'Guest'created_at < NOW() - INTERVAL '30 days'
Note: Deletion is based on
created_at(original record creation date), notupdated_at. A record that was created 30 days ago and still has not converted is deleted — regardless of any subsequent updates to it.
BR_2.6.2.10: abandoned_step — Monotonic Progression Guard
abandoned_step must always reflect the furthest step the user has ever reached for that row, never the current/latest one. On every write (Phase 1 UPSERT or any later re-entry UPDATE), the backend compares the incoming step against the stored value using a fixed ordinal rank and only overwrites if the incoming rank is strictly greater:
| Rank | Value |
|---|---|
| 1 | Step 5: PII & Details |
| 2 | Step 6: Payment |
| 3 | Step 7: Order Processing |
| 3 | Step 7: Claim Account |
| 4 | Completed (terminal — see BR_2.6.2.7 for Klaviyo kill signal) |
Effectively abandoned_step = GREATEST(rank(stored), rank(incoming)), resolved back to the value name. Example: user reaches Step 7: Order Processing then reloads and lands back on Step 5 → abandoned_step stays Step 7: Order Processing; it does not regress to Step 5: PII & Details.
Source: Client instruction (chat), 2026-07-30.
Changelog
| Date | Version | Updated Item | Before | After | Notes |
| 2026-09-03 | v20 | UC_2.3 §5 "Evaluation Target / Stop" row + §7 Screen Description Row 1 (Evaluation Requirements box, Row 2 text) | Labeled "14% Target / 7.5% Stop (Static)" with Source "Fixed (no Zapier table to config)" — both percentages hardcoded as static literals in the UI copy. | Corrected to (Dynamic) — both percentages are Ops-editable Zapier Table C fields: SIM_target_percent (default 14%) and SIM_stop_percent (default 7.5%). Source column updated to Table C (SIM_target_percent / SIM_stop_percent) [CHR-49]. 60 days remains a separate fixed literal (Today + 60 Days, no Table C field — Ref: Zapier Integration V7.pdf, Flow 3A Step "Store expiration_date"), called out explicitly so it isn't conflated with the two dynamic percentages. §7 Row 2 UI copy changed from a static string literal to a dynamically-rendered template. | User audit finding, 2026-09-03. Both SIM_stop_percent and SIM_target_percent confirmed under [CHR-49] — per official CHR mapping table docs/BA/Impact_matrix_CR/CHR_Tag_Impact_Matrix_20260821_v1.md L156: CR-20260820-001 \| CHR-49 \| SIM stop/target percent. CR_summary.md's own "Nội dung thay đổi cụ thể" section only details the stop-percent formula rewrite (scoped to UC_4.10.1/UC_4.10.2, which have no Profit Target) and its Open Questions line notes SIM_target_percent wasn't raised as a formula change for UC_4.8.1/UC_4.10.4 specifically — that is a scope note for those UCs, not a statement that SIM_target_percent sits outside CHR-49. The raw CR-index CSV row (line 427-437, client Adrian, Slack thread 2026-08-14) requests both fields as one combined Table C addition, and UC_5.1.md v2.2 (2026-08-24) already dynamizes SIM_target_percent off the same Table C field — that doc should also be tagged [CHR-49] (currently cites only "Client confirmed 2026-08-24", no CHR tag; flagged for a follow-up CHR-tag pass). |
| 2026-09-03 | v21 | UC_2.3 §5 "Eval Requirement — Futures" / "Eval Requirement — Forex" rows | Source column "Fixed (no Zapier table to config)" — implied these SIM-side notional/contract values were hardcoded UI literals. | Source column corrected to Table B (Futures Configuration Matrix, Max_Contracts_ES_Equiv) and Table A (Forex Configuration Matrix, Max_Forex_Notional) respectively — same fields already cited for the "Live Capital Allocation — Futures/Forex" rows below, since SIM Eval Requirement and Live Capital Allocation read the identical Table A/B values for Levels 1/2/5 (only the SIM vs Live label differs, not the underlying data source). | User request, 2026-09-03. Consistent with the v19 fix already applied to the "Live Capital Allocation" rows (Source: Zapier Integration V7.pdf, Appendix A "Forex Configuration Matrix" / Appendix B "Futures Configuration Matrix"). |
| 2026-08-24 | v19 | BR_2.1.1.3 — Session Recovery Matrix (Table 1/2/3), Step 6 row — F5 reload during payment processing no longer renders OV-05 | Step 6 reload evaluated a "payment in progress" condition: if true → OV-05 processing overlay rendered, then routed to OV-06 (success) or raw decline banner (failure). | F5 during payment processing routes directly to the final state: if payment already succeeded → frontend routes immediately to Step 7 (Claim Account); if payment failed / no payment attempted → Step 6 split-panel renders normally (email-lock check still applies). No OV-05 is re-rendered on reload. Rationale: payment is a single continuous backend flow with no resumable mid-state; backend idempotency enforced viaprovider_event_id; NMI transactions are normally < 30 s. Table 2 and Table 3 cross-references updated from "payment-in-progress check" to "direct-routing logic". | Client directive (chat), 2026-08-24. Companion change:UC_2.7-2.8_v1.md v4.0 (UC_2.7.1 §3/§5 updated). |
|---|---|---|---|---|---|
| 2026-08-20 | v18 | UC_2.6.2 §5 Phase 2 (step 7) — "Session data" bullet removed;everflow_click_id/everflow_transaction_id renamed | Phase 2 UPSERT step 7 listed a 3rd bullet: "Session data:asset_class, product_id, platform, addon_ids[], UTM parameters..., the calculate-cart result held from Step 5" — not traceable to any source doc; source (RFQ V7/Zapier V7) instead shows these fields flowing through POST /execute-checkout (Step 6) and Flow 1 Step 4 (post-payment), not the Step 5 UPSERT. Also, field name inconsistently appeared as everflow_transaction_id in this doc vs everflow_click_id in UC_2.7-2.8_v1.md. | Bullet deleted (audit finding — BA session, 2026-08-20); no source backs this claim. Field renamed toeverflow_id across this doc and UC_2.7-2.8_v1.md for consistency — Ref: QnA STAGE1-061 confirms the underlying rename from impact_click_id, though QnA's exact new name is everflow_transaction_id; everflow_id was chosen instead per user directive, 2026-08-20 — flagged as a deviation from QnA wording. | |
| 2026-08-15 | v17 | BR_2.1.1.3 — Session Recovery Matrix (Table 1), Step 7.1 (Claim Account) row | No mention of what happens if the background SIM provisioning (POST /provision-sim-user) fails while the user is still on Step 7.1, entering their phone number — gap flagged during OV-07/BN-06 consolidation. | Added: if SIM provisioning fails at this point, no indication is shown on Step 7.1 — cross-referenced to newBR_2.8.3.5 in UC_2.7-2.8_v1.md (failure stays silent until surfaced by OV-07 on the Phase 3 interstitial). | Client directive (chat), 2026-08-15. Companion change:UC_2.7-2.8_v1.md v3.4 (new BR_2.8.3.5). |
| 2026-07-25 | v1 | Initial document | — | — | First version |
| 2026-07-26 | v1 | BR_2.6.1.3 — Everflow SDK failure behavior | Fail-open: attribution lost khi SDK bị chặn | S2S Attribution Fallback:everflow_sdk_blocked flag + st_affiliate_data cookie + backend S2S Click API on-the-fly transaction generation | STAGE1-068 (Adrian Stack, Google Docs, 2026-07-26) |
| 2026-07-26 | v1 | Zapier Table J — tất cả pricing rows | Giá cũ (Advanced $4,500/$3,599; Accelerated $1,250/$1,049; Associate $650/$499) + Reset/Extension fees cũ | Giá mới (Advanced $7,000/$5,599; Accelerated $1,250/$1,049; Associate $650/$499) + Reset/Extension fees mới theo data khách cung cấp 2026-07-26 | UC_2.1.5 Table J + UC_2.3 §5 |
| 2026-07-26 | v1 | BR_2.2.3 — Progress bar step count | Chưa nêu rõ số step (7 vs 6) | Thêm: Futures=7 steps, Forex=6 steps; áp dụng cả initial selection và back navigation | UC_2.2 §6 |
| 2026-07-26 | v1 | abandoned_step — Completed state & Klaviyo kill signal | Thiếu"Completed" state và Klaviyo kill signal | Thêm"Completed" vào abandoned_step table + note inline trong Phase 1 §5 (gộp vào flow, không tách BR) | Adrian Stack (Google Docs, 2026-07-26) |
| 2026-07-27 | v2 | UC_2.1.1 — ZIP requirements map + table schema | GET /system/status response schema had no ZIP-related field | Addedzip_requirements object to response payload, Field Notes row, and new country_zip_requirements Table Schema | [CHR-6]; impact analysis |
| 2026-08-14 | v13 | UC_2.6.1 §5/§6/§7/§8 — Sanctions pre-check + single calculate-cart call on [Next] | POST /calculate-cart fired on a shared debounce across Country/State/ZIP, and only that call surfaced the 403 Sanctions Gate error — user had to fill all fields before seeing the block. BR_2.6.1.8 did not exclude NULL fields from the reload invalid-data display. | Added Sanctions pre-check on Country/State selection (immediate, before any API call) with new BR_2.6.1.9 (field disable on match). POST /calculate-cart now fires exactly once, on [Next] click, dropping the debounce entirely (rewrote BR_2.6.1.1, BR_2.6.1.2, §5 Basic Flow, §6 Exceptional Flow, §8 Screen Description Country/State/ZIP rows). BR_2.6.1.8 now explicitly excludes NULL/empty fields from the reload auto-error. | BA directive, 2026-08-14. |
| 2026-07-27 | v2 | UC_2.6.1 — ZIP / Postal Code field | No ZIP field at Step 5; debounce logic covered only Country + State/Region | Added Screen Description Row 3b (ZIP / Postal Code); updated Country/State rows; rewrote BR_2.6.1.1 (3-field debounce); added BR_2.6.1.6 (ZIP dynamic validation) | [CHR-6]; impact analysis |
| 2026-07-27 | v2 | UC_2.6.2 — Blacklist & Duplicate Account Check moved to Step 5 | Both checks previously ran post-payment only, in Zapier Flow 1 (UC_2.8.2) | Added Step A (Blacklist Check) and Step B (Duplicate Account Check, 3-branch) to Phase 1 Basic Flow, ahead of the capture-lead UPSERT; relocatedblacklist Table Schema here; added BR_2.6.2.9; Phase 2 UPSERT payload now includes zip_code | Impact analysis |
| 2026-07-27 | v2 | BR_2.6.2.9 — Duplicate Account Check branch table | 3-branch table with generic "Inline warning beneath the Email field" text, no message copy, noFailed/Terminated case | Expanded to 4 branches: addedFailed/Terminated → Allow (no-op at Step 5; archive/reset stays strictly post-payment per STAGE1-086 — moving it to Step 5 would risk wiping a user's history before payment clears if they abandon cart); replaced generic text with exact copy + refs to new IN-02/IN-03/IN-04; Step A/B narrative in §5 updated to match | BA feedback item 7 (2026-07-27); STAGE1-083/STAGE1-084/STAGE1-086 |
| 2026-07-28 | v3 | UC_2.1.1 §Step 3f —cached_pass_rate calculation & source | Formula:COUNT(user_id WHERE status='Passed') / COUNT(total_challenges), refreshed by custom Node.js cron @ 00:00 UTC; no Firm_Sim_Funnel_Snapshots schema documented in this doc | Formula:SUM(sims_passed) / (SUM(sims_passed)+SUM(sims_failed)) from Firm_Sim_Funnel_Snapshots (append-only); refreshed via Zapier Flow 29 → POST /financial/daily-snap @ 5:05 PM EST — no custom cron job needed; added Firm_Sim_Funnel_Snapshots Table Schema | [CHR-16]; client correction of RFQ V7 §3.3 |
| 2026-07-29 | v4 | UC_2.1.1 (new BR_2.1.1.4), UC_2.6.1 (BR_2.6.1.3), UC_2.1.3 | UTM captured only at Step 5 page init (BR_2.6.1.3), first-seen-wins anchored there; Waitlist Gate (UC_2.1.3) = ActiveCampaign-only direct call, Klaviyo fully out-of-scope | UTM now captured globally at Step 0 (new BR_2.1.1.4), applying to every branch incl. the Waitlist Gate; first-seen-wins re-anchored to Step 0; BR_2.6.1.3 renamed to "Everflow Cookie Capture" (UTM sub-section removed, cross-refs BR_2.1.1.4); Waitlist Gate (UC_2.1.3) now fires direct parallel API calls to BOTH ActiveCampaign + Klaviyo with email+UTM — scope boundary rewritten (ActiveCampaign = CRM/identity only, no emails; Klaviyo Waitlist_Welcome drip = out-of-system once the API call fires); POST /capture-lead utm_source cross-referenced to session-state value from BR_2.1.1.4 | Client clarification, 2026-07-29 (Waitlist/Newsletter direct dual-call architecture + UTM capture timing) |
| 2026-07-29 | v5 | UC_2.6.1 §8 (Row 2 Email, new Row 2b Confirm Email), BR_2.6.1.2, new BR_2.6.1.7 | EmailonBlur triggered Phase 1 directly; no Confirm Email field; [Next] gate had 4 conditions | EmailonBlur now triggers Phase 0 only; added Row 2b Confirm Email (frontend-only match validation); [Next] gate now has 5 conditions (added Confirm Email match); added BR_2.6.1.7 defining Confirm Email sequencing and re-validation rule on Email edit | CR-20260729-001; Client (chat), 2026-07-29 |
| 2026-07-29 | v5 | UC_2.6.2 §2/§3/§5/§6 (BR_2.6.2.1) | Phase 1 (POST /capture-lead) fired once both Phase 0 gates passed | Phase 1 now also requires Confirm Email filled/valid/matched; §5 Basic Flow split into Phase 0 — Blacklist Check & Duplicate Account Check + Confirm Email Gate + Phase 1 Trigger; BR_2.6.2.1 cross-refs BR_2.6.1.7 | CR-20260729-001; Client (chat), 2026-07-29 |
| 2026-07-29 | v5 | BR_2.1.1.3 — Session Recovery Matrix (Table 1, Table 2, Table 3), Step 6 rows | Step 6 rows on reload/deep-link/back-from-marketing did not account for an in-progress payment | Added payment-in-progress check on Step 6 mount: if in progress → OV-05 loading → OV-06 success (Step 7) or failure banner; if not, normal layout | CR-20260729-002; Client (chat), 2026-07-29 |
| 2026-07-30 | v6 | BR_2.1.1.3 — Session Recovery Matrix (Table 1), Step 7.3 (Provisioning) row | Guest → kept showing provisioning/waiting state, and the frontend 60s timeout/retry mechanism remained active (Ref: UC_2.8.4 §5 Exceptional Flow) | Guest → keep showing provisioning/waiting state indefinitely (no frontend timeout); failure handling delegated to backend auto-retry mechanism (Ref: UC_2.8.2 §Step 3 Exception); on exhaustion, banner BN-06 renders (Ref: UC_2.8.4 §6 Exceptional Flow) | Client confirmed (chat), 2026-07-30 — remove 60s provisioning timeout/retry mechanism (see UC_2.7-2.8_v1.md changelog v2.2 for the primary change) |
| 2026-07-30 | v7 | BR_2.1.1.4 (renamed), UC_2.1.3 §4/§5/§8, UC_2.6.1 §5, UC_2.6.2 §5 Phase 1/2 | UTM captured once at Step 0, stored insession state, first-seen-wins (no overwrite on subsequent loads/deep-links) | UTM capture logic extracted to new cross-cuttingCR-12 (root-layout script, site-wide, not checkout-only). Storage changed to localStorage; attribution rule changed to last-touch (overwrite on presence, untouched if URL has no UTM) — supersedes QnA Q9 first-seen-wins. All UTM read-points in this doc updated to "read from localStorage per CR-12" at time-of-action, instead of a session-state value cached once at Step 0 | CR-20260730-002; Adrian Stack (Slack, 2026-07-30) via Hexie |
| 2026-07-30 | v8 | UC_2.6.2 §8 Field Notes (abandoned_step), new BR_2.6.2.10 | abandoned_step updated to the user's current step on every Phase 1/re-entry write — could regress backwards (e.g. Step 7 → Step 5 on reload) | abandoned_step now monotonic — only overwritten when the incoming step's rank is strictly greater than the stored value's rank; new BR_2.6.2.10 defines the ordinal rank table and GREATEST()-style comparison rule; Field Notes description updated with cross-ref | Client instruction (chat), 2026-07-30 |
| 2026-08-01 | v10 | UC_2.1.1 (Trigger note item 4, §5 Basic Flow Step 3b), UC_2.1.3 (UC Index description, §1 Overview Description, §5 Basic Flow steps 2/7, §6 Exceptional Flow, §7 BR_2.1.3.1, §8 Screen Description) | Gate 1 rendered a Waitlist Lead Captureoverlay on top of the checkout page (which itself has no Header/Footer); overlay had a [✕] Close icon that dismissed it and returned the user to the checkout page underneath. | Gate 1 now redirects (client-side navigation, same tab) to adedicated Waitlist page that retains the standard Marketing Header and Footer (exception carved into UC_2.1.1's "No Standard Marketing Header/Footer" rule). Checkout UI is unmounted on redirect — there is no longer a checkout page underneath to return to. The [✕] Close icon and its "returns to checkout" behavior are removed entirely — no Close action exists on the Waitlist page. "Overlay Header" renamed to "Page Content Header" to avoid confusion with the site-wide Header. | User decision (BA session), 2026-08-01 — architecture change from overlay to dedicated page; avoids the redirect-loop risk of "Close → back to checkout" once the checkout page no longer exists underneath. Paired withUC_1.1-1.6_v1.md new BR_1.1.1.4 (Header CTA suppression on this page) and list-toast-popup.md OV-04 update. |
| 2026-07-31 | v9 | UC_2.1.2 §4 — Checkbox 2 "Terms of Service" click behavior | Checkbox 2 label text documented, no click/hyperlink behavior for the "Terms of Service" words within it | Added: "Terms of Service" text is a hyperlink opening a popup at Step 5 (no navigation away, no flow reset); popup content identical to the public/terms page (UC_29) — same text, same "Last Updated" timestamp; closing popup returns to Step 5 with all form/checkbox state preserved. Cross-updated UC_29 BR-29-03 (supersedes A-10 "distinct document" framing) and corrected its scope note UC reference from UC_49 (Regional Payment Aggregators) to this section | User request 2026-07-31 |
| 2026-08-08 | v13 | BR_2.1.5.2 — Stale Pricing: thêm tax change trigger + spec [Refresh now] | Chỉ đề cập Founder cohort sold out; [Refresh now] không có spec behavior. | Thêm tax rate change là trigger thứ hai của OV-08. Spec [Refresh now]: đóng OV-08 và refresh Order Summary với giá/tax mới — không reload page. | BA directive 2026-08-08. |
| 2026-08-04 | v11 | BR_2.6.1.7 §5 (Sequencing step 2.2), UC_2.6.2 §5 (Confirm Email Gate step 3) | Single IN-05 for both invalid format AND mismatch:"Emails do not match." | Split into two separate inline error messages:IN-07 ("Please enter a valid email address") for invalid email format, IN-05 ("Emails do not match") for mismatch. BR_2.6.1.7 steps renumbered: 2.2 → 2.2 (invalid format, IN-07) + 2.3 (mismatch, IN-05); UC_2.6.2 §5 step 3 split into 3a/3b; cross-ref from re-validation section updated to "step 2.3". | BA directive — separate error messages for email format vs mismatch on Confirm Email field. (Source: list-toast-popup.md IN-05/IN-07) |
| 2026-08-05 | v12 | UC_2.6.1 §5 step 4, §8 Rows 3b/4/5, BR_2.6.1.1, BR_2.6.1.6 — Country/Region/ZIP field visibility pattern | Progressive disclosure pattern: State/Region disabled until Country selected; ZIP hidden by default, shown only whenrequires_zip=true. BR_2.6.1.1 debounce rule hardcoded 2-or-3-field combinations. | Reverse visibility pattern: all 3 fields (Country, State/Region, ZIP) visible by default on Step 5 load. On Country selection → hide State/Region if country has no subdivisions; hide ZIP ifrequires_zip=false. BR_2.6.1.1 debounce rule rewritten to check "currently visible fields" with 4-branch matrix (State visible/hidden × ZIP visible/hidden). | Client directive (chat), 2026-08-05. |
| 2026-08-09 | v14 | BR_2.1.1.3 Table 1 Step 6 — email lock overlay added; split-panel always renders on mount | Step 6 reload only documented payment-in-progress overlay (OV-05); email lock state not evaluated on mount; wording implied split-panel was conditional on payment check. | Revised: Step 6 always renders split-panel on reload. Two overlay conditions evaluated independently after render: email locked (BR_2.8.1.2) → OV-03 on top; payment in progress → OV-05 on top. If neither: normal split-panel, no overlay. | BA gap analysis, 2026-08-09. |
| 2026-08-10 | v15 | UC_2.1.1 §4 Field Notes (id, icon_tags rows), §5 Step 3d, Payment_Method_Config Table Schema (method_id, gateway_router) | Payment method enum/config listedNOMUPAY, NUVEI, DUSUPAY; Step 3d used target_country = 'BR' → Nuvei Pix/Boleto as the country-routing example | Nomupay and Nuvei removed from all enums/config rows (method_id, gateway_router, id field notes); added T365; Step 3d example rewritten to target_country = 'ZA' → Dusupay Mobile Money/Bank; icon_tags exclusion list updated to Dusupay/T365 | Client supplement (chat), 2026-08-10 — aligns with UC_2.7-2.8_v1.md changelog v3.2 (Dusupay redirect flow + new T365 method; Nomupay/Nuvei removed). WBS intentionally NOT updated per client instruction. |
| 2026-08-12 | v16 | UC_2.6.1 new BR_2.6.1.8, §8 Rows 1/2/3/3b (Full Name, Email, Billing Address, ZIP/Postal Code) | No rule covering invalid field data at Step 5 on reload/back/next; behavior undefined | Added BR_2.6.1.8: on full page refresh (F5, per BR_2.1.1.3 Table 1), [Back], or [Next], an invalid field displays its invalid data as-is together with an inline error message and red border; cross-referenced from Rows 1/2/3/3b | User directive (chat), 2026-08-12 |
| 2026-08-15 | v17 | UC_2.6.1 new BR_2.6.1.10, §8 Rows 2/2b, BR_2.6.1.7 (Sequencing); UC_2.6.2 §4/§5/§5a (Phase 0) | No rule blocking Stack Trading company email addresses (e.g.stacktrading.com) at Step 5 Email/Confirm Email | Added BR_2.6.1.10 (Company Email Domain Block): Email and Confirm Email onBlur each independently reject domain@stacktrading.com, halting before Phase 0 (Blacklist/Duplicate Account Check) with new inline error IN-11 ("Stacktrading email addresses are not allowed for this field"); updated BR_2.6.1.7 sequencing, §8 Rows 2/2b, and UC_2.6.2 §4/§5/§5a accordingly | [CHR-50]; Adrian Stack (Slack, via Hexie), 2026-08-15 |