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 28 — Compliance, trust & audit (derived view)
Read-only per-epic slice of
BRD.md§9, lines 11704–12078. The canonical source of truth isBRD.md— edit there, never here. The stable address for a story is its US-ID (US-28.x), not a line number. Regenerates on everydev → mainsync viaderive-state-on-main.
- Stories (7): US-28.1, US-28.2, US-28.3, US-28.4, US-28.5, US-28.6, US-28.7
- Generated: 2026-07-01T17:48:39.076Z · as-of commit:
b083f095
Epic 28 — Compliance, trust & audit
<!-- traceability:start:BRD:Epic-28 --><!-- traceability:end:BRD:Epic-28 -->Prototype: Data Rights · PCI Scope · Consent Log · Audit Reports
Value: The platform meets regulatory, privacy, and audit requirements for mid-market and enterprise.
US-28.1: GDPR data subject access request (DSAR)
<!-- traceability:start:US-28.1 --><!-- traceability:end:US-28.1 -->Prototype: Data Rights
Phase: MVP · Priority: P0 · Effort: M · Persona: Subscriber / Merchant Admin
As a Subscriber, I want to request a copy of all my data, so that I exercise my GDPR rights. As a Merchant Admin, I want to fulfill that request in one click.
Acceptance criteria:
- Given a DSAR is submitted (via portal or merchant-initiated), When processed, Then a downloadable JSON + CSV bundle is generated within 30 days and delivered via secure link.
Data contract.
- Subscriber-initiated: request from portal → generates workflow job
- Merchant-initiated: admin UI to enter customer email → same job
- Job output: JSON + CSV bundle containing subscriber-scoped data from all tables (subscriptions, charges, events, addresses, notes), encrypted with a passphrase, uploaded to Vercel Blob, expiring link emailed
Success metrics.
- Functional: bundle delivered within 30 days (regulatory requirement)
- Product (target): most DSARs fulfilled automatically; merchant interventions < 5%
- Operational: zero leakage (bundle link expiry + passphrase gating)
Dependencies.
- US-13.1 (event log for DSAR completeness)
Non-functional.
- Bundle includes all subscriber data including Event payloads — redaction only for merchant-internal fields (support notes)
UI states.
<!-- ui-states US-28.1 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) compliance panel — DSAR submission and fulfillment view at /admin/compliance/dsar. Persona: Merchant Admin (fulfilling data subject requests). Subscriber-initiated portal requests route to the same backend job; this contract covers the admin fulfillment surface only."
idle:
render: "Page titled 'Data Subject Access Requests' with a short regulatory context note ('GDPR Article 15 — fulfill within 30 days'). A FormGroup with a customer-email Input (type=email, label 'Customer email', placeholder 'shopper@example.com') and a 'Request data export' primary Button. A 'Recent requests' section below lists submitted DSARs with columns: customer email, requested date, status badge (pending / ready / expired), and a 'Download bundle' link for ready requests."
primary_action: "'Request data export' — disabled until the email field contains a valid email shape."
loading:
trigger: "POST /api/v1/admin/compliance/dsar with {customer_email}"
render: "The 'Request data export' Button shows its isLoading spinner with label 'Submitting…'; the email Input and Button are disabled. The recent-requests list remains static."
error:
surfaced_at: "Inline — a BigDesign Message(type='error') rendered directly below the email Input and above the Button, scoped to the submit action; never a toast."
render: "API error detail string — e.g., 'No customer found for that email' (404), 'A DSAR for this customer is already in progress' (409)."
recovery: "The Button re-enables and the email Input remains populated. For a duplicate-in-progress error (409), the existing request row in the recent-requests list is the action point — the rep monitors its status there."
empty:
render: "When no prior DSAR requests exist for this store, the 'Recent requests' section shows a BigDesign Text component with caption 'No data access requests yet — requests you submit will appear here.'"
inputs:
- field: "customer_email"
control: "email"
label: "'Customer email' — BigDesign Input type=email in a FormGroup"
validation: "Valid email shape required on the client; resolved server-side to a store-scoped subscriber record."
edge_status:
- status: "bundle_ready — the DSAR job completed and the encrypted bundle is available for download"
affordance: "A 'Download bundle' link (real <a> element with expiry-date tooltip) appears in the recent-requests row; clicking it downloads the encrypted JSON + CSV bundle."
- status: "bundle_expired — the secure download link has passed its expiry window"
affordance: "A 'Re-trigger export' Button in the request row re-queues the DSAR job for the same customer, generating a fresh expiring link."
- status: "customer_not_found — email does not match any subscriber in this store"
affordance: "Submit error Message instructs the rep to verify the email spelling or confirm the subscriber exists in BC before re-submitting."
disabled_focus:
keyboard: "The customer-email Input and 'Request data export' Button are real BigDesign components wrapping native focusable elements — no div-onClick. Tab order: customer-email Input → 'Request data export' Button. Download links and Re-trigger Buttons in the recent-requests list are real <a> and <button> elements reachable in tab order. The primary Button is activatable with Enter or Space; its native disabled state removes it from tab order until a valid email is present."
US-28.2: GDPR/CCPA erasure
<!-- traceability:start:US-28.2 --><!-- traceability:end:US-28.2 -->Prototype: PCI Scope
Phase: MVP · Priority: P0 · Effort: L · Persona: Subscriber / Merchant Admin
As a Subscriber, I want my data erased on request (with subscription cancellation), so that my personal info is not retained.
Acceptance criteria:
- Given an erasure request, When processed, Then all PII is deleted/anonymized within 30 days; financial records are retained with PII stripped per legal basis.
Data contract.
- On request: subscription state preserved (cancelled), PII scrubbed (email → hashed, name → "Erased User", addresses → null)
- Financial records preserved with PII stripped (for legal 7-year retention)
- Processor PM references deleted (adapter calls processor-side deletion)
- Irreversible confirmation: "This cannot be undone" + typed confirmation
Success metrics.
- Functional (statutory — GDPR/DSAR 30-day deadline): complete within 30 days
- Operational: zero re-surfacing of erased subscriber PII in any system (verified by post-erasure search)
Dependencies.
- Processor adapter's customer-data deletion APIs
Non-functional.
- Erasure job logged separately in an immutable audit log (even when event rows are stripped)
UI states.
<!-- ui-states US-28.2 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) compliance panel — erasure request form at /admin/compliance/erasure. Persona: Merchant Admin (fulfilling a GDPR/CCPA right-to-erasure request). Irreversible destructive action requiring typed confirmation."
idle:
render: "Page titled 'Erase subscriber data' with a BigDesign Message(type='error') acting as a persistent warning banner: 'This action permanently deletes all PII. Financial records are retained with PII stripped for legal compliance. This cannot be undone.' A FormGroup with a customer-email Input (type=email, label 'Customer email') and a second typed-confirmation Input (type=text, label 'Type exactly: This cannot be undone', placeholder 'This cannot be undone'). An 'Erase subscriber data' destructive-variant Button is disabled until both fields pass their gates."
primary_action: "'Erase subscriber data' — enabled only when the email field contains a valid email shape AND the confirmation Input's trimmed value equals the string 'This cannot be undone' (case-sensitive)."
loading:
trigger: "POST /api/v1/admin/compliance/erasure with {customer_email, confirmed: true}"
render: "The 'Erase subscriber data' Button shows isLoading with label 'Erasing…'; both Inputs and the Button are disabled for the duration of the job."
error:
surfaced_at: "Inline — a BigDesign Message(type='error') beneath the typed-confirmation Input and above the Button; never a toast. Scoped to the submit action."
render: "API error detail — e.g., 'No subscriber found for that email' (404), 'Erasure already in progress for this subscriber' (409), 'Processor-side deletion failed — re-submit to retry' (502)."
recovery: "Both Inputs re-enable with prior values intact. For a 409 the rep waits for the running job. For a 502 (processor failure), PII has already been scrubbed from our DB — the rep re-submits to retry the processor-side deletion step only."
empty:
render: "Not a list surface — a single-action destructive form. The typed-confirmation guard ('This cannot be undone') is the idle-state safeguard against accidental activation; no separate empty state applies."
inputs:
- field: "customer_email"
control: "email"
label: "'Customer email' — BigDesign Input type=email in FormGroup"
validation: "Valid email shape on client; resolved server-side to a store-scoped subscriber record."
- field: "typed_confirmation"
control: "text"
label: "'Type exactly: This cannot be undone' — BigDesign Input type=text in FormGroup"
allowed_values: ["This cannot be undone"]
edge_status:
- status: "processor_deletion_failed — adapter's customer-data deletion API returned non-2xx after PII scrub completed"
affordance: "Submit error Message (502) surfaces the partial state and instructs re-submission to retry the processor-side step only; PII has already been scrubbed from our DB."
- status: "erasure_already_in_progress — a concurrent erasure job is running for this subscriber"
affordance: "Submit error Message (409) informs the rep that the in-progress job will complete the erasure; no additional action required."
- status: "subscriber_not_found — email does not match any store-scoped subscriber"
affordance: "Submit error Message (404) instructs the rep to verify the email or confirm the subscriber has not already been erased — in which case the GDPR obligation is already satisfied."
disabled_focus:
keyboard: "The customer-email Input, typed-confirmation Input, and 'Erase subscriber data' Button are real BigDesign components wrapping native focusable elements — no div-onClick. Tab order: customer-email Input → typed-confirmation Input → 'Erase subscriber data' Button. The destructive Button is reachable via Tab and activatable with Enter or Space only after both inputs are valid; native disabled removes it from tab order until the confirmation string matches exactly."
US-28.3: Audit log export
<!-- traceability:start:US-28.3 --><!-- traceability:end:US-28.3 -->Prototype: Consent Log
Phase: P2 · Persona: Merchant Admin
As a Merchant Admin preparing for SOC 2 audit, I want to export a full audit log for any time range, so that auditors can review.
Acceptance criteria:
- Given I request an export for a date range, When it generates, Then every Event row in that range is included with actor, action, subject, before/after.
- Given a charge in the date range, When exported, Then the row carries
mit_subtype(recurring/unscheduled/installment),chain_position(initial/subsequent), andactor_user_id(oractor_kindfor system-initiated) — sufficient for SCA dispute defense and DSAR scope.
UI states.
<!-- ui-states US-28.3 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) compliance panel — audit log export at /admin/compliance/audit-log. Persona: Merchant Admin (SOC 2 audit preparation). Phase P2."
idle:
render: "Page titled 'Audit log export' with subtitle 'Export all system events for a date range — each row includes actor, action, subject entity, before/after state, MIT subtype, and chain position.' A FormGroup with two date Inputs (label 'From date', type=date; label 'To date', type=date, max=today). An 'Export audit log' primary Button. A 'Recent exports' section lists prior export jobs with a status badge (generating / ready / expired) and a 'Download CSV' link for ready exports."
primary_action: "'Export audit log' — disabled until both date Inputs are filled and the From date is not after the To date."
loading:
trigger: "POST /api/v1/admin/compliance/audit-log/export with {from_date, to_date}"
render: "The 'Export audit log' Button shows isLoading with label 'Queuing export…'; both date Inputs and the Button are disabled. The recent-exports list gains a new 'generating' row once the API responds."
error:
surfaced_at: "Inline — a BigDesign Message(type='error') beneath the date Inputs and above the Button; never a toast. Scoped to the submit action."
render: "API error detail — e.g., 'Date range exceeds 90-day maximum — narrow your selection' (400), or 'Export generation failed — please retry' (502)."
recovery: "Both date Inputs and the Button re-enable. For a range-too-large error the rep narrows the date range and resubmits. For a 502 the rep retries; the failed export row in the recent-exports list shows a 'Retry' Button."
empty:
render: "When no events exist in the selected date range, the export job completes with a CSV containing only the header row. The recent-exports row shows a 'ready' badge with a 'Download CSV' link and a helper note: 'No events found for this date range — the file contains column headers only.' The rep can still download it for auditor documentation."
inputs:
- field: "from_date"
control: "date"
label: "'From date' — BigDesign Input type=date in FormGroup"
validation: "Required; must not be after To date."
- field: "to_date"
control: "date"
label: "'To date' — BigDesign Input type=date in FormGroup; max=today"
validation: "Required; must not be before From date; capped at today."
edge_status:
- status: "export_generating — async job is building the CSV from the event log"
affordance: "The recent-exports row shows a 'Generating…' status badge; the row polls for completion and promotes to 'ready' without a page reload."
- status: "export_ready — CSV is built and the download link is active"
affordance: "A 'Download CSV' link (real <a> element) in the recent-exports row delivers the file; each row in the export carries actor, action, subject entity, before/after, mit_subtype, chain_position, and actor_user_id."
- status: "export_expired — the secure download link has passed its expiry window"
affordance: "A 'Re-export' Button in the recent-exports row re-queues the job for the same date range without requiring the rep to re-enter dates."
- status: "date_range_invalid — From date is after To date"
affordance: "Client-side validation blocks submit and an inline error beneath the date Inputs reads 'From date must be before To date.' — the Button remains disabled until the dates are corrected."
disabled_focus:
keyboard: "The From date Input, To date Input, and 'Export audit log' Button are real BigDesign components wrapping native focusable elements — no div-onClick. Tab order: From date → To date → 'Export audit log' Button. Download links and Retry Buttons in the recent-exports list are real <a> and <button> elements in tab order. The primary Button is activatable with Enter or Space; native disabled removes it from tab order until both dates are valid."
US-28.4: PCI DSS scope maintenance
<!-- traceability:start:US-28.4 --><!-- traceability:end:US-28.4 -->Prototype: Audit Reports
Phase: MVP · Priority: P0 · Effort: M · Persona: Developer / Merchant Admin
As the platform, I want to maintain PCI SAQ-A eligibility, so that merchants don't inherit elevated compliance scope from using us.
Acceptance criteria:
- Given any user flow involves card data, When implemented, Then card data is collected only by the processor's hosted element and never touches our servers or DB.
Architecture requirement. Our servers never receive PAN, CVV, or full card data. Processor-hosted elements (Stripe Elements, Braintree Hosted Fields) tokenize card data in the browser; we receive only tokens. Our DB stores only payment_method_ref (an opaque processor token).
Verification.
- Code review checklist: no
card_number,cvv,full_panfields anywhere in our schema or API - Pre-commit hook: grep for disallowed patterns
- Annual PCI SAQ-A self-assessment
Success metrics.
- Functional: PCI SAQ-A eligibility maintained; no scope expansion events
- Operational: zero card-data incidents in audit logs
Dependencies.
- All charge flows must use BC's stored-instruments vault rail (ADR-0037); Stripe-direct edge case uses Stripe Elements. No raw card data enters our application.
UI states.
<!-- ui-states US-28.4 -->surface: "NOT YET BUILT — forward-looking contract. No interactive admin panel — US-28.4 is a code-review and architectural constraint with no user-facing UI surface. The enforcement surface is the developer's commit workflow (pre-commit grep hook) and the annual PCI SAQ-A self-assessment checklist. Persona: Developer / Merchant Admin (the human actor is the developer authoring code and the operator performing the annual assessment)."
idle:
render: "No user-facing UI component renders for this AC. PCI SAQ-A scope is maintained by three non-visual controls: (1) a pre-commit grep hook that rejects any commit introducing card_number, cvv, or full_pan in schema migrations, API handlers, or client code; (2) a code-review checklist item on every PR touching payment or storage paths; (3) an annual SAQ-A self-assessment. The clean in-scope state is the codebase containing zero occurrences of disallowed card-data field names — the enforcement is structural, not a rendered panel."
primary_action: "Commit — the pre-commit hook runs as the enforcing gate; a clean commit is the happy-path result."
loading:
trigger: "git commit on any path containing schema, migration, or payment-handler files"
render: "The pre-commit hook runs synchronously in the developer's terminal. There is no async loading state and no user-facing spinner; the hook exits 0 (pass) or non-zero (violation) in under one second."
error:
surfaced_at: "Developer's terminal at commit time — the pre-commit hook prints a scope-expansion violation to stderr naming the offending filename and matched pattern (card_number, cvv, or full_pan). No admin panel surfaces this; the gate is at the codebase level."
recovery: "Remove or rename the offending field so it holds only a processor-opaque token (e.g., payment_method_ref), then recommit. The hook exits 0 when no disallowed patterns are present."
empty:
render: "The clean in-scope state is the codebase containing zero occurrences of card_number/cvv/full_pan. The pre-commit hook exits silently — that silence is the passing condition. There is no list to show empty; the absence of violations is the rendered result."
disabled_focus:
keyboard: "No interactive controls render in this AC's scope; PCI SAQ-A compliance is enforced at the codebase and CI layer, not a tab-order UI. Card collection happens only in the processor-hosted iframe (Stripe Elements or Braintree Hosted Fields), which owns its own focus and tab order — our application renders no card-data inputs to place in tab order. Accessibility requirements do not apply to this AC; the relevant PCI assurance is that our surfaces never host card-data inputs."
US-28.5: SOC 2 Type II readiness
Phase: P2 · Persona: Developer
As a Developer/SRE, I want SOC 2 Type II evidence to flow automatically from our controls, so that we achieve audit readiness without manual evidence hunting.
Acceptance criteria:
- Given SOC 2 controls are defined, When operations run (access reviews, backup tests, incident drills), Then evidence is captured and stored in our compliance tool (e.g., Vanta).
US-28.6: Data residency
Phase: P2 · Persona: Merchant Admin (EU)
As a Merchant Admin in the EU, I want subscriber data to stay in EU regions, so that I meet data-residency obligations.
Acceptance criteria:
- Given a store declares
region: eu, When data is stored, Then Postgres and Redis primaries are EU-region.
UI states.
<!-- ui-states US-28.6 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) compliance settings — Data Residency tab at /admin/settings/compliance/data-residency. Persona: Merchant Admin (EU). Phase P2."
idle:
render: "Page titled 'Data Residency' with a BigDesign Message(type='info') explaining: 'Changing your data residency region routes Postgres and Redis primaries to the selected region. A short migration window applies after saving.' A FormGroup with a 'Data residency region' BigDesign Select (label 'Data residency region'; options: 'Global (US-East)' and 'European Union (EU-West)'). A read-only 'Current region' status line below the Select showing the active routing region. A 'Save region setting' primary Button."
primary_action: "'Save region setting' — enabled only when the selected option differs from the currently-saved region."
loading:
trigger: "PUT /api/v1/admin/settings/data-residency with {region: 'eu' | 'global'}"
render: "The 'Save region setting' Button shows isLoading with label 'Saving…'; the region Select and Button are disabled."
error:
surfaced_at: "Inline — a BigDesign Message(type='error') beneath the region Select and above the Button; never a toast."
render: "API error detail — e.g., 'Region migration already in progress — retry in a few minutes' (409), or 'EU data residency is not available on this account plan' (403)."
recovery: "The Select and Button re-enable with the previously-saved region restored as the selected value. For a 409 the rep waits and retries; for a 403 the rep contacts support to unlock EU routing for their account."
empty:
render: "When no region has been configured, the Select shows its default placeholder ('Select a region') and the 'Current region' status line reads 'Not configured — defaults to Global (US-East).' The rep selects a region before the Save Button enables."
inputs:
- field: "region"
control: "select"
label: "'Data residency region' — BigDesign Select in FormGroup"
allowed_values: ["global", "eu"]
edge_status:
- status: "region_migration_in_progress — a prior region-change is still being applied to the infrastructure"
affordance: "A status Badge below the Select reads 'Migration in progress…'; the Save Button is disabled and a help note reads 'Region change is being applied — this may take a few minutes. The page will re-enable when migration completes.'"
- status: "eu_tier_locked — the merchant's plan does not include EU data residency"
affordance: "Submit error Message (403) reads 'EU data residency requires an upgraded plan — contact support.' The Select reverts to the current saved region."
- status: "region_saved — region persisted and infrastructure routing updated"
affordance: "A BigDesign Message(type='success') confirms 'Data residency updated to European Union (EU-West).' The 'Current region' status line updates immediately without a page reload."
disabled_focus:
keyboard: "The region Select and 'Save region setting' Button are real BigDesign components wrapping native focusable elements — no div-onClick. Tab order: region Select (arrow keys to choose between Global and EU-West) → 'Save region setting' Button. The Button is reachable via Tab and activatable with Enter or Space. Native disabled removes the Button from tab order when the value matches the saved value or a migration is in progress."
US-28.7: Auto-renewal disclosure policy & consent-record retention
Phase: P1 · Priority: P0 · Effort: M · Persona: Merchant Admin / Compliance
As a Merchant, I want a defined auto-renewal compliance posture and a durable, auditable store of every consent and disclosure record, so that I can demonstrate negative-option-law compliance on demand and survive an enforcement inquiry or dispute.
Compliance note (research red team S3, synthesis #1822, ADR-0079). Epic 28 covered GDPR/CCPA/PCI/SOC2 but had no negative-option / cancellation-law surface — the highest-severity omission in the audit. This story adds the retention + posture half (US-9.6 adds the capture half). Federal ROSCA (15 U.S.C. §8403) + state ARLs (e.g., CA AB 2863, effective 2025-07-01) require retained proof of disclosure and affirmative consent. Final legal sufficiency is counsel-gated — ADR-0079 captures the posture; the sign-off lives as an attestation (
docs/attestations/), not asserted here.
Acceptance criteria:
- Given any subscription is created with captured consent (US-9.6), When it is stored, Then a consent record persists: subscriber, subscription, the exact disclosed terms, the disclosure surface + version, the affirmative-consent timestamp, and the actor — append-only and immutable.
- Given an auto-renewal reminder is sent (US-23.18) or a material-change notice is issued, When it dispatches, Then the send is retained against the subscription as part of the same compliance record.
- Given a regulator inquiry, dispute, or DSAR (US-28.1), When the merchant requests a subscription's compliance history, Then the full consent + disclosure + reminder trail is exportable with integrity (tamper-evident, retained for the legally-required period).
- Given the merchant configures the compliance posture, When they set covered jurisdictions and disclosure/reminder defaults, Then those settings derive from ADR-0079's posture and cannot be set below the compliant floor.
UX notes.
- Surface: admin compliance settings + a per-subscription "Compliance record" view in the merchant dashboard
- The record view shows the exact terms the subscriber saw, when, and how they consented — readable, not raw JSON
- Export is one action, suitable for handing to counsel or a regulator
Data contract.
- Entity:
consent_records(append-only):subscription_id,subscriber_id,disclosed_terms,disclosure_surface,disclosure_version,consented_at,actor,channel - Linked: reminder/notice sends (US-23.18) keyed to the same
subscription_id - Retention: configurable per jurisdiction, defaulting to the longest legally-required period; records survive erasure of marketing PII under the legal-basis carve-out (consistent with US-28.2 financial-record retention)
- No new BC platform primitive — an internal compliance store
Success metrics.
- Functional: 100% of created subscriptions have a retrievable consent record (a subscription without one is a compliance defect)
- Operational (target): a single subscription's full compliance trail exports in under 5s
- Product (target): zero enforcement/dispute outcomes lost for want of a retained consent or disclosure record (the record exists whenever it is asked for)
Dependencies.
- US-9.6 (consent capture — the write side)
- US-23.18 (reminder sends — also retained)
- US-28.1 / US-28.2 (DSAR export + erasure carve-out alignment)
- US-28.3 (audit log export — the compliance trail rides the same export surface)
- ADR-0079 (posture); counsel attestation in
docs/attestations/for final legal sufficiency
Non-functional.
- Consent records are append-only and tamper-evident; no update or delete path exists outside the legal-retention expiry
- The compliance store is in scope for the SOC 2 + audit-log controls (US-28.3 / US-28.5)
Risks / open questions.
- Per-jurisdiction retention periods differ — ADR-0079 sets the platform default to the longest covered; counsel confirms the set
- The boundary between consent-record retention (kept) and DSAR erasure (PII removed) needs a documented legal-basis carve-out — aligns with US-28.2
UI states.
<!-- ui-states US-28.7 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) — two linked panels: (1) Compliance Settings page at /admin/settings/compliance with jurisdiction configuration and disclosure defaults; (2) per-subscription 'Compliance record' tab at /admin/subscriptions/{id}/compliance showing the full consent + disclosure + reminder trail with one-click export. Persona: Merchant Admin / Compliance."
idle:
render: "Compliance Settings page: a 'Jurisdictions covered' section with one row per configured jurisdiction — each row carries a jurisdiction Select, a disclosure-mode Select, and a 'Require auto-renewal reminder' Checkbox. A floor note beneath each row reads 'Cannot be set below the legal minimum for this jurisdiction.' A 'Save compliance settings' Button at the bottom. Compliance record tab: a 'Compliance record' panel with a structured view of the consent record (disclosed terms, disclosure surface, disclosure version, consented_at, actor, channel) followed by a chronological list of linked reminder/notice sends. An 'Export compliance trail' Button in the panel header."
primary_action: "Settings: 'Save compliance settings' — enabled when any field differs from the saved values. Record tab: 'Export compliance trail' — enabled whenever a consent record exists for the subscription."
loading:
trigger: "Settings: PUT /api/v1/admin/settings/compliance. Record tab: GET /api/v1/admin/subscriptions/{id}/compliance on mount; GET /api/v1/admin/subscriptions/{id}/compliance/export on export click."
render: "Settings: Save Button shows isLoading with label 'Saving…'; all Selects, Checkboxes, and the Button are disabled. Record tab: a BigDesign Spinner covers the consent record list on initial load; Export Button shows isLoading with label 'Exporting…' and is disabled while the file generates."
error:
surfaced_at: "Inline — a BigDesign Message(type='error') beneath the offending form section (settings) or at the top of the compliance record panel (record tab); never a toast. Scoped to the failing action."
render: "API error detail — e.g., 'Disclosure mode cannot be set below the legal floor for CA AB 2863' (400) on settings save; 'Export generation failed — retry' (502) on export."
recovery: "Settings: the form re-enables; for a floor-violation the offending Select is highlighted and a BigDesign helper-text note names the minimum-compliant option and the statute it derives from. Record tab: an inline 'Retry' link re-triggers the failed load or export."
empty:
render: "Record tab: when a subscription has no consent record, a BigDesign Message(type='warning') reads 'No consent record found for this subscription — this is a compliance defect. This subscription may predate the consent-capture requirement or was created manually without a consent step. File a manual attestation or contact support.' The Export Button is disabled until a consent record exists."
inputs:
- field: "jurisdiction"
control: "select"
label: "'Jurisdiction' — BigDesign Select per row in the Jurisdictions covered section"
allowed_values: ["us_federal_rosca", "ca_arl", "eu_gdpr", "uk_cra"]
- field: "disclosure_mode"
control: "select"
label: "'Disclosure mode' — BigDesign Select per jurisdiction row"
allowed_values: ["mandatory_pre_signup", "mandatory_pre_charge", "reminder_only"]
- field: "reminder_required"
control: "checkbox"
label: "'Require auto-renewal reminder' — BigDesign Checkbox per jurisdiction row"
edge_status:
- status: "consent_record_missing — subscription has no captured consent record (compliance defect)"
affordance: "BigDesign Message(type='warning') on the compliance record tab; a 'File manual attestation' Button is offered for pre-capture-era subscriptions to document the basis retroactively."
- status: "floor_violation — configured disclosure mode is below the legal minimum for a jurisdiction"
affordance: "Save is blocked; a BigDesign Message(type='error') beneath the offending Select names the statute and the minimum-compliant option derived from ADR-0079."
- status: "reminder_send_missing — a required auto-renewal reminder was due but no send is on record"
affordance: "A BigDesign Message(type='warning') in the compliance record tab notes the missed send date; a 'Send now' Button dispatches the overdue reminder and retains the send against the subscription's compliance record."
- status: "export_ready — compliance trail exported as a tamper-evident file"
affordance: "A 'Download' link (real <a> element) appears inline in the compliance record panel header after export completes; clicking it downloads the file suitable for handing to counsel or a regulator."
disabled_focus:
keyboard: "All Selects, Checkboxes, and Buttons in the settings page are real BigDesign components wrapping native focusable elements — no div-onClick. Tab order in the settings page follows DOM source order across jurisdiction rows: jurisdiction Select → disclosure-mode Select → reminder-required Checkbox → next row → 'Save compliance settings' Button. In the compliance record tab, the 'Export compliance trail' Button and any 'File manual attestation' or 'Send now' Buttons are real <button> elements in tab order; Download links are real <a> elements. Focus does not move on successful save — a BigDesign Message(type='success') announces the change without stealing focus from the form."