Read-only per-epic slice. The canonical source of truth is BRD.md — stories are addressed by US-ID, not by this page's line numbers.
Epic 2 — Processor onboarding (derived view)
Read-only per-epic slice of
BRD.md§9, lines 582–1048. The canonical source of truth isBRD.md— edit there, never here. The stable address for a story is its US-ID (US-2.x), not a line number. Regenerates on everydev → mainsync viaderive-state-on-main.
- Stories (7): US-2.1, US-2.2, US-2.3, US-2.4, US-2.5, US-2.6, US-2.7
- Generated: 2026-07-01T17:48:39.076Z · as-of commit:
b083f095
Epic 2 — Processor onboarding
<!-- traceability:start:BRD:Epic-2 --><!-- traceability:end:BRD:Epic-2 -->Prototype: Gateway Compatibility · BC Payments · Stripe Connect · Test Mode · Processor Health · Capture-timing advisory · Capture-timing config (settings)
Value: Merchant can connect a payment processor within minutes so subscriptions can actually charge.
Epic context. The processor setup is the first value-demonstrating step. UX quality here correlates directly with activation rate.
US-2.1: Auto-detect BC Payments
<!-- traceability:start:US-2.1 --><!-- traceability:end:US-2.1 -->Prototype: BC Payments
Phase: MVP · Priority: P0 · Effort: M · Persona: Merchant Admin
As a Merchant Admin on BC Payments, I want the app to detect my BC Payments account automatically, so that I don't configure a separate processor.
Acceptance criteria:
- Given I open the processor setup page, When BC Payments is enabled on my store, Then "BC Payments" is pre-selected and marked "Ready."
- Given BC Payments is not enabled, When I view the page, Then I see an "Enable BC Payments" link to BC's own onboarding flow.
UI states.
<!-- ui-states US-2.1 -->surface: "/settings/payment → 'BC Payments — US-2.1' panel (apps/admin/src/pages/settings/PaymentSettings.tsx); BRD-stated surface /stores/[storeHash]/setup/processor"
idle:
render: "The 'BC Payments — US-2.1' panel reads 'If your BigCommerce store has BC Payments (bigpay) enabled, click below to auto-detect and register it. No keys needed — BC Payments authenticates via your store access token.' with an 'Auto-detect BC Payments' button; the 'Connected processors' panel above lists any existing processor_connection rows."
primary_action: "Auto-detect BC Payments — POST /api/v1/admin/processor-connections/auto-detect, which probes BC GET /v3/payments/methods for a bigpay.* method."
loading:
trigger: "POST /api/v1/admin/processor-connections/auto-detect in flight (autoDetectBusy), plus the initial GET /api/v1/admin/processor-connections list."
render: "The 'Auto-detect BC Payments' button shows its in-flight spinner and is disabled (no double-submit); the 'Connected processors' panel shows 'Checking your BC Payments status…' while the connection list resolves."
error:
surfaced_at: "Inline inside the 'BC Payments — US-2.1' panel, directly above the 'Auto-detect BC Payments' button (role=alert, dismissable) — never a toast that vanishes; the connection-list failure surfaces inline at the top of the 'Connected processors' panel."
render: "'Unable to verify BC Payments status — retry in a minute.' shown when the BC probe or the connection-list call fails."
recovery: "Dismiss the message and re-click 'Auto-detect BC Payments'; the button stays enabled so the probe retries without a page reload."
empty:
render: "When no processor_connection exists, the 'Connected processors' panel reads 'No processor connected yet. Connect Stripe below, or auto-detect BC Payments if your store has it enabled.' — never a blank surface."
cta: "Auto-detect BC Payments (this surface's primary action)."
inputs: []
edge_status:
- status: "detected — bigpay.* active, processor_connection created (bc_payments / active / auto_detected)"
badge: "Ready"
affordance: "Success message 'BC Payments detected and registered for this store.'; the 'Connected processors' panel refreshes to list 'BC Payments' with an 'active' badge — merchant continues to plan setup."
- status: "already_active — a bc_payments connection already existed"
badge: "Ready"
affordance: "Info message 'BC Payments is already registered.'; no duplicate row is created — merchant continues to the next setup step."
- status: "not_enabled — bigpay.* not active on the store"
badge: "Not enabled"
affordance: "Warning message plus an 'Enable BC Payments' link to BC's own onboarding flow (AC2); after enabling, 'Auto-detect BC Payments' re-runs the probe."
- status: "stored_instruments_disabled — BC Payments detected but GET /v3/payments/stored-instruments is off"
badge: "Action needed"
affordance: "Warning message carrying the storedPaymentsWarning copy that stored instruments must be enabled for recurring charges, with a link to enable it, then re-run auto-detect."
- status: "disconnected — an existing bc_payments connection lost authorization"
badge: "disconnected"
affordance: "The 'Connected processors' row shows a 'disconnected' warning badge; re-run 'Auto-detect BC Payments' to re-establish the connection."
- status: "error — an existing bc_payments connection is in an error state"
badge: "error"
affordance: "The 'Connected processors' row shows an 'error' danger badge; re-run 'Auto-detect BC Payments' to re-probe and clear the error."
disabled_focus:
keyboard: "'Auto-detect BC Payments' is a real BigDesign <Button> reachable in tab order with a visible focus ring (never a div-onClick); the not_enabled recovery is a real focusable <a> 'Enable BC Payments' link, not static text — no keyboard dead-end."
focus_move: "When the async probe resolves, the outcome message is an aria-live=polite region and focus moves to the result so a keyboard / screen-reader merchant learns the outcome; on a not_enabled result focus lands on the 'Enable BC Payments' link so the next step is one keystroke away."
guard: "Auto-detect is a single non-destructive, idempotent click (a read-only probe) — no typed-confirm required; re-running is always safe."
UX notes.
- Surface:
/stores/[storeHash]/setup/processor - Loading state: "Checking your BC Payments status…" skeleton for ≤ 2s
- Success: card with BC Payments logo, "Connected and ready" check, "Powered by PayPal" microcopy
- Alternate: if BC Payments is available-but-not-enabled in their region: "Enable BC Payments now" CTA (deep link to BC's setup) + "Continue with Stripe instead" secondary CTA
- Unavailable: if BC Payments is not in their region: Stripe is pre-selected with a note explaining BC Payments regional availability
Data contract.
- Our API:
POST /api/admin/processor-connections/auto-detect— probes BC, createsprocessor_connectionrow ifbigpay.*detected. Backed byapps/api/src/services/processor-auto-detect.ts(synthesis 9457c80c, PR #737). - BC API:
GET /stores/{store_hash}/v3/payments/methods— filter forbigpay.*method ID. Verified 2026-05-14 against BC docs; scopestore_payments_methods_read.order_idis optional for store-level detection. BC Payments identifier:bigpay.cardper ADR-0035. - Our DB:
processor_connectionsrow inserted on successful detection withprocessor: 'bc_payments',status: 'active',account_ref: 'auto_detected'. - Events:
processor_connection.auto_detectedwith{processor, processor_connection_id, method: 'auto_detect'}.
Success metrics.
- Functional: 100% of BC Payments–enabled stores are correctly detected
- Product (target): % of MVP merchants who complete processor setup in < 60s
- Operational (target): P95 detection latency < 2s
Dependencies.
- Upstream:
GET /v3/payments/methodsis GA (no beta enrollment required). Verified 2026-05-14.OPEN QUESTION — confirm with BC Payments team→ RESOLVED (synthesis 9457c80c). - Downstream: Epic 10 (scheduling can begin after processor connected)
Non-functional.
- If BC API is down, show "Unable to verify BC Payments status — retry in a minute" rather than blocking
- Cache detection result for 1 hour (avoid per-page-load API hammering)
Risks / open questions.
Confirm the detection endpoint exists publicly— RESOLVED (2026-05-14):GET /v3/payments/methodsis publicly documented and GA (no BC partner-track enrollment required). Implemented in synthesis 9457c80c. No merchant checkbox needed.
US-2.2: Stripe Connect onboarding
<!-- traceability:start:US-2.2 --><!-- traceability:end:US-2.2 -->Prototype: Stripe Connect
Phase: MVP · Priority: P0 · Effort: L · Persona: Merchant Admin
As a Merchant Admin who wants to use Stripe, I want to connect my Stripe account via OAuth, so that renewal charges settle into my Stripe account.
Acceptance criteria:
- Given I pick "Stripe" on the processor page, When I click "Connect," Then I am redirected through Stripe Connect OAuth and returned with an attached
stripe_account_id. - Given the connection succeeds, When I return, Then I see "Connected" with the last 4 of my Stripe account name and a "Test charge" button.
- Given I revoke access in Stripe, When I return to our app, Then we detect the disconnection and prompt re-auth.
UX notes.
- Surface:
/stores/[storeHash]/setup/processor/stripe - Flow: "Connect Stripe account" button → redirect to Stripe Connect OAuth → callback returns us the
stripe_account_id→ show connected state - States: not connected | connecting | connected (with last 4 of account display name) | error (Stripe returned error, expand for detail)
- Post-connect: a "Run test charge" affordance that charges $1 to a Stripe test PM to verify end-to-end wiring
Data contract.
- Our API: GET
/api/v1/processors/stripe/connect→ returns Stripe OAuth URL with state param - Stripe API: OAuth code exchange, then GET
/v1/accounts/{id}to fetch account metadata - Our DB:
processor_connectionsrow with encryptedaccount_ref(stripe_account_id) +credentials_encrypted(we use Stripe Connect standard accounts — no Stripe secret key stored) - Events:
processor.connectedwith{processor: stripe, account_id: acct_...}
Success metrics.
- Functional: test charge succeeds in test mode
- Product (target): ≥ 80% of merchants starting Stripe onboarding complete it in one session
- Operational (target): P95 OAuth round-trip < 8s (heavily Stripe-dependent)
Dependencies.
- Upstream: Stripe Connect app registered; platform account configured
- Downstream: Epic 10 (Stripe adapter implementation)
Non-functional.
- Disconnection from Stripe side (merchant revokes) must be detected within 1h by a polling job; badge turns red
- Webhook endpoint registered automatically during connect (for
payment_intent.*,payment_method.*)
Risks / open questions.
- Stripe Connect requires our platform to be on a specific Stripe plan; verify pricing before Phase 1
- Tax handling: default to "do not use Stripe Tax" in MVP; merchant uses BC tax. Revisit in P3 "enterprise Stripe mode" (see PRD §16.1)
UI states.
<!-- ui-states US-2.2 -->surface: "Admin (React/BigDesign) Payment Settings — the 'Connect Stripe — US-2.2' Panel in apps/admin/src/pages/settings/PaymentSettings.tsx:288-351. Persona: Merchant Admin. A raw Stripe API-key entry form (publishable + secret key) that POSTs connectStripeProcessor; NOTE BRD AC-1 mandates a Stripe Connect OAuth redirect flow — the built surface is key-entry, not OAuth (see disabled_focus.gaps)."
idle:
render: "The Panel shows intro copy linking to dashboard.stripe.com/apikeys, a 'Publishable key' BigDesign Input (placeholder pk_live_..., lines 318-324), a masked 'Secret key' Input (type=password, placeholder sk_live_..., lines 326-333), a 'Connect Stripe' primary Button (lines 337-343), and a Small note that keys are validated against the pk_/sk_ prefix before posting. When a Stripe connection is already active the form is replaced by the text 'A Stripe processor is already active for this store…' (hasActiveStripe, lines 305-308)."
primary_action: "'Connect Stripe' → handleConnectStripe (lines 204-222) calls connectStripeProcessor with the trimmed keys; disabled until stripeFormValid (pk_ + sk_ prefixes and secret length >= 20, lines 245-246)."
loading:
trigger: "POST /api/v1/admin/processor-connections via connectStripeProcessor (processor-connections.ts:89-115) while stripeBusy === true."
render: "The 'Connect Stripe' Button shows its BigDesign isLoading spinner and both Inputs are disabled (disabled={stripeBusy}, lines 323/332/338); no double-submit. No skeleton — the Panel chrome stays mounted."
error:
surfaced_at: "Inline BigDesign Message(type='error') at the top of the Stripe Panel (lines 297-304), dismissible via onClose (X) — never a toast that vanishes."
render: "The thrown reason verbatim from connectStripeProcessor — e.g. 'HTTP 409: <body>' when an active Stripe connection already exists (documented at processor-connections.ts:87-111), or 'Connect failed'."
recovery: "setStripeBusy(false) re-enables the still-populated form (finally block, lines 219-221) so the merchant corrects a key and re-presses 'Connect Stripe'; a 409 directs disconnecting the existing connection first (the hasActiveStripe path)."
empty:
render: "Not a list surface — a single connect form. The processor inventory (and its 'No processor connected yet…' empty copy) is owned by the sibling 'Connected processors' panel (US-2.5, lines 252-286)."
cta: "n/a — single-action form, not a collection."
inputs:
- field: "stripe_publishable_key"
control: "text"
allowed_values: "n/a — free-form Stripe key, not an enumerable domain; client guard requires the pk_ prefix (stripeFormValid, lines 245-246)."
- field: "stripe_secret_key"
control: "text"
allowed_values: "n/a — free-form secret (BigDesign Input type=password, lines 326-333); client guard requires the sk_ prefix and length >= 20 (stripeFormValid, lines 245-246)."
edge_status:
- status: "keys_fail_prefix_validation — pk_/sk_ prefix or secret length not met"
affordance: "'Connect Stripe' stays disabled (stripeFormValid false, lines 245-246); the Small helper names the pk_/sk_ format so the merchant corrects the key before any POST."
- status: "stripe_already_active — an active Stripe connection exists"
affordance: "the form is replaced by 'A Stripe processor is already active…' (lines 305-308); to rotate the secret the merchant disconnects the existing connection first."
- status: "connect_failed — POST rejected (e.g. HTTP 409 duplicate)"
affordance: "error Message (lines 297-304); the form stays populated and re-enabled so the merchant retries with corrected keys."
disabled_focus:
keyboard: "All three controls are real BigDesign components (Input/Input/Button) wrapping native focusable elements — no div-onClick. Tab order follows DOM source order: publishable key Input → secret key Input → 'Connect Stripe' Button. Native disabled removes the inputs and button from tab order while stripeBusy; the primary Button is Enter/Space-activatable and gated by stripeFormValid."
focus_move: "On a connect failure the error Message (lines 297-304) is inserted with no role='alert' / aria-live and no programmatic focus move; PaymentSettings.tsx imports no useRef and calls no .focus() anywhere. A keyboard/screen-reader merchant gets no announcement of success or failure."
guard: "Connecting is a single intentional, non-destructive click — no typed-confirm; double-submit is blocked by the isLoading Button + disabled Inputs while stripeBusy."
gaps: "BRD AC-1 (MVP/P0) mandates a Stripe Connect OAuth redirect (GET stripe/connect → Stripe OAuth → callback attaches stripe_account_id); the build ships raw publishable/secret key entry with NO OAuth redirect or callback path. AC-2's 'Connected' display with the account last-4 and a 'Run test charge' affordance is absent (no test-charge UI exists in the file). AC-3 revoke-detection (badge + re-auth prompt) is not surfaced here — it depends on the US-2.5 polling job that is also unbuilt. North-star: replace the key form with the OAuth Connect button, render the connected account summary + test-charge, and add aria-live on the result Message."
US-2.3: Processor compatibility detection at install
<!-- traceability:start:US-2.3 --><!-- traceability:end:US-2.3 -->Prototype: Gateway Compatibility
Phase: MVP · Priority: P0 · Effort: M · Persona: Merchant Admin
As a Merchant Admin, I want to know at install time whether my current payment gateway can support subscriptions, so that I don't discover incompatibility after launch.
Acceptance criteria:
- Given I complete install, When the processor-detection step runs, Then it lists my active BC payment methods with each marked "Supported," "Supported in Phase 2," or "Not supported" per §6.3 of the PRD.
- Given none of my active gateways are MVP-supported, When I reach the setup screen, Then I see three explicit options: switch to BC Payments, add Stripe, or decline install.
UX notes.
- Surface: install-flow processor step — shown before US-2.1 / US-2.2 setup
- Layout: table of merchant's active gateways with compatibility badge:
MVP Supported,Phase 2,Not Supported,Incompatible (BNPL/session-bound) - For unsupported gateways, tooltip explains why and shows "Switch to BC Payments" / "Add Stripe" options
- Empty state (no gateways enabled): guide merchant to enable BC Payments first via BC control panel
Data contract.
- Our API: GET
/api/v1/store/gateways/compatibility - BC API:
GET /v3/payments/methodsto enumerate merchant's enabled gateways - Lookup: compare against our internal matrix (PRD §6.3 data); each entry has
mvp|p2|p3|neverclassification
Success metrics.
- Functional: table accurately reflects BC's enabled-gateways list
- Product (target): reduces post-install "my gateway doesn't work" tickets by ≥ 80%
- Operational (target): detection < 1s P95
Dependencies.
- Gateway support matrix (PRD §6.3)
- BC's
/v3/payments/methodsavailability
Non-functional.
- Compatibility matrix is versioned in code; updates trigger a release (not a DB change)
UI states.
<!-- ui-states US-2.3 -->surface: "Admin (React/BigDesign) onboarding — processor compatibility, surfaced TODAY only as a checklist item. apps/admin/src/pages/onboarding/SetupChecklist.tsx (routed at /v2/onboarding/setup, App.tsx line 133): a checklist item whose status can be 'pending-incompatible' renders a warning Badge + WarningIcon when the merchant's active gateways exist but none support MIT subscription charges (ChecklistItemStatus, apps/admin/src/lib/api-client.ts line 362). Persona: Merchant Admin. The BRD's dedicated gateway-detection table (per-gateway MVP/Phase2/Not-Supported badges) and the three-option fallback for wholly-unsupported stores are NOT built in the admin SPA — see gaps."
idle:
render: "Within the checklist, the processor item renders as an ItemRow: a status icon (WarningIcon color=warning50 for pending-incompatible, statusIcon line 121), the item title/body, a status Badge (variant=warning, label 'Incompatible' via statusBadge lines 143-146), and a CTA Button that links the merchant to fix it (item CTA, line 311). North-star: a separate compatibility Panel listing each active gateway with its own badge."
primary_action: "The checklist item's CTA Button (line 311) navigates (or opens an external BC settings link) to connect a compatible gateway. North-star adds explicit 'Switch to BC Payments' / 'Add Stripe' / 'Decline install' actions."
loading:
trigger: "On mount getOnboardingChecklist() GET /api/v1/onboarding/checklist (line 161); the item status (incl. pending-incompatible) is computed server-side."
render: "While !loaded a plain 'Loading' Text is shown (line 260); no skeleton. North-star gateway-detection table would show a per-row skeleton while GET /api/v1/store/gateways/compatibility resolves."
error:
surfaced_at: "If the checklist fetch fails, the .catch sets error and a BigDesign <Message type=warning> renders above the list (lines 231-239, header errorHeader)."
recovery: "The warning Message explains the load failed; there is no in-app retry control, so the merchant reloads the page. A north-star detection table would surface its own inline error + retry for the gateways probe."
empty:
render: "North-star empty (no active gateways enabled): per the BRD, guide the merchant to enable BC Payments first via the BC control panel. TODAY this empty case has no dedicated surface — with zero gateways the checklist simply shows the processor item as pending/incompatible with its generic CTA; the gateway-list empty state is part of the unbuilt detection table — see gaps."
edge_status:
- status: "gateways present but none MIT-capable (pending-incompatible) — BUILT"
affordance: "warning Badge + WarningIcon (lines 121, 143-146); the checklist item CTA (line 311) links the merchant to connect a compatible gateway."
- status: "gateway = Not Supported / none MVP-supported — north-star"
affordance: "three explicit options per the AC — 'Switch to BC Payments', 'Add Stripe', 'Decline install' — each a distinct action; NOT built today (see gaps)."
- status: "gateway = Supported in Phase 2 — north-star"
affordance: "informational badge in the detection table noting the gateway activates for subscriptions in Phase 2; no blocking action. NOT built today."
- status: "no gateways enabled (empty) — north-star"
affordance: "guide the merchant to enable BC Payments in the BC control panel (deep-link); NOT built today."
inputs: []
disabled_focus:
keyboard: "The checklist item CTA is a real BigDesign <Button> (line 311) reachable via Tab and activatable with Enter/Space; the WarningIcon is decorative and the incompatible state is also conveyed by the Badge's text label (statusBadge, line 146), so the warning is not color-only. No div-onClick dead-ends."
gaps: "The incompatible status relies on icon + colored badge + text label; the text label keeps it non-color-only. The north-star per-gateway detection table (with focusable per-row actions) does not exist, so its keyboard/focus contract is unverified."
gaps: "Only the checklist-level 'pending-incompatible' signal is built (one warning badge for the whole processor step). The BRD AC requires (a) a table of the merchant's active gateways each marked Supported/Phase 2/Not Supported/Incompatible from GET /api/v1/store/gateways/compatibility, and (b) when none are MVP-supported, three explicit options — switch to BC Payments, add Stripe, or decline install. Neither the detection table nor the three-option fallback exists in the admin SPA (no gateway-compatibility component found), so a merchant on a wholly-unsupported store gets a single generic warning, not the required decision surface."
US-2.4: Sandbox/test mode
<!-- traceability:start:US-2.4 --><!-- traceability:end:US-2.4 -->Prototype: Test Mode
Phase: MVP · Persona: Merchant Admin / Developer
As a Merchant Admin, I want to test the subscription flow end-to-end before going live, so that I can validate pricing and cadence without risking real charges.
Acceptance criteria:
- Given I toggle "Test mode" on, When I create a subscription, Then charges route to the processor's sandbox (Stripe test mode; BC Payments sandbox) and no real card is debited.
- Given test-mode charges succeed, When they generate BC orders, Then the orders are tagged with
[TEST]in staff_notes and are filterable in the merchant dashboard. - Given I toggle test mode off, When I have test subscriptions active, Then I am prompted to archive them before going live.
UI states.
<!-- ui-states US-2.4 -->surface: "Admin (React/BigDesign) Settings, Store, General. apps/admin/src/pages/settings/store/General.tsx, routed at /v2/settings/store and /v2/settings/store/general (App.tsx lines 138-139). A single settings form (umbrella GET/PATCH /api/v1/admin/settings/store) with a 'Test Mode' Panel whose Checkbox enables test mode for new subscriptions (routing charges to the processor sandbox), alongside Capture Timing, read-only Region, and BC Order Status panels and one shared Save. Persona: Merchant Admin / Developer."
idle:
render: "An H1 'General' + intro, then four Panels: Capture Timing (Select + advisory Message), Region (read-only country_code shown in a <code> block sourced from the BC profile — not an editable control), Test Mode (Checkbox 'Enable test mode for new subscriptions', line 212, with help copy), and BC Order Status (numeric Input). A single Save Button (line 248) sits at the bottom."
primary_action: "Toggle the Test Mode Checkbox (line 212) then click Save (handleSave to PATCH /api/v1/admin/settings/store, line 79); the toggle alone does not route charges until Save persists test_mode_enabled=1."
loading:
trigger: "On mount getSettingsStore() GET /api/v1/admin/settings/store (line 65)."
render: "While loading===true every control is disabled (capture Select disabled line 175, Test Mode Checkbox disabled line 216, order-status Input disabled line 238, Save disabled line 248); no skeleton. On success settings is populated and the controls enable."
error:
surfaced_at: "Inline BigDesign <Message> banners at the top of the page, never toasts. A load failure renders a non-dismissable Message type=error 'Failed to load settings: ...' (lines 129-135). A save failure renders a dismissable Message type=error 'Save failed: ...' (lines 146-153). A success/no-op renders a dismissable Message type=success (lines 137-144)."
recovery: "Load failure: settings stays null and no form renders, so there is no in-app retry — the merchant reloads the page. Save failure: setSaving(false) re-enables the still-populated form (finally, line 116) so the merchant fixes the value and clicks Save again."
empty:
render: "Not a collection — a single settings form. Before load completes settings===null and the controls render disabled; if the load fails settings stays null and only the loadError Message shows (no form body). On success the four panels always render with current values (country_code shows a '(not set — update via your BigCommerce store profile)' fallback when null, line 198)."
inputs:
- field: "test_mode_enabled"
control: "checkbox"
label: "'Enable test mode for new subscriptions' — BigDesign Checkbox (lines 212-217), default from settings.test_mode_enabled===1 (line 69)."
validation: "boolean; persisted as 1/0 via PATCH (test_mode_enabled: testMode ? 1 : 0, line 88)."
- field: "capture_timing"
control: "select"
label: "'Capture Timing' — BigDesign Select (lines 173-180)."
allowed_values: "immediate, on_fulfillment, on_ship (CAPTURE_TIMING_OPTIONS, lines 47-51); non-immediate modes are advisory-only until Phase 2 (ADR-0038)."
- field: "default_sub_status_id"
control: "number"
label: "'Default order status ID' — BigDesign Input type=number, min=1 (lines 231-239); blank uses default 11."
edge_status:
- status: "test mode toggled ON but not yet saved"
affordance: "the Checkbox flips immediately but charges do not route to sandbox until Save persists test_mode_enabled=1 (handleSave line 79); the merchant must click Save to apply."
- status: "Save returned no changed_keys (clicked Save with no edits)"
affordance: "a success Message reads 'No changes to save.' (lines 108-112) — a deliberate signal distinct from a generic 'Saved'; the merchant edits a field and Saves again."
disabled_focus:
keyboard: "All editable controls are real BigDesign components (Select/Checkbox/Input/Button) wrapping native focusable elements — no div-onClick. Tab order follows DOM order: Capture Timing Select, then (read-only country_code <code> is skipped, not focusable), Test Mode Checkbox, Order Status Input, Save. Native disabled removes controls from tab order while loading/saving. The Save Button is reachable via Tab and activatable with Enter/Space."
gaps: "The conditional load/save Messages (lines 129-153) are inserted with no role=alert/aria-live and no programmatic focus move — a keyboard/screen-reader merchant gets no announcement when a save fails (same class as US-22.1). Also: the file header (lines 8-10) claims the test-mode toggle is 'gated behind a confirmation modal so it is not a one-click foot-gun', but no Modal exists — the Checkbox onChange sets testMode directly (line 215)."
gaps: "Two divergences from spec/comment. (1) The file header (lines 8-10) documents a confirmation modal guarding the test-mode toggle; the code has no Modal — the Checkbox flips state on a single click (line 215). Low risk (non-destructive; requires Save; affects only NEW subs) but the comment is stale. (2) BRD AC3 — 'toggle test mode OFF while test subscriptions are active, then prompt to archive them before going live' — is not implemented here; toggling off and saving writes test_mode_enabled=0 with no archive prompt. AC2 ([TEST] staff_notes tagging + dashboard filter) is backend/other-surface, not this panel."
US-2.5: Processor health check
<!-- traceability:start:US-2.5 --><!-- traceability:end:US-2.5 -->Prototype: Processor Health
Phase: MVP · Persona: Support / Ops
As Support / Ops, I want a continuous health signal for my processor connection, so that I know immediately if charges will fail.
Acceptance criteria:
- Given a processor is connected, When its API returns 401/403 or the account is suspended, Then our health badge turns red and an email goes to the merchant contact.
- Given the processor recovers, When a subsequent test ping succeeds, Then the badge returns to green automatically.
UI states.
<!-- ui-states US-2.5 -->surface: "Admin (React/BigDesign) Payment Settings — the 'Connected processors' Panel in apps/admin/src/pages/settings/PaymentSettings.tsx:252-286. Persona: Support / Ops. A read-only health list: one row per processor_connections row (listProcessorConnections, processor-connections.ts:77-81) showing processorLabel + a BigDesign Badge whose variant comes from processorBadgeVariant (lines 113-122: active→success, disconnected→warning, error→danger)."
idle:
render: "For each connection a Flex row renders the processor name (processorLabel, lines 124-135) plus an optional account_ref Small, and a status Badge on the right (line 281). The list refreshes only via loadConnections() on mount (useEffect, lines 174-176) and after a successful connect/auto-detect — there is no live health polling."
primary_action: "None — this widget is display-only; the connect/auto-detect actions live in the sibling Stripe (US-2.2) and BC Payments (US-2.1) panels."
loading:
trigger: "GET /api/v1/admin/processor-connections via listProcessorConnections on mount (loadConnections, lines 174-186)."
render: "While connections === null and there is no error, the Panel shows the text 'Loading processor connections…' (lines 260-261). No skeleton or spinner."
error:
surfaced_at: "Inline BigDesign Message(type='error') at the top of the Panel — 'Failed to load connections: <reason>' (connectionsError, lines 253-259). Persistent inline, not a toast."
render: "The thrown reason from authedFetch — e.g. 'HTTP 500: <body>' or 'NO_AUTH_TOKEN' (processor-connections.ts:26-39)."
recovery: "GAP — the error Message carries no onClose and no in-panel 'Retry' control; loadConnections re-runs only on mount, so the only recovery today is a full page reload. North-star: a Retry button that re-invokes loadConnections (see disabled_focus.gaps)."
empty:
render: "When connections.length === 0 the Panel shows 'No processor connected yet. Connect Stripe below, or auto-detect BC Payments if your store has it enabled.' (lines 262-265) — never a blank pane."
cta: "Points the operator down to the Stripe connect form (US-2.2) and the BC Payments auto-detect button (US-2.1)."
edge_status:
- status: "active — processor reachable / charges will route"
badge: "active (raw enum rendered verbatim as the Badge label — see gaps)"
affordance: "no action needed; the green Badge confirms the connection is healthy."
- status: "disconnected — merchant revoked or the connection lapsed"
badge: "disconnected (amber/warning)"
affordance: "reconnect via the Stripe panel (US-2.2) or re-run BC Payments auto-detect (US-2.1) to restore an active connection."
- status: "error — processor API returned 401/403 or the account is suspended"
badge: "error (danger)"
affordance: "the danger Badge flags the failure; the operator fixes credentials in the processor panel and reconnects. NORTH-STAR (BRD AC-2): a background ping should auto-recover the badge to active — no such polling exists in the frontend (see gaps)."
disabled_focus:
keyboard: "The widget renders no interactive controls — it is a static Badge/Text list, so it adds no tab stops to trap; the Badge and Text are non-focusable presentational elements. Keyboard operators reach the adjacent connect/auto-detect Buttons (real BigDesign <button>s) in the sibling panels."
focus_move: "No dynamic focus concerns — the list re-renders in place after loadConnections; nothing opens or closes. The status change is NOT wrapped in an aria-live region, so a screen-reader user is not announced when a badge flips active↔error."
guard: "Read-only surface — no destructive or mutating action, so no confirm is required."
gaps: "raw-enum DEFECT — the Badge label is the raw status token c.status ('active'/'disconnected'/'error') rendered verbatim (line 281), not an i18n'd human string; north-star is a localized label (e.g. 'Active', 'Disconnected', 'Connection error'). NO auto-polling/health loop exists (no setInterval/refresh in the file), so BRD AC-2 ('processor recovers → badge returns to green automatically') is unmet on the frontend. The load-error Message has no Retry affordance and no onClose, so a transient list-load failure dead-ends at a page reload."
US-2.6: Multiple processor connections
Phase: P2 · Persona: Merchant Admin
As a Merchant Admin expanding internationally, I want to connect more than one processor and route subscriptions by channel or currency, so that I optimize fees and regional compliance.
Acceptance criteria:
- Given I add a second processor, When I save, Then I can designate one as default and assign others to specific channels or currencies.
- Given a subscription is created on a non-default channel, When scheduling the first charge, Then the charge routes to the channel-assigned processor.
UI states.
<!-- ui-states US-2.6 -->surface: "Admin (React/BigDesign) Payment Settings — the shared 'Connected processors' Panel in apps/admin/src/pages/settings/PaymentSettings.tsx:252-286, viewed as the multi-processor inventory. Persona: Merchant Admin. Today a display-only list (processorLabel + status Badge per connection, listProcessorConnections, processor-connections.ts:77-81); BRD US-2.6 (Phase 2) adds default-designation and channel/currency routing controls that are NOT yet built (see disabled_focus.gaps)."
idle:
render: "Each connected processor renders as a Flex row (processorLabel + optional account_ref + status Badge, lines 268-283). With two or more processors the list shows them all, but there is no 'default' marker, no per-row routing control, and no channel/currency column — the rows are informational only."
primary_action: "None today — the surface is display-only. NORTH-STAR: a 'Set as default' control on one processor and a channel/currency assignment Select on the others (uncited — unbuilt)."
loading:
trigger: "GET /api/v1/admin/processor-connections via listProcessorConnections on mount (loadConnections, lines 174-186)."
render: "While connections === null and there is no error, the Panel shows 'Loading processor connections…' (lines 260-261); no skeleton."
error:
surfaced_at: "Inline BigDesign Message(type='error') at the top of the Panel — 'Failed to load connections: <reason>' (connectionsError, lines 253-259); persistent, not a toast."
render: "The thrown reason from authedFetch (processor-connections.ts:26-39), e.g. 'HTTP 500: <body>'."
recovery: "Reload the page — loadConnections re-runs on mount; there is no inline Retry control today. North-star: a Retry button plus routing-save error handling once the routing controls land."
empty:
render: "With zero processors the Panel shows 'No processor connected yet…' (lines 262-265). With exactly one processor the list renders that single row but the north-star default/routing controls stay hidden — routing is meaningless until a second processor exists."
cta: "Connect a second processor (Stripe US-2.2 / BC Payments US-2.1) to make default-designation and channel/currency routing meaningful."
inputs:
- field: "default_processor"
control: "radio"
allowed_values: "the merchant's connected processors (one selectable as default) — NORTH-STAR, not yet rendered."
- field: "route_by"
control: "select"
allowed_values: "channel | currency — NORTH-STAR routing dimension, not yet rendered."
- field: "currency"
control: "select"
allowed_values: "the store's enabled ISO-4217 currency codes — NORTH-STAR; rendered as a select (never a raw text field) when routing-by-currency is chosen."
edge_status:
- status: "single_processor_connected — only one processor exists"
affordance: "default-designation and routing are moot; the row renders informational-only with a prompt to connect a second processor to unlock routing."
- status: "multiple_processors_no_default — two or more, none marked default (north-star)"
affordance: "the merchant picks one as default (default_processor radio) so unrouted charges have a deterministic target."
- status: "channel_unassigned — a non-default channel/currency has no processor mapping (north-star)"
affordance: "assign the channel/currency to a processor via the routing Select; until then it falls back to the default processor."
- status: "disconnected_or_error — a connection in the list is unhealthy"
affordance: "reconnect that processor (US-2.2/US-2.1); a routed channel whose processor is unhealthy falls back to the default with a warning (north-star)."
disabled_focus:
keyboard: "Today the list renders no interactive controls (static Badge/Text rows), so there are no tab stops here; merchants reach the connect/auto-detect Buttons in the sibling panels. NORTH-STAR: the default-designation radios and routing Selects must be real BigDesign Radio/Select in tab order (never div-onClick), each labeled via FormGroup."
focus_move: "No dynamic reveal today. North-star: when routing controls save, surface the result in an aria-live region scoped to the affected row."
guard: "Read-only today — no confirm needed. North-star: changing the default processor reroutes future charges, so confirm + announce the change; the action is non-destructive (applies to future scheduling only)."
gaps: "BRD US-2.6 is Phase 2 — the entire routing half is unbuilt: AC-1 ('designate one as default and assign others to specific channels or currencies') has NO controls; the list (lines 252-286) is display-only. AC-2 (first charge routes to the channel-assigned processor) is backend scheduling, not surfaced here. The default_processor / route_by / currency inputs above are the north-star contract and are intentionally uncited (no code yet)."
US-2.7: Capture timing advisory at processor onboarding
<!-- traceability:start:US-2.7 --><!-- traceability:end:US-2.7 -->Prototype: Capture-timing advisory
Phase: Phase 1 (advisory) · Phase 2 (enforced) · Priority: P1 · Effort: XS · Persona: Merchant Admin
As a Merchant Admin completing processor onboarding, I want to be informed about capture timing options early in setup, so that I can choose the right capture mode for my fulfillment model before subscriptions go live.
Acceptance criteria:
- Given I complete processor onboarding (US-2.1 or US-2.2), When I reach the final setup-checklist step, Then a capture timing summary card is shown with current setting (
Immediateby default), a link to Payment Settings, and a Phase 1 advisory note: "Deferred capture (On fulfillment / On ship) is configurable today but takes effect in Phase 2." - Given my store region is EU, When the advisory card is shown, Then it includes a highlighted compliance note: "EU regulations require capture at shipment for physical goods — select On ship capture when Phase 2 ships."
- Given I click the capture timing card, When it opens, Then I am taken to the Payment Settings capture timing section (US-10.9).
- Given I dismiss or skip the card, When I return to the setup checklist, Then the capture timing item is marked "Configured: Immediate (Phase 2 advisory)" so I know it was seen.
Data contract.
- Reads
stores.capture_timingfrom the existing column (migration 0019, ADR-0038 Phase 1). - No new API calls — reads from the same store settings endpoint as US-10.9.
- Events: none (advisory display only).
Dependencies.
- Upstream: US-2.1 / US-2.2 (processor connected — prerequisite to showing the card).
- Downstream: US-10.9 (capture timing configuration — the target of the deep-link).
Cross-references. ADR-0038 (capture timing strategy — canonical spec); US-10.9 (full capture timing config story in Epic 10); synthesis #840.
UI states.
<!-- ui-states US-2.7 -->surface: "Admin (React/BigDesign) Payment Settings — the region-aware capture-timing advisory rendered by the RegionAdvisory component (apps/admin/src/pages/settings/PaymentSettings.tsx:61-111), shown inside the 'Capture Timing' Panel (rendered at line 451, gated on !loading). Persona: Merchant Admin. NOTE BRD US-2.7 AC-1 places this advisory at the final setup-checklist step after processor onboarding; the built advisory lives ONLY in standalone Payment Settings (see disabled_focus.gaps)."
idle:
render: "After settings load, RegionAdvisory renders a region-appropriate BigDesign Message: for EU stores a warning 'EU Compliance Advisory' (lines 68-87) telling the merchant to select On Ship; for US stores an info 'FTC Best-Practice Advisory' (lines 89-108). Region is derived from settings.country_code via detectRegion (lines 53-59) against EU_COUNTRY_CODES (lines 45-49). Above it, a static Phase-1 'advisory only' Message (lines 440-448) notes deferred capture activates in Phase 2."
primary_action: "The advisory is informational; it points the merchant at the capture-timing Select below (US-10.9) to change the mode."
loading:
trigger: "GET /api/v1/admin/store/settings via getStoreSettings on mount (lines 161-172)."
render: "While loading === true the advisory is not rendered at all — RegionAdvisory is gated on {!loading && …} (line 451) — so no region message flashes before country_code is known; the static Phase-1 advisory Message still shows."
error:
surfaced_at: "Inline BigDesign Message(type='error') near the bottom of the page — 'Failed to load store settings: <reason>' (loadError, lines 409-414)."
render: "The thrown reason from getStoreSettings (stores.ts:29-44), e.g. 'HTTP 500: <body>'."
recovery: "GAP — on a settings-load failure settings stays null, region defaults to 'other' (region const, line 241), and RegionAdvisory returns null (line 110): an EU merchant silently sees NO compliance advisory. The loadError Message has no Retry; recovery today is a page reload. North-star: a Retry + a neutral 'region unknown — choose On Ship for EU' fallback advisory (see gaps)."
empty:
render: "Not a list surface — a single advisory card. When region resolves to 'other' (non-EU/non-US, or country_code absent) RegionAdvisory returns null (line 110) and only the static Phase-1 advisory Message (lines 440-448) shows; there is no region-specific card."
cta: "n/a — single advisory, not a collection."
edge_status:
- status: "eu_not_on_ship — EU store with capture_timing != on_ship"
affordance: "warning Message (lines 68-80) instructs selecting 'On Ship'; the merchant changes the capture-timing Select below (US-10.9) and Saves."
- status: "eu_on_ship — EU store already on On Ship"
affordance: "confirmation Message (lines 80-82) states the store is aligned with EU compliance; informational — no change needed (enforcement activates Phase 2)."
- status: "us_immediate — US store on Immediate capture"
affordance: "FTC best-practice info Message (lines 96-102) suggests considering On Ship; the merchant may change the capture-timing Select (US-10.9)."
- status: "region_unknown — country_code absent or non-EU/non-US"
affordance: "RegionAdvisory renders nothing (line 110). NORTH-STAR: show a neutral capture-timing advisory + a link to confirm the store region, so an EU merchant with a missing country_code is not silently left without guidance (see gaps)."
disabled_focus:
keyboard: "The advisory card itself is a presentational BigDesign Message with no interactive controls, so it adds no tab stops; the actionable control it references (the capture-timing Select, US-10.9) is a real BigDesign Select reachable via Tab with arrow-key option selection."
focus_move: "The advisory renders / does-not-render based on region with no aria-live region and no focus move; a screen-reader merchant is not announced when the EU/US advisory appears. PaymentSettings.tsx imports no useRef and calls no .focus()."
guard: "Informational surface — no destructive action, no confirm. The advisory is read-only guidance toward the capture-timing setting."
gaps: "route-orphan DEFECT — BRD AC-1 (Phase 1) requires the advisory at the final setup-checklist step; SetupChecklist.tsx (apps/admin/src/pages/onboarding/SetupChecklist.tsx) has NO capture-timing ChecklistItem, so a merchant who finishes onboarding and never opens Payment Settings never sees the advisory. silent-failure DEFECT — detectRegion falls back to 'other' when country_code is missing (line 54), so an EU store with an absent country_code (or a settings-load error) is shown NO compliance warning. AC-4 (checklist item marked 'Configured: Immediate (Phase 2 advisory)') is unbuilt. North-star: render the advisory card on the setup checklist and default unknown-region EU-shipping stores to the warning."