Capabilities, by iteration
Source: docs/audits/derived/_state.json (derived by tools/state-derive from BRD + PRD + ADR catalogues).
Grouped by phase, then category. Every row maps to a real BRD acceptance criterion or ADR commitment.
Phase unassigned
419 capabilities · 8 categories
capability
344 rows- compliant
shipped-b2b-edition-detectionB2B Edition support — variant detection + admin enrollment (Epic 22, called "Phase 2 planned" by stale audit)
PR #547 (admin enrollment) + PR #548 (storefront detection)
- compliant
shipped-dsar-endpointDSAR endpoint — per-customer export + erasure (Epic 28, called "Phase 2 planned" by stale audit)
PR #546
- compliant
shipped-bigeng-slices-bcdBigEng Slices B/C/D shipped — typed errors + schema retrofit + audit decorator (called "in flight" by stale audit)
PRs #542 (B) + #543 (C) + #545 (D)
- compliant
shipped-phase-2-gcp-scaffoldPhase 2 GCP Terraform scaffold (called "Phase-2 candidate / future" by stale audit; in flight now)
PR #561
- compliant
idempotency-keys-tableidempotency_keys ledger table for request-level idempotency (Hive #581)
Hive #581
- compliant
idempotency-middlewareMiddleware reads Idempotency-Key header, caches mutation responses, returns 409 on fingerprint mismatch
Hive #581
- compliant
openapi-spec-existsOpenAPI 3.0 spec at apps/api/openapi.yaml (Hive #580)
Hive #580
- compliant
openapi-docs-route-existsGET /docs Swagger UI handler exists (Hive #580) — route mount in worker.ts required
Hive #580
- compliant
idempotency-cleanup-cron24h TTL cleanup cron for idempotency_keys table
Hive #581
- compliant
openapi-drift-ci-gateCI gate validates OpenAPI spec on PRs touching API or spec (Hive #580)
Hive #580
- compliant
schema-charges-tablecharges table — first-class entity for each subscription billing attempt
BRD.md §Epic-10 (charge scheduling), §Epic-11 (dunning), §Epic-12 (refunds)
- compliant
schema-customers-tablecustomers table — mirror of BC customers with subscription-specific fields (payment_customer_ref, consent_metadata)
BRD.md §Epic-1 (BC customer identity), §Epic-19 (PM management depends on customers FK)
- compliant
schema-payment-methods-tablepayment_methods table — gateway-tokenized PMs with NTI (Network Transaction ID) for MIT credential continuity
BRD.md §Epic-19 (subscriber PM management), §D17 PRD-COMPANION (MIT/NTI)
- compliant
schema-reconciliation-runs-tablereconciliation_runs table — observability for the periodic reconcile sweep
BRD.md §Epic-13 (reconciliation), migration 0013
- compliant
dependabot-configDependabot configured for weekly dependency updates across all workspaces
Hive #582 / test-stack-baseline skill
- compliant
gitleaks-workflowGitleaks CI workflow scans every PR for secrets; allowlist documented
Hive #582 / test-stack-baseline skill
- compliant
changelog-md-existsCHANGELOG.md exists at repo root
Hive #586
- compliant
changelog-on-release-workflowWorkflow auto-generates changelog drafts on tag push
Hive #586
- compliant
brd-us-1-1-oauth-stores-rowSuccessful OAuth callback creates stores row with encrypted access token and emits store.installed
BRD.md §US-1.1: Given I click "Install" from the BC Marketplace, When BC redirects with code/scope/context, Then we exchange the code for an access token and persist encrypted credentials
- manual review
brd-us-1-1-oauth-failure-messageOAuth handshake failure shows descriptive error message; BC does not show app as installed. SetupError admin page surfaces partial-failure recovery for the post-OAuth scenarios (extension_scope_missing, webhook_register_5xx, scope_drift, duplicate_install).
BRD.md §US-1.1: Given the install fails mid-flight, When the handshake errors out, Then the merchant sees a descriptive message and the BC side does not show the app as installed
- compliant
brd-us-1-2-jwt-verify-session-cookieValid signed_payload_jwt on /api/load mints session cookie: httpOnly, Secure, SameSite=none, Partitioned
BRD.md §US-1.2: Given I click the app icon in BC, When BC redirects to /api/load?signed_payload_jwt=..., Then we verify the JWT and mint an internal session cookie
- compliant
brd-us-1-2-jwt-malformed-returns-401Malformed or wrong-secret JWT returns 401 and does not set a cookie
BRD.md §US-1.2: Given the JWT is malformed or signed with a wrong secret, When verification fails, Then we return 401 and do not set a cookie
- compliant
brd-us-1-3-app-extensions-registeredInstall callback issues GraphQL createAppExtension mutations for PANEL context on Orders, Products, and Customers models
BRD.md §US-1.3: Given the OAuth handshake completes, When the install callback finishes, Then we issue GraphQL createAppExtension mutations for PANEL context on Orders, Products, and Customers models
- manual review
brd-us-1-3-reinstall-conflict-skippedReinstall extension conflict (extension already exists) is detected and skipped gracefully
BRD.md §US-1.3: Given an extension already exists (reinstall), When we try to create it, Then we detect the conflict and skip gracefully
- compliant
brd-us-1-4-webhooks-registeredInstall registers store/order/*, store/cart/*, store/customer/*, store/product/*, store/app/uninstalled webhook subscriptions
BRD.md §US-1.4: Given install completes, When we subscribe to webhooks, Then store/order/*, store/cart/*, store/customer/*, store/product/*, store/app/uninstalled are all registered
- compliant
brd-us-1-5-uninstall-revokes-pausesUninstall webhook verifies signed payload, revokes credentials, deregisters extensions, pauses all active subscriptions
BRD.md §US-1.5: Given I uninstall from BC, When BC POSTs to /api/uninstall, Then we verify the signed payload, revoke credentials, deregister extensions, and pause all active subscriptions
- manual review
brd-us-1-5-uninstall-retains-30dSubscription data retained 30 days after uninstall for potential reinstall recovery
BRD.md §US-1.5: Given subscriptions exist on uninstall, When uninstall processes, Then we retain subscription data for 30 days for potential reinstall recovery
- manual review
brd-us-1-5-uninstall-pii-purgeAfter 30 days post-uninstall without reinstall, subscriber PII is hard-deleted, only anonymized revenue records retained
BRD.md §US-1.5: Given 30 days pass post-uninstall without reinstall, When the retention window closes, Then we hard-delete subscriber PII and retain only anonymized revenue records
- compliant
brd-us-1-6-users-row-upsertedEvery successful /api/load JWT verify upserts a users row keyed by (store_hash, bc_user_id) with email, is_owner, first_seen_at, last_seen_at
BRD.md §US-1.6: Given a successful signed_payload_jwt verify on /api/load, When we extract user.{id, email}, Then we upsert a users row keyed by (store_hash, bc_user_id)
- compliant
brd-us-1-7-events-actor-user-idMutating events carry actor_user_id from session JWT; system events carry null actor_user_id with actor_kind set
BRD.md §US-1.7: Given a session-authenticated mutating call, When we write the change, Then the resulting events row carries actor_user_id resolved from the session JWT
- manual review
brd-us-2-1-bc-payments-auto-detectedProcessor setup page pre-selects and marks "BC Payments" as Ready when BC Payments is enabled on the store
BRD.md §US-2.1: Given I open the processor setup page, When BC Payments is enabled on my store, Then "BC Payments" is pre-selected and marked "Ready"
- manual review
brd-us-2-1-bc-payments-not-enabled-linkWhen BC Payments is not enabled, shows "Enable BC Payments" link to BC onboarding flow
BRD.md §US-2.1: 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
- compliant
brd-us-2-2-stripe-connect-oauthSelecting Stripe and clicking Connect redirects through Stripe Connect OAuth and returns with stripe_account_id attached
BRD.md §US-2.2: 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
- manual review
brd-us-2-2-stripe-connected-stateSuccessful Stripe connection shows "Connected" state with last 4 of account name and a "Test charge" button
BRD.md §US-2.2: 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
- manual review
brd-us-2-2-stripe-revoked-prompts-reauthAccess revoked in Stripe is detected and prompts re-auth in our app
BRD.md §US-2.2: Given I revoke access in Stripe, When I return to our app, Then we detect the disconnection and prompt re-auth
- manual review
brd-us-2-3-gateway-compatibility-tableInstall-time compatibility detection lists active BC payment methods with Supported/Supported in Phase 2/Not supported badges
BRD.md §US-2.3: 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"
- manual review
brd-us-2-3-no-supported-gateway-optionsWhen none of merchant's active gateways are MVP-supported, shows three options: switch to BC Payments, add Stripe, or decline install
BRD.md §US-2.3: 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
- manual review
brd-us-2-3-stored-payments-enabled-warningWhen BC Payments is detected but Stored Payments is not enabled, ConnectProcessor wizard shows an orange warning banner with a link to gateway settings
BRD.md §US-2.3 / ADR-0037 (P-comp Sub-B): stored payments enablement check in auto-detect
- compliant
brd-us-2-4-test-mode-routes-to-sandboxTest mode toggle routes subscription charges to processor sandbox without debiting real cards
BRD.md §US-2.4: Given I toggle "Test mode" on, When I create a subscription, Then charges route to the processor's sandbox and no real card is debited
- compliant
brd-us-2-4-test-orders-taggedTest-mode charges generate BC orders tagged with [TEST] in staff_notes and filterable in merchant dashboard
BRD.md §US-2.4: Given test-mode charges succeed, When they generate BC orders, Then the orders are tagged with [TEST] in staff_notes
- manual review
brd-us-2-4-test-mode-off-archive-promptToggling test mode off with active test subscriptions prompts merchant to archive them before going live
BRD.md §US-2.4: Given I toggle test mode off, When I have test subscriptions active, Then I am prompted to archive them before going live
- manual review
brd-us-2-5-processor-health-badgeProcessor 401/403 or account suspension turns health badge red and emails merchant contact; recovery auto-restores green
BRD.md §US-2.5: 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
- manual review
brd-us-2-6-multiple-processorsMerchant can connect multiple processors and route subscriptions by channel or currency
BRD.md §US-2.6: Given I add a second processor, When I save, Then I can designate one as default and assign others to specific channels or currencies
- manual review
brd-us-2-7-capture-timing-onboarding-advisorySetup checklist surfaces a capture-timing summary card with current setting, link to Payment Settings (US-10.9), and Phase 1 advisory note; EU stores see a compliance highlight
BRD.md §US-2.7: Given I complete processor onboarding, When I reach the final setup-checklist step, Then a capture-timing summary card is shown with current setting, link to Payment Settings, and Phase 1 advisory note
- manual review
brd-us-3-1-recharge-csv-importRecharge CSV export upload triggers row-by-row validation and creates subscriptions in paused-pending-PM state
BRD.md §US-3.1: Given I upload a Recharge CSV export, When I map fields in the wizard, Then the system validates row-by-row and shows pass/fail counts
- manual review
brd-us-3-2-pm-migration-stripe-matchStripe customer_id match links existing PaymentMethod to imported subscription; unmatched subscriptions stay paused with subscriber email for PM update
BRD.md §US-3.2: Given I import a Recharge export and my Stripe account is connected, When Recharge customer_id matches a Stripe customer, Then we attach the default PaymentMethod to the imported subscription
- manual review
brd-us-3-3-next-charge-at-preservedImported next_charge_at preserved exactly; past-date anomalies surfaced for merchant rescue-or-retry decision
BRD.md §US-3.3: Given the import includes next_charge_at, When I activate post-migration, Then no charge is scheduled earlier than the imported date
- manual review
brd-us-3-4-paywhirl-importPayWhirl CSV source selection auto-defaults field mappings to PayWhirl schema; plan-to-product mappings surfaced for confirmation
BRD.md §US-3.4: Given I pick "PayWhirl" as the source, When I upload their CSV, Then field mapping defaults match PayWhirl's schema
- manual review
brd-us-3-5-bold-minibc-importBold Subscriptions and MINIBC source selection applies vendor-specific field mappings
BRD.md §US-3.5: Given I pick the respective source, When I upload the export, Then vendor-specific field mappings are applied
- manual review
brd-us-3-6-dry-run-impact-reportDry-run simulates import and produces report: subs created, PMs matched, estimated MRR, revenue at risk; merchant can adjust and re-run
BRD.md §US-3.6: Given I upload an export, When I click "Dry run," Then the system simulates the import and produces a report
- partial
brd-us-4-1-product-panel-rendersSubscriptions App Extension panel appears on BC product edit page showing plan status, intervals, pricing, eligibility
BRD.md §US-4.1: Given I open any BC product, When the product edit page renders, Then the "Subscriptions" App Extension panel appears
- compliant
brd-us-4-1-panel-cta-opens-wizardWhen no plan is configured, clicking the panel opens the plan wizard via a "Configure subscription plan" CTA
BRD.md §US-4.1: Given no plan is configured, When I click the panel, Then a "Configure subscription plan" CTA opens the plan wizard
- compliant
brd-us-4-2-wizard-steps-flowPlan wizard has steps: Interval → Pricing → Options → Eligibility → Review
BRD.md §US-4.2: Given I click "Configure," When the wizard opens, Then it has steps: Interval(s) → Pricing → Optional extras → Eligibility → Review
- compliant
brd-us-4-2-wizard-activate-saves-planCompleting wizard and clicking Activate saves the plan and makes the product subscribable
BRD.md §US-4.2: Given I complete all steps, When I click "Activate," Then the plan is saved, the product is flagged subscribable, and the storefront widget renders on the PDP
- compliant
brd-us-4-3-variant-eligibility-checkboxesVariant eligibility step shows all variants with checkboxes, defaulting to all eligible
BRD.md §US-4.3: Given a product has multiple variants, When I reach the eligibility step, Then I see all variants with checkboxes, defaulting to "all eligible"
- manual review
brd-us-4-3-unchecked-variant-hides-widgetUnchecked variant in eligibility step causes storefront widget to hide subscribe option for that variant
BRD.md §US-4.3: Given I uncheck a variant, When I activate, Then the storefront widget hides the subscribe option for that variant
- manual review
brd-us-4-4-deactivate-warning-optionsDeactivating an active plan warns about active subscribers and offers: stop new signups / end at next cycle / cancel all immediately
BRD.md §US-4.4: Given a plan is active, When I click "Deactivate," Then I am warned about active subscribers and asked to choose
- manual review
brd-us-4-4-stop-new-signups-existing-continue"Stop new signups" hides storefront widget but existing subscriptions continue unaffected
BRD.md §US-4.4: Given I choose "Stop new signups," When the change saves, Then the storefront widget disappears but existing subs continue unaffected
- compliant
brd-us-4-5-copy-plan-multi-selectCopy plan to multiple products via multi-select with search
BRD.md §US-4.5: Given a plan exists on product A, When I click "Copy to products," Then I select target products via multi-select
- compliant
brd-us-4-5-copy-plan-summaryConfirming copy shows summary of successful/failed copies and conflicts
BRD.md §US-4.5: Given I confirm the copy, When the operation completes, Then I see a summary of successful/failed copies and any conflicts
- compliant
brd-us-5-1-multiple-intervals-configuredPlan supports any combination of every N days/weeks/months (N ≤ 24) intervals
BRD.md §US-5.1: Given plan design, When I add intervals, Then I can specify any combination from: every N days/weeks/months (N ≤ 24)
- compliant
brd-us-5-1-intervals-shown-on-pdpMultiple configured intervals shown as dropdown/radio on PDP with per-interval savings indication
BRD.md §US-5.1: Given multiple intervals are configured, When a subscriber reaches PDP, Then a dropdown/radio shows all options with per-interval savings indication
- compliant
brd-us-5-2-fixed-discount-renewal-priceFixed discount % pricing computes renewal charge as current_catalog_price * (1 - discount/100)
BRD.md §US-5.2: Given I select "Fixed discount %" and enter 10, When a subscriber renews, Then the renewal charge is computed as current_catalog_price * 0.9 per unit
- manual review
brd-us-5-2-discount-follows-catalog-priceCatalog price changes flow through to next renewal (unless lock-at-creation enabled)
BRD.md §US-5.2: Given the catalog price changes, When the next renewal runs, Then the renewal uses the new base price
- compliant
brd-us-5-3-price-list-chargesPrice List strategy reads charge amounts from the BC Price List for the subscriber's variant at renewal
BRD.md §US-5.3: Given I select "Price List" and pick one, When subscribers renew, Then charge amounts are read from the Price List's price for their variant
- manual review
brd-us-5-3-price-list-deleted-exceptionDeleted or archived Price List triggers exception queue entry for merchant attention at renewal
BRD.md §US-5.3: Given the Price List is deleted or archived in BC, When the renewal runs, Then the charge goes into the exception queue for merchant attention
- compliant
brd-us-5-4-fixed-price-ignores-catalogFixed price strategy charges the configured absolute amount regardless of catalog changes
BRD.md §US-5.4: Given I select "Fixed price" and enter amount + currency, When renewals run, Then the charge amount ignores catalog changes
- compliant
brd-us-5-5-trial-skips-first-chargeTrial of N days skips or discounts first charge and schedules second charge at N + interval days
BRD.md §US-5.5: Given I enable a trial of N days, When a subscriber signs up, Then the first charge is skipped (free trial) or discounted (paid trial) and the second charge is scheduled N + interval days out
- compliant
brd-us-5-5-trial-cancel-no-chargeCancelling during trial results in no charge and no cancel penalty
BRD.md §US-5.5: Given a trial is active, When a subscriber cancels before the trial ends, Then no charge occurs and no cancel-penalty applies
- compliant
brd-us-5-5-trial-end-invalid-pm-dunningInvalid payment method when trial ends triggers standard dunning on first full charge
BRD.md §US-5.5: Given a trial ends and payment method is invalid, When the first full charge runs, Then standard dunning applies
- compliant
brd-us-5-6-lock-price-snapshottedLock-price-at-creation snapshots locked_price_cents on subscription record; renewals use snapshot regardless of catalog changes
BRD.md §US-5.6: Given I enable "Lock price at creation" in plan settings, When a new subscription starts, Then locked_price_cents is snapshotted on the subscription record
- manual review
brd-us-5-7-min-cycles-shown-in-widgetMinimum cycles commitment shown in widget; cancel flow blocked or shows early-termination notice
BRD.md §US-5.7: Given I configure min_cycles: 6, When a subscriber signs up, Then they are shown "Requires 6-cycle commitment" in the widget
- compliant
brd-us-5-8-cancel-lock-policyPlan-level commitment_cycles drives subscription.lock_expires_at computation; cancel-lock-policy service returns locked + lock_expires_at; portal cancel route returns 409 conflict with lock_expires_at while locked
BRD.md §US-5.8: Given I set minimum_term_cycles on a plan with early_cancel_policy: block, When a subscriber signs up, Then lock_expires_at is computed and stored; cancel attempts during lock window return 409.
- partial
brd-us-5-9-calendar-anchorplans.billing_anchor_month + billing_anchor_day power calendar-anchored renewals; scheduler reads anchor columns to converge next_charge_at to the configured anchor date
BRD.md §US-5.9: Given I enable calendar anchoring on a plan, When a subscriber signs up, Then their next_charge_at is computed as the nearest future anchor date; existing subscribers on the plan converge to the anchor.
- compliant
brd-us-6-1-gift-subscription-purchaseGift-giver pays upfront for N cycles; recipient gets reveal email; claiming creates subscription in recipient's BC account with no PM required
BRD.md §US-6.1: Given a subscribable product and a "Gift this subscription" toggle, When I pick it and enter recipient info, Then I pay upfront for N cycles and the recipient receives a gift-reveal email
- compliant
brd-us-6-1-gift-claim-creates-subscriptionRecipient gift-reveal link claims gift and creates subscription with cycles_prepaid counting down from N
BRD.md §US-6.1: Given the gift recipient clicks the reveal link, When they claim the gift, Then a subscription is created in their BC customer account with no payment method required and cycles counting down from N
- compliant
brd-us-6-2-prepaid-single-chargePrepaid checkout collects full period amount in one charge; prepaid_cycles_remaining tracked; BC orders created per cycle with captured payment
BRD.md §US-6.2: Given a plan with prepaid options, When I check out with "6 months prepaid," Then a single charge collects the full period amount
- compliant
brd-us-6-2-prepaid-period-end-renewal-promptPrepaid period end prompts subscriber to renew or lapse; no dunning during prepaid window
BRD.md §US-6.2: Given the prepaid period ends, When the next cycle would occur, Then I am prompted to renew or let it lapse
- partial
brd-us-6-3-build-a-box-compositionBuild-a-box plan with box_size, eligible_products, customization_window; subscriber picks items; previous composition rolls forward if not customized
BRD.md §US-6.3: Given a build-a-box plan with box_size, When I sign up, Then I pick my initial items from the set
- compliant
brd-us-6-4-bundle-single-bc-orderBundle plan generates single BC order per cycle with all bundle SKUs at bundle discount; OOS SKU flags exception queue
BRD.md §US-6.4: Given I configure a bundle plan with fixed composition, When a subscriber signs up, Then each cycle generates a single BC order with all bundle SKUs at the bundle discount
- compliant
brd-us-6-5-membership-entitlement-state-machineMembership plan creates Entitlement rows per configured key; PENDING→ACTIVE on charge success; ACTIVE→SUSPENDED on dunning exhausted; SUSPENDED→ACTIVE on recovery; terminal REVOKED on cancel
BRD.md §US-6.5: Given a "Membership" plan type with entitlements, When a subscriber signs up, Then one Entitlement row is created per configured key with status=pending
- compliant
brd-us-6-6-usage-based-metered-billingUsage-based plan accumulates meter units per cycle via API; cycle charge = sum(usage) * price_per_unit + fixed base
BRD.md §US-6.6: Given a usage-based plan with meter_name and price_per_unit, When subscribers report usage, Then usage accumulates per cycle
- compliant
brd-us-6-7-curation-sku-rotationCurated plan pulls merchant-selected or rule-selected SKU per cycle honoring subscriber preferences
BRD.md §US-6.7: Given a "Curated" plan with merchant-configured rotation rules, When each cycle runs, Then the merchant-picked SKU is included in the BC order
- compliant
brd-us-6-8-referral-creditActive subscriber earns merchant-configured credit on referral first-charge success; credit applied to next charge; fraudulent referrals reversed
BRD.md §US-6.8: Given I have an active subscription, When a referred user signs up through my link and their first charge succeeds, Then I earn merchant-configured credit applied to my next charge
- compliant
brd-us-6-9-allotment-grant-schemaallotment_grants table tracks recurring quota per customer/org with current_balance, refresh_cadence, rollover_policy; allotment_debits links to BC orders or Charges
BRD.md §US-6.9: Given I create an AllotmentGrant, When I issue it to a customer, Then a grant row is created with current_balance = amount_per_period and next_refresh_at set per cadence
- compliant
brd-us-6-10-multi-actor-rolesactors table with owner/payer/beneficiary/manager/org_admin roles; payer PM used for renewals; beneficiary gets delivery notifications; manager cc'd on dunning
BRD.md §US-6.10: Given a subscription is created, When persisted, Then an actors row is written for at least the owner role; payer.role is unique-per-subscription
- compliant
brd-us-6-11-custom-field-definitionscustom_field_definitions table per plan/subscription/grant with scope, key, type, required, validation_regex, subscriber visibility; 422 on missing required field
BRD.md §US-6.11: Given I configure a CustomFieldDefinition, When I save, Then the field appears in admin/storefront UI immediately
- manual review
brd-us-7-1-plan-channel-scopePlan with channel_ids restricts widget rendering and subscription creation to allowed BC channels; channel_id stamped on subscription
BRD.md §US-7.1: Given a plan has channels: [1, 2], When a subscriber browses channel 3, Then the subscription widget does not render on that product's PDP
- manual review
brd-us-7-2-plan-customer-group-scopePlan with customer_groups restricts widget to matching groups; grandfathering/cancel/pause behavior on group-loss is merchant-configurable
BRD.md §US-7.2: Given a plan has customer_groups: [vip], When a non-VIP subscriber reaches the PDP, Then the subscription widget does not render
- manual review
brd-us-7-3-plan-country-scopePlan with country_codes restricts storefront plan discovery to subscribers in allowed countries; country_code stamped on subscription from BC order billing_address
BRD.md §US-7.3: Given a plan has allowed_countries: [US, CA], When a subscriber enters a shipping address outside the allowed list, Then checkout blocks with a clear message
- manual review
brd-us-7-4-plan-price-list-scopePlan with price_list_ids restricts storefront plan discovery to customers whose active price list is in the allowed set; BC native Price List resolution order for multi-list membership is Phase 3
BRD.md §US-7.4: Given a plan pricing strategy is "Price List" and a customer belongs to multiple Price Lists, When a charge computes, Then BC's native Price List resolution order is respected
- manual review
brd-us-7-5-plan-coverage-matrixAdmin GET /api/v1/admin/plans/:id/coverage returns scoping rollup (channels × customer_groups × countries × price_lists); admin Coverage.tsx renders the matrix
BRD.md §US-7.5: Given I open "Plan coverage" in the admin, When it loads, Then I see a matrix of channels × customer groups × countries with count of applicable plans per cell
- compliant
brd-us-8-1-widget-renders-on-pdpStencil PDP widget renders: one-time/subscribe radio, interval dropdown, savings indication, next-charge preview
BRD.md §US-8.1: Given a subscribable product and a Stencil theme, When I load the PDP, Then a widget renders with: "One-time purchase" vs. "Subscribe and save" radio, interval dropdown, savings indication, and next-charge preview
- compliant
brd-us-8-1-subscribe-adds-cart-intentPicking "Subscribe" and adding to cart injects subscription intent custom fields on the cart line-item
BRD.md §US-8.1: Given I pick "Subscribe," When I add to cart, Then the cart line-item carries subscription intent custom fields
- compliant
brd-us-8-2-cart-subscription-badgeMixed cart shows subscription items with distinct badge, interval label, and "Remove subscription commitment" shortcut
BRD.md §US-8.2: Given my cart contains both subscription and one-time items, When the cart renders, Then subscription items show a distinct badge, interval label, and "Remove subscription commitment" shortcut
- compliant
brd-us-8-2-cart-quantity-interval-updateChanging subscription line quantity or interval in cart updates the line-item custom fields
BRD.md §US-8.2: Given I change a subscription line's quantity or interval in cart, When the cart updates, Then the line-item custom fields update accordingly
- compliant
brd-us-8-3-sdk-use-subscription-optionsuseSubscriptionOptions(productId) returns { intervals, pricingPreview, eligibility }
BRD.md §US-8.3: Given I install @bc-subscriptions/react, When I call useSubscriptionOptions(productId), Then I get { intervals, pricingPreview, eligibility }
- compliant
brd-us-8-3-sdk-add-subscription-to-cartaddSubscriptionToCart updates BC cart with correct line-item custom fields
BRD.md §US-8.3: Given I call addSubscriptionToCart({ productId, variantId, intervalKey }), When it resolves, Then the BC cart is updated with correct line-item custom fields
- manual review
brd-us-8-3-sdk-ineligibility-typedEligibility failure (customer group, region, channel) returns typed ineligibility reason from hook
BRD.md §US-8.3: Given eligibility fails, When I call the hook, Then I receive a typed ineligibility reason
- compliant
brd-us-8-4-widget-reads-css-varsWidget reads --color-primary, --font-body theme CSS variables on mount; falls back to neutral defaults when absent
BRD.md §US-8.4: Given a Stencil theme with standard CSS variables, When the widget mounts, Then it reads --color-primary, --font-body etc. and uses them
- manual review
brd-us-8-5-ab-widget-copyMerchant-created copy variant shown 50/50 by subscriber session with conversion rate and confidence reporting
BRD.md §US-8.5: Given I create a copy variant in admin, When the widget renders, Then it splits 50/50 between control and variant by subscriber session
- compliant
hive-1149-cdn-webcomponent-distributionWebcomponent IIFE bundle is published to a reachable CDN (bc-subscriptions-cdn.pages.dev/v1/) and the API + admin reference that URL consistently
Hive #1149 Option B-pages — Cloudflare Pages-served distribution until cdn.subscriptions.bigcommerce.com is delegated.
- compliant
brd-us-8-6-education-panelEducation panel expands to show FAQ items (Cancel anytime, Change cadence, Skip); dismissal remembered per session
BRD.md §US-8.6: Given a merchant-enabled education panel, When I expand it on the widget, Then I see short FAQ items with icons
- compliant
brd-us-9-1-cart-custom-fields-intentSubscription add-to-cart sets line_item.custom_fields with {sub_plan_id, sub_interval_key, sub_quantity}
BRD.md §US-9.1: Given a subscriber adds a subscription item, When the cart POST fires, Then line_item.custom_fields includes {sub_plan_id, sub_interval_key, sub_quantity}
- manual review
brd-us-9-1-cart-edit-consistent-fieldsCart quantity edit keeps custom fields consistent with subscription intent
BRD.md §US-9.1: Given the subscriber edits quantity in cart, When the line updates, Then custom fields stay consistent
- compliant
brd-us-9-2-order-webhook-creates-subscriptionstore/order/created webhook creates Subscription row (status: active) with cycle-0 charge marked succeeded, bc_order_id set
BRD.md §US-9.2: Given a new order contains a line with sub_plan_id, When the webhook processes, Then a Subscription row is created with status active, first charge marked succeeded with bc_order_id of the triggering order
- compliant
brd-us-9-2-multiple-sub-items-per-orderOrder with multiple subscription items creates one Subscription per item
BRD.md §US-9.2: Given the order contains multiple subscription items, When processing, Then one Subscription per item is created
- compliant
brd-us-9-2-invalid-plan-exception-queueOrder with missing or deactivated plan flags the order in the exception queue without blocking BC checkout
BRD.md §US-9.2: Given the order creation fails our validation (missing plan, deactivated plan), When processing, Then we flag the order in the exception queue without blocking the BC checkout
- compliant
brd-us-9-3-fallback-capture-from-sessionThank-you page JS detects subscription intent from sessionStorage and POSTs to fallback endpoint if custom fields absent from order
BRD.md §US-9.3: Given an order is created without subscription custom fields, When our thank-you-page JS detects the subscriber intended a subscription (from pre-checkout session storage), Then it POSTs to our fallback endpoint
- compliant
brd-us-9-3-fallback-links-subscription-to-orderSuccessful fallback links the subscription to the just-created order
BRD.md §US-9.3: Given the fallback triggers, When it succeeds, Then the subscription is linked to the just-created order
- manual review
brd-us-9-3-fallback-failure-messageFallback timeout or error shows subscriber clear message with support link
BRD.md §US-9.3: Given the fallback fails, When it times out or errors, Then the subscriber sees a clear message with a support link
- compliant
brd-us-9-4-mixed-cart-only-sub-lines-create-rowsMixed checkout produces order with both item types; only subscription lines create Subscription rows
BRD.md §US-9.4: Given a mixed cart, When checkout completes, Then the order contains both item types and only the subscription lines create Subscription rows
- manual review
brd-us-9-5-incompatible-checkout-bannerMerchant using incompatible third-party checkout sees dashboard banner with named fallback options
BRD.md §US-9.5: Given the merchant uses a third-party checkout that doesn't support line-item custom fields, When they install, Then the dashboard shows an "Incompatible checkout" banner
- compliant
brd-us-10-1-scheduler-enqueues-due-chargesScheduler queries charges with status=scheduled AND scheduled_at <= now() + 15min and enqueues each; already-processing charges not re-enqueued
BRD.md §US-10.1: Given a Vercel Cron runs every 15 minutes, When it fires, Then it queries charges with status=scheduled AND scheduled_at <= now() + 15min and enqueues each into Vercel Queue
- compliant
brd-us-10-2-redis-lock-prevents-doubleCharge executor acquires Redis lock on charge.id (5-min TTL) and sets status=processing before dispatching to processor
BRD.md §US-10.2: Given a charge is dequeued, When the workflow starts, Then it acquires a Redis lock on charge.id (5-min TTL) and sets status=processing
- compliant
brd-us-10-2-idempotency-key-prevents-double-chargeProcessor called with idempotency key = charge.id; same key presented twice returns same result without double-charging
BRD.md §US-10.2: Given the workflow calls the processor with idempotency key = charge.id, When the same key is presented again, Then the processor returns the same result without double-charging
- manual review
brd-us-10-2-workflow-crash-recoveryMid-flight workflow crash resumes from last completed step using durable workflow state
BRD.md §US-10.2: Given the workflow crashes mid-flight, When it restarts, Then it resumes from the last completed step using the workflow's durable state
- compliant
brd-us-10-3-anchor-date-stable-scheduleNext charge scheduled at anchor_date + N×interval; handles month-length edge cases (Jan 31 + 1mo = Feb 28/29)
BRD.md §US-10.3: Given a monthly subscription with anchor_date = 2026-04-10, When cycle N closes, Then the next charge is scheduled at anchor_date + N months with month-length edge case handling
- compliant
brd-us-10-3-late-retry-does-not-shift-anchorCharge that retries and succeeds late still schedules next charge at original anchor, not late completion time
BRD.md §US-10.3: Given a charge retries and succeeds late, When it resolves, Then the next charge is still anchored to the original schedule
- compliant
brd-us-10-4-charge-jitter-hash-basedTime-of-day component of scheduled charge derived from deterministic hash of subscription.id to distribute load across 24h
BRD.md §US-10.4: Given a subscription's anchor_date is set, When scheduling, Then the time-of-day component is deterministically derived from a hash of subscription.id
- compliant
brd-us-10-5-pause-shifts-next-chargePausing for N days shifts next_charge_at by exactly N days and all future scheduled charges shift accordingly
BRD.md §US-10.5: Given an active subscription with next_charge_at = T, When I pause for 14 days, Then next_charge_at moves to T+14d and all future charges shift accordingly
- manual review
brd-us-10-5-early-resume-snaps-to-anchorUn-pausing early snaps schedule back to closest future anchor interval
BRD.md §US-10.5: Given I un-pause early, When I resume, Then the schedule snaps back to the closest future anchor interval
- compliant
brd-us-10-6-upcoming-5-chargesSubscription detail shows next 5 upcoming charges with date, estimated amount, and status
BRD.md §US-10.6: Given I open a subscription detail, When the upcoming-charges panel renders, Then it lists the next 5 charges with date, amount (estimated), status
- partial
brd-us-10-7-pending-start-deferred-activationSubscriptions created with future starts_at land in status="pending_start"; scheduler's processPendingStarts scan flips them to active when starts_at <= now and emits subscription.activated with reason=starts_at_reached
BRD.md §US-10.7: Given a subscriber selects a future start date at checkout, When starts_at <= now, Then the deferred-activation scan transitions the subscription to active, enqueues first charge, and anchor_date is initialized from starts_at.
- partial
brd-us-10-8-first-cycle-prorationMid-year sign-ups on calendar-anchored plans pay a prorated first charge for the partial period; plans.proration_policy controls proportional vs skip_stub behaviour
BRD.md §US-10.8: Given an annual plan with billing_anchor_month/day set, When a subscriber signs up mid-year, Then the first charge is prorated as (days_to_anchor / 365) × annual_price; proration_policy=skip_stub bypasses stub cycles within 7 days of the anchor.
- partial
brd-us-10-9-capture-timing-configstores.capture_timing column persists merchant choice (immediate / on_fulfillment / on_ship); PaymentSettings admin UI renders dropdown + Phase 1 advisory banner + EU compliance note; capture-timing-resolver service surfaces effective mode for Phase 2 enforcement
BRD.md §US-10.9: Given I open Payment Settings, When I select capture_timing, Then it persists and an advisory banner explains Phase 1 = display-only / Phase 2 = enforced; EU stores see a compliance prompt.
- compliant
brd-us-11-1-dunning-policy-configMerchant can configure retry intervals, on_exhaustion behavior, and grace period in dunning settings
BRD.md §US-11.1: Given I open dunning settings, When I edit the policy, Then I can set: retry intervals, on_exhaustion, grace period
- compliant
brd-us-11-1-dunning-policy-applies-to-new-chargesSaved dunning policy applies to new charge scheduling
BRD.md §US-11.1: Given I save the policy, When new charges are scheduled, Then retries follow the new policy
- compliant
brd-us-11-2-soft-decline-schedules-retryFailed charge with soft decline schedules retry per policy next interval
BRD.md §US-11.2: Given a charge fails with a soft decline, When the workflow branches to dunning, Then it schedules a retry per the policy's next interval
- compliant
charges-retry-with-backoffFailed charges retry up to 3 times with 1h/4h/24h exponential backoff (ADR-0011 gap 1)
BRD.md §US-11.2 / ADR-0011 §5: retry curve 1h/4h/24h; idempotency key stable across retries; after 3 failures → failed_permanently + exception queue entry
- compliant
brd-us-11-2-exhaustion-triggers-actionWhen all retries exhaust, subscription transitions per on_exhaustion policy and final email sends
BRD.md §US-11.2: Given all retries exhaust, When the on_exhaustion action triggers, Then subscription transitions to cancelled/paused per policy and a final email goes out
- compliant
brd-us-11-3-hard-decline-no-retryHard decline codes result in no further retries, subscription moves to past_due with subscriber notification
BRD.md §US-11.3: Given a processor returns a hard decline code, When classified by the adapter, Then no further retries are scheduled
- compliant
brd-us-11-3-soft-decline-proceeds-retrySoft decline (insufficient funds, network timeout) proceeds to retry per policy
BRD.md §US-11.3: Given a processor returns a soft decline (insufficient funds, network timeout), When classified, Then retries proceed per policy
- compliant
brd-us-11-3-mit-context-normalizationAdapter normalizes mitContext to processor-specific indicator (BC Payments MREC flag, Stripe mit_exemption, Braintree transactionSource)
BRD.md §US-11.3: Given a charge dispatch carries mitContext, When the adapter handles it, Then the adapter normalizes the context to its processor-specific indicator
- manual review
brd-us-11-4-automatic-card-updaterExpired/replaced cards auto-updated via network services (Visa VAU, Mastercard ABU) without triggering dunning
BRD.md §US-11.4: Given the processor supports Automatic Card Updater, When a card is replaced by the issuer, Then stored PM details update automatically
- compliant
brd-us-11-5-pm-update-resets-retriesSubscriber updating PM during dunning resets retry counter to 0 and enqueues retry within 1 minute
BRD.md §US-11.5: Given my charge has failed and I am in dunning, When I update my PM via the portal, Then the retry counter resets to 0 and a retry is enqueued within 1 minute
- manual review
brd-us-11-6-retry-failure-emailEmail sent when retry fails and next retry is >24h away, with what went wrong, next retry time, and PM update CTA
BRD.md §US-11.6: Given a retry fails, When the next retry is scheduled > 24h out, Then an email is sent with: what went wrong, when the next retry is, a CTA to update PM
- manual review
brd-us-11-6-sms-retry-imminentSMS sent when retry is imminent (<6h) for opted-in subscribers with PM update link
BRD.md §US-11.6: Given I have opted in to SMS, When a retry is imminent (< 6h), Then an SMS is sent with the update-PM link (P2)
- manual review
brd-us-11-7-merchant-dunning-digestDaily merchant digest of subscribers in dunning, sorted by MRR desc with attempt count and days-in-dunning
BRD.md §US-11.7: Given subscribers are in dunning, When the daily digest fires, Then it lists them by MRR desc, shows attempt count, days in dunning
- compliant
brd-us-12-1-refund-via-processor-adapterFull or partial charge refund issued via processor adapter; BC order status updated; Event written; charge marked refunded
BRD.md §US-12.1: Given I click "Refund" on a succeeded charge, When I enter amount and reason, Then the refund is issued via the processor adapter
- partial
brd-us-12-2-store-credit-applied-at-renewalStore credit balance reduces next charge amount; remainder rolls forward; credit balance decremented atomically with charge decrement
BRD.md §US-12.2: Given I issue $10 store credit to a subscriber, When the next charge runs, Then the charge amount is reduced by up to $10 and the credit balance decrements
- compliant
brd-us-12-3-skip-cycle-advances-scheduleSkip next charge marks upcoming charge abandoned with reason=skipped and advances next_charge_at by one interval
BRD.md §US-12.3: Given I click "Skip next charge" on an upcoming scheduled charge, When I confirm, Then the charge is marked abandoned with reason skipped and next_charge_at advances by one interval
- partial
brd-us-12-4-manual-chargeManual ad-hoc charge executed against stored PM with optional BC order creation; blocked without opt-in auth
BRD.md §US-12.4: Given I click "Add manual charge" on a subscription, When I enter amount and confirmation, Then the charge is executed immediately against the PM and a BC order is created optionally
- manual review
brd-us-12-5-chargeback-exception-queueProcessor chargeback webhook auto-pauses subscription, marks charge disputed, emails merchant with evidence-upload link
BRD.md §US-12.5: Given a chargeback is received (processor webhook), When we process it, Then the subscription is auto-paused, the charge is marked disputed, and an email goes to the merchant with evidence-upload link
- manual review
brd-us-12-6-proportional-tax-on-partial-refundPartial refund includes proportional tax; BC order reflects adjusted tax amount
BRD.md §US-12.6: Given a $100 charge with $7 tax is partially refunded $30, When the refund processes, Then the refund includes proportional tax (~$2.10) and the BC order reflects the adjusted tax
- manual review
brd-us-13-1-event-timeline-per-subscriptionSubscription event timeline lists every lifecycle event with timestamp, actor, and payload snippet; filterable by event type and actor
BRD.md §US-13.1: Given I open a subscription, When the timeline renders, Then it lists every lifecycle event with timestamp, actor, and payload snippet
- compliant
brd-us-13-2-reconciliation-cronDaily reconciliation cron detects: succeeded charges with no bc_order_id, stuck processing charges >1h, BC order/charge mismatches; drifts create exception_queue entries
BRD.md §US-13.2: Given the reconciliation cron runs, When it executes, Then it samples charges without bc_order_id, BC orders without matching charge, stuck processing charges
- compliant
brd-us-13-3-workflow-replay-idempotentFailed charge can be replayed with same idempotency key via admin; never double-charges; every replay audited with Support user ID
BRD.md §US-13.3: Given a charge is stuck in failed, When I click "Replay" in the admin, Then the workflow re-runs using the same idempotency key and does not double-charge
- manual review
brd-us-13-4-structured-logs-correlation-idEvery log line carries subscription_id, charge_id, and request correlation ID; queryable end-to-end across services
BRD.md §US-13.4: Given any logged event, When I query by charge_id, Then I see every log line across all services for that charge
- manual review
brd-us-13-5-platform-metrics-dashboardInternal metrics dashboard shows per-store and aggregate charge outcomes, latency distributions, queue depth, dunning recovery rates
BRD.md §US-13.5: Given I open the internal metrics dashboard, When it renders, Then I see per-store and aggregate charge outcomes, latency distributions, queue depth, dunning recovery rates
- compliant
brd-us-14-1-bc-order-created-on-charge-successSuccessful charge triggers POST /v2/orders with customer, addresses, line items, payment_provider_id, staff_notes, external_source; charge.bc_order_id stored
BRD.md §US-14.1: Given a charge succeeds, When the workflow continues, Then a BC order POST happens with customer, billing/shipping addresses, line items, payment_status=captured, payment_provider_id, status_id
- compliant
brd-us-14-1-order-creation-failure-exception-queueBC order creation failure routes charge to exception queue while processor settlement remains
BRD.md §US-14.1: Given the order creation fails, When the error is caught, Then the charge goes into the exception queue while the processor settlement remains
- compliant
brd-us-14-2-staff-notes-sub-prefixstaff_notes starts with [SUB] {subscription_id} cycle {N} on every subscription order
BRD.md §US-14.2: Given a subscription order is created, When it posts, Then staff_notes starts with [SUB] {subscription_id} cycle {N}
- compliant
brd-us-14-2-external-source-identifierexternal_source set to bc-subscriptions-app on every subscription order
BRD.md §US-14.2: Given a subscription order is created, When it posts, Then external_source is set to our app identifier
- manual review
brd-us-14-2-custom-fields-order-metadatacustom_fields {subscription_id, charge_id, cycle_number, plan_id} posted via follow-up /v2/orders/{id}/custom_fields
BRD.md §US-14.2: Given a subscription order is created, When it posts, Then custom_fields contains {subscription_id, charge_id, cycle_number, plan_id}
- manual review
brd-us-14-3-first-order-linkage-patchstore/order/created webhook PATCHes the order custom_fields and staff_notes to add subscription linkage after Subscription creation
BRD.md §US-14.3: Given a store/order/created webhook creates a Subscription, When we process, Then we PATCH the order's custom_fields and staff_notes to add subscription linkage
- manual review
brd-us-14-4-merchant-default-status-idMerchant-configured default status_id applied to new subscription orders; merchant settings page exposes this setting
BRD.md §US-14.4: Given the merchant settings page, When I select a default status_id, Then new subscription orders post into that status
- compliant
brd-us-14-5-order-edit-syncstore/order/updated webhook handler reacts to external order changes (status sync + advisory audit on field edits)
BRD.md §US-14.5: Given I edit a subscription order's shipping address in BC, When store/order/updated fires, Then the parent Subscription's default shipping address optionally updates per merchant setting or logs as an Event only
- partial
brd-us-14-5-address-propagation-p2merchant-setting-driven propagation of order shipping_address edits to parent subscription row (sync_order_edits_to_subscription = true); pending charges in active bundles are unbundled so re-materialization honours the new address
BRD.md §US-14.5 Tier 3: optional shipping_address update per merchant setting (Phase 2 — Hive #924, PR #981)
- compliant
brd-us-14-6-delta-capture-prorate-nowprorate_now policy captures (new_price - old_price) * (N-D)/N immediately on plan upgrade; next renewal at new price
BRD.md §US-14.6: Given a plan-upgrade and policy is prorate_now, When the upgrade saves, Then a one-time charge is captured for the prorated delta
- manual review
brd-us-14-6-delta-capture-policies-completeskip_and_bill_next and overcapture delta-capture policies implemented; destination plan policy applies on cross-plan upgrades
BRD.md §US-14.6: Given policies skip_and_bill_next and overcapture, When upgrade saves, Then correct charge timing and cycle semantics apply; destination plan policy governs cross-plan upgrades
- partial
brd-us-14-7-nti-verification-chargeStale NTI triggers MUSE verification charge before renewal MIT; success refreshes last_nti_refreshed_at; failure routes to dunning
BRD.md §US-14.7: Given subscriptions.last_nti_refreshed_at > merchant.nti_freshness_days, When renewal worker runs, Then a MUSE verification charge fires before the renewal MIT
- compliant
pre-charge-hook-registryPre-charge hook chain for renewal-time pre-charge logic; hooks register via preChargeRegistry
Epic 14 design plan / Hive #589
- compliant
brd-us-15-1-inventory-check-pre-chargeInventory check runs before charge execution; low-inventory triggers merchant policy: allow/skip/substitute/pause
BRD.md §US-15.1: Given a charge is about to execute, When the inventory check runs, Then BC API returns current inventory per variant; low inventory triggers merchant backorder policy
- compliant
brd-us-15-2-price-recalculation-at-renewalPrice List re-read at renewal for Price List strategy; fixed discount uses current catalog × discount; locked_price_cents overrides all
BRD.md §US-15.2: Given a plan using "BC Price List" strategy, When the charge computes, Then the Price List is re-read at renewal time
- manual review
brd-us-15-3-tax-recalculation-at-renewalBC tax-calculation endpoint called with current shipping address jurisdiction at renewal; final charge includes computed tax
BRD.md §US-15.3: Given a subscription has shipping address and current line items, When the charge runs, Then BC's tax-calculation endpoint is called with current jurisdiction
- manual review
brd-us-15-4-shipping-recalculation-at-renewalBC shipping-quote endpoint called at renewal; merchant-selected method rate applied; invalid shipping method routes to exception queue
BRD.md §US-15.4: Given a subscription renews, When the charge computes, Then BC's shipping-quote endpoint is called and the merchant-selected shipping method's rate is applied
- partial
brd-us-15-5-free-shipping-plan-settingPlan "Free shipping for subscribers" setting zeros shipping cost on all renewals regardless of cart value
BRD.md §US-15.5: Given plan setting "Free shipping for subscribers: on," When a renewal runs, Then shipping cost is zeroed regardless of cart value
- partial
brd-us-15-6-pre-renewal-oos-checkPre-renewal proactive inventory scan runs ahead of charge window; plans.oos_renewal_policy (proceed/pause/skip_cycle/notify_and_wait) drives outcome; on_hold_oos status + Exception Queue oos_renewal_blocked entry surface to merchant for resolution (Resume now / Skip cycle / Substitute product)
BRD.md §US-15.6: Given a plan with oos_renewal_policy set, When the scheduler pre-renewal OOS check runs, Then the policy is applied (pause / skip_cycle / notify_and_wait / proceed) with subscriber notification and merchant Exception Queue resolution UI.
- partial
brd-us-16-1-bundle-toggle-portalSubscriber with ≥2 active subscriptions to same address sees "Bundle shipments" toggle in portal
BRD.md §US-16.1: Given I have ≥ 2 active subscriptions shipping to the same address, When I open the portal, Then I see a "Bundle shipments" toggle
- compliant
brd-us-16-1-bundle-snaps-renewal-datesEnabling bundle toggle snaps eligible renewals to a common date within merchant-configured window
BRD.md §US-16.1: Given I toggle on, When my next charges are scheduled, Then the scheduler snaps eligible renewals to a common date
- compliant
brd-us-16-2-bundle-single-bc-orderBundled renewals produce a single BC order with combined line items tagged with all bundle subscription_ids and charge_ids
BRD.md §US-16.2: Given two charges in a bundle succeed on the same cycle, When creating the BC order, Then one order is posted with line items from both subscriptions
- compliant
brd-us-16-2-bundle-failed-charge-excludedFailed charge in a bundle is excluded from the BC order while succeeded ones proceed; failed charge continues own dunning
BRD.md §US-16.2: Given a charge fails while others succeed, When creating the order, Then only the succeeded subscriptions' line items are included
- compliant
brd-us-16-3-bundle-order-staff-notesBundled BC orders have staff_notes showing [BUNDLE] and contributing subscription IDs
BRD.md §US-16.3: Given a bundled order exists, When I view it in BC, Then the staff_notes show [BUNDLE] and list contributing subscription IDs
- partial
brd-us-16-3-admin-bundle-detailAdmin bundle detail view: line-item → subscription attribution + lifecycle events
BRD.md §US-16.3: Given I click into the bundle detail in our admin, When it renders, Then I see line-item → subscription attribution
- partial
brd-us-16-4-shipping-cost-proportionalBundle shipping rate allocated proportionally by line-item value across contributing subscription charges
BRD.md §US-16.4: Given a bundle renewal runs, When shipping is quoted, Then the single shipping rate is applied to the bundle order and charged proportionally
- manual review
brd-us-16-5-decoupled-billing-shipping-cadencePhase 3 deferred-build scaffold: plans.billing_cadence + plans.shipping_cadence decoupled; billing scheduler fires MIT charges on billing cadence; fulfillment scheduler fires BC orders on shipping cadence (no additional MIT); delivery_instances rows track per-shipment fulfillment
BRD.md §US-16.5: Phase 3 — decoupled billing and shipping cadence (WCSubs feature-request #1, 727 votes — top blue-ocean differentiator). Architecture per future ADR.
- compliant
brd-us-17-1-magic-link-email-sentEntering email on portal login page sends a magic link email
BRD.md §US-17.1: Given I enter my email on the portal login page, When I submit, Then a magic link is emailed
- compliant
brd-us-17-1-magic-link-login-within-15minClicking valid magic link within 15 minutes logs subscriber in with session cookie scoped to portal subdomain
BRD.md §US-17.1: Given I click the link within 15 minutes, When it resolves, Then I'm logged in with a session cookie scoped to the portal subdomain
- compliant
brd-us-17-1-expired-link-shows-reuse-pageExpired or reused magic link shows "Request a new link" page
BRD.md §US-17.1: Given the link is expired or reused, When I click, Then I'm shown a "Request a new link" page
- compliant
brd-us-17-2-storefront-ssoSubscriber logged into BC storefront can access portal without re-authenticating via BC customer JWT exchange
BRD.md §US-17.2: Given I am logged into the BC storefront and click "Manage subscriptions," When I land on our portal, Then the BC customer JWT is exchanged for a portal session
- manual review
brd-us-17-3-custom-domainPortal served from merchant custom subdomain with provisioned TLS cert after CNAME verification
BRD.md §US-17.3: Given I add a CNAME pointing to our portal hostname, When I verify it, Then the portal serves on my custom domain
- compliant
brd-us-17-4-portal-themingPortal applies merchant logo, colors, and Google Font via CSS variables when subscriber loads the portal
BRD.md §US-17.4: Given I upload logo + pick colors + pick a Google Font in admin, When a subscriber loads the portal, Then those values apply via CSS variables
- compliant
brd-us-17-5-sdk-get-subscriptionsHeadless SDK getSubscriptions(customerToken) returns typed subscription list
BRD.md §US-17.5: Given the headless SDK is installed, When I call getSubscriptions(customerToken), Then I get a typed list
- compliant
brd-us-17-5-sdk-perform-actionHeadless SDK performAction(subscriptionId, action, params) applies action server-side and returns updated subscription
BRD.md §US-17.5: Given I call performAction(subscriptionId, 'pause', params), When it resolves, Then the action is applied server-side and the updated subscription is returned
- compliant
brd-us-18-1-skip-marks-abandonedSkip next charge marks upcoming charge as abandoned (reason: skipped) and advances next_charge_at by one interval
BRD.md §US-18.1: Given I view a subscription, When I click "Skip next," Then the upcoming charge is marked abandoned (reason: skipped) and next_charge_at advances one interval
- compliant
brd-us-18-1-unskip-within-24hAlready-skipped charge shows Unskip CTA for up to 24h before originally scheduled date
BRD.md §US-18.1: Given I have already skipped the next charge, When I view the subscription, Then the CTA changes to "Unskip" for up to 24h before the originally scheduled date
- compliant
brd-us-18-2-swap-updates-variantSelecting eligible alternate product/variant updates subscription to new product/variant from next cycle forward
BRD.md §US-18.2: Given plan-eligible alternate products/variants exist, When I click "Swap" and pick an alternate, Then the subscription updates to the new product/variant from the next cycle forward
- compliant
brd-us-18-2-swap-price-previewAlternate with different price shows new estimated charge amount before confirm
BRD.md §US-18.2: Given the alternate has a different price, When I confirm, Then I see the new estimated charge amount before confirming
- manual review
brd-us-18-2-swap-disabled-when-lockedSwap CTA is disabled with explainer when no eligible alternate exists (plan locked to single variant)
BRD.md §US-18.2: Given no alternate is eligible, When I click swap, Then the CTA is disabled with an explainer
- compliant
brd-us-18-3-pause-with-resume-dateChoosing a resume date moves subscription to paused status with no charges until resume
BRD.md §US-18.3: Given I click "Pause," When I pick a resume date, Then subscription status moves to paused and no charges occur until resume
- compliant
brd-us-18-3-pause-indefinitePause without resume date (indefinite) sets status to paused until manual resume
BRD.md §US-18.3: Given I did not pick a resume date (indefinite pause), When I save, Then status is paused until I manually resume
- compliant
brd-us-18-4-reschedule-updates-next-chargeRescheduling to a date within allowed window (+90 days) updates next_charge_at and shifts all future charges
BRD.md §US-18.4: Given I click "Reschedule," When I pick a date within the allowed window, Then next_charge_at updates and all future charges shift
- compliant
brd-us-18-5-cancel-captures-reasonCancel flow captures reason first before showing interventions
BRD.md §US-18.5: Given I click "Cancel," When the flow starts, Then I capture a reason first
- compliant
brd-us-18-5-intervention-accept-applies-actionAccepting an offered intervention (e.g. pause) applies the action and exits cancel flow without cancelling
BRD.md §US-18.5: Given an intervention is shown and I accept it, When I confirm, Then the action is applied and I don't cancel
- compliant
brd-us-18-5-decline-intervention-cancelsDeclining interventions and confirming cancel moves subscription to cancelled with cancel reason logged
BRD.md §US-18.5: Given I decline interventions and confirm cancel, When I save, Then subscription status moves to cancelled with cancel reason logged
- compliant
brd-us-18-6-quantity-updateIncrementing quantity prices next and all subsequent charges at new quantity
BRD.md §US-18.6: Given I view my subscription, When I increment quantity and save, Then the next charge and all subsequent are priced at new quantity
- non-compliant
brd-us-18-6-quantity-silent-clampOut-of-range quantity is silently clamped to [1..100] rather than rejected; audit event records original_quantity + clamped:true
BRD.md §US-18.6 / Tier α polish decision e21ea786: silent-clamp closes UX gap where subscribers typing "0" got a 400
- compliant
brd-us-18-3-auto-resume-cronPaused subscriptions with current_period_end <= now() are automatically flipped to active by a scheduled sweep
BRD.md §US-18.3: Given pause period has elapsed, When the scheduler fires, Then subscription auto-resumes and subscription.resumed event is logged
- compliant
brd-us-18-lazy-fill-on-skipSkipping a delivery instance immediately triggers fillForSubscription to refresh the rolling horizon rather than waiting for the next cron
Tier α polish decision e21ea786: lazy-fill horizon materializes immediately on skip action
- compliant
brd-us-18-bundle-lock-guardSkip/pause/swap handlers exclude the affected charge from any non-terminal bundle (pending/locked/partially_failed) via unbundleCharge before applying the lifecycle action; materialized bundles return 409 (Epic 12 refund path).
docs/architecture/order-bundling.md §5 (Conflict Resolution Protocol) + ADR-0049 + Hive #887; Epic-18 must not desync bundle composition by skipping/pausing/swapping a member without unbundling.
- compliant
brd-us-18-7-reactivate-within-90dSubscriber can reactivate cancelled subscription within 90 days, restoring active status with fresh next_charge_at
BRD.md §US-18.7: Given I have a cancelled subscription < 90 days old, When I click "Reactivate" in the portal, Then status returns to active with a fresh next_charge_at
- manual review
brd-us-18-7-reactivate-inactive-plan-promptReactivating when product or plan is no longer active prompts subscriber to pick an alternate
BRD.md §US-18.7: Given the product or plan is no longer active, When I reactivate, Then I'm prompted to pick an alternate
- compliant
brd-us-18-8-interval-changeChanging interval applies new cadence from next cycle onward with recomputed next_charge_at
BRD.md §US-18.8: Given the plan offers multiple intervals, When I change interval and save, Then the new interval applies from the next cycle onward and next_charge_at is recomputed
- partial
brd-us-18-9-term-nudge-emailterm_end_at - nudge_lead_days fires "renew your subscription" email and flags subscription pending_renewal_decision
BRD.md §US-18.9: Given a subscription has term_end_at set, When term_end_at - merchant.nudge_lead_days arrives, Then a "renew your subscription" email fires
- partial
brd-us-18-9-term-confirm-advances-endSubscriber confirming re-up advances term_end_at by one term and returns subscription to standard renewal flow
BRD.md §US-18.9: Given the subscriber confirms re-up, When confirmation is recorded, Then term_end_at advances by one term and subscription returns to standard renewal flow
- compliant
brd-us-18-9-term-decline-completesSubscriber declining or ignoring nudge through term_end_at transitions subscription to completed (terminal)
BRD.md §US-18.9: Given the subscriber declines or ignores the nudge through term_end_at, When the term ends, Then the subscription transitions to completed
- compliant
brd-us-18-10-cancel-lock-portal-enforcementPortal cancel route returns 409 cancel_locked with lock_expires_at while plan-commitment lock is active; admin override available via POST /api/v1/admin/subscriptions/:id/force-cancel with reason capture and audit emission
BRD.md §US-18.10: Given lock_expires_at > now() and early_cancel_policy=block, When the subscriber attempts cancel, Then 409 is returned; manager/admin role can override via admin-cancel with required reason; audit event subscription.admin_cancel emitted.
- compliant
brd-us-19-1-pm-update-per-subscriptionSubscriber updates PM on a subscription via processor-hosted tokenization form; payment_method_ref updated; dunning retries reset on PM update
BRD.md §US-19.1: Given I click "Update payment method" on a subscription, When the secure form loads, Then I enter new card details and submit; update succeeds and dunning retries reset
- compliant
brd-us-19-2-default-vs-per-sub-pmSubscriber can update default PM (applies to all subs) or per-subscription PM (single sub only)
BRD.md §US-19.2: Given I have 3 subscriptions, When I update "Default PM," Then all 3 subs point at the new default; per-sub update affects only that sub
- compliant
brd-us-19-3-shipping-address-updateSubscriber updates shipping address; next charge uses new address for tax/shipping recalc; blocked with message if plan disallows new country
BRD.md §US-19.3: Given I open address editor, When I save a new address, Then the next charge uses that address for tax/shipping recalc
- manual review
brd-us-19-4-billing-address-updateSubscriber can enter separate billing address stored with the PM
BRD.md §US-19.4: Given I toggle "Different billing address" in the PM editor, When I enter and save, Then the billing address is stored with the PM
- manual review
brd-us-19-5-subscriber-preferencesSubscriber records preferences (allergens, dislikes, sizes) used by merchant curation logic; exposed via API and merchant admin
BRD.md §US-19.5: Given the merchant has enabled the preferences schema, When I edit preferences in the portal, Then the updated values are available to the merchant's curation logic
- compliant
brd-us-19-6-pm-update-all-subscriptions-atomicCustomer-level PM update atomically applies to all N subscriptions; single events row with all affected subscription IDs; partial-update failure state not exposed
BRD.md §US-19.6: Given I have N active subscriptions, When I update my PM at the customer/wallet level, Then all N subscriptions reflect the new PM by next renewal with atomic semantics
- compliant
brd-us-20-1-cancel-reason-persistedCancel flow presents merchant-configured reason list + "Other"; selected reason persisted with the cancel event
BRD.md §US-20.1: Given I click "Cancel," When the flow starts, Then I see a radio list of merchant-configured reasons + "Other"; reason is persisted with the cancel event
- compliant
brd-us-20-2-pause-intervention"Don't need right now" reason triggers pause-offer screen with 30/60/90 day options; accepting applies pause and exits cancel flow
BRD.md §US-20.2: Given reason = "Don't need right now," When the flow proceeds, Then an offer screen shows "Pause 30/60/90 days" options
- manual review
brd-us-20-3-discount-intervention"Too expensive" reason triggers merchant-configured discount offer; acceptance applies time-limited promotion; frequency cap enforced
BRD.md §US-20.3: Given reason = "Too expensive," When the flow proceeds, Then a merchant-configured discount is offered; acceptance is frequency-capped per customer
- compliant
brd-us-20-4-cadence-change-intervention"Ordering too much" reason offers longer available intervals; acceptance changes interval and exits cancel flow
BRD.md §US-20.4: Given reason = "Ordering too much," When the flow proceeds, Then longer available intervals are offered; accepting changes cadence
- manual review
brd-us-20-5-support-escalation-intervention"Product issue" reason routes subscriber to merchant-configured support handoff (Gorgias ticket, email, or phone link)
BRD.md §US-20.5: Given reason = "Product issue," When the flow proceeds, Then a merchant-configured support handoff happens
- manual review
brd-us-20-6-intervention-ab-testingAdmin-configured intervention variants A/B tested; save rates attributable per variant
BRD.md §US-20.6: Given I create variants in admin, When subscribers enter the flow, Then they're randomized and save rates are attributable per variant
- compliant
brd-us-21-1-dashboard-kpi-tilesDashboard shows MRR, active subs, new subs (30d), cancels (30d), gross churn %, net revenue churn % tiles
BRD.md §US-21.1: Given I open the dashboard, When it renders, Then tiles show: MRR (with trailing 30-day delta), active subs, new subs (30d), cancels (30d), gross churn %, net revenue churn %
- compliant
brd-us-21-1-dashboard-tile-drilldownClicking a KPI tile navigates to the corresponding filtered subscriptions or exceptions list with time-series sparkline visible on the tile itself
BRD.md §US-21.1: Given I click a tile, When it drills in, Then I see time-series and supporting detail
- compliant
brd-us-21-2-upcoming-charges-panelDashboard shows upcoming charge $ value, count, and mini-chart for next 7 and 30 days
BRD.md §US-21.2: Given upcoming charges exist, When the panel renders, Then I see $ value, charge count, and a mini-chart over the period
- compliant
brd-us-21-3-exception-queue-groupedException queue groups items by type (failed charge, OOS renewal, reconciliation drift) sorted by MRR impact and days in exception
BRD.md §US-21.3: Given exceptions exist, When I open the queue, Then items are grouped by type and sorted by impact
- compliant
brd-us-21-3-exception-mark-resolvedResolving an exception with a note removes it from the queue and event-logs the resolution
BRD.md §US-21.3: Given I resolve an item, When I click "Mark resolved" with a note, Then it leaves the queue and Event-logs the resolution
- compliant
brd-us-21-4-subscription-list-paginatedSubscription list shows paginated table with customer, plan, status, MRR, next charge, cycles completed columns
BRD.md §US-21.4: Given I open "Subscriptions," When it loads, Then I see a paginated table with columns: customer, plan, status, MRR, next charge, cycles completed
- compliant
brd-us-21-4-subscription-filters-shareable-urlApplied filters (status, plan, query) are reflected in the URL as query params; sharing the URL restores the filter state
BRD.md §US-21.4: Given I apply filters (status=past_due, plan=X), When results update, Then the URL is shareable
- compliant
brd-us-21-5-bulk-skip-pause-cancelMerchant can bulk-skip, bulk-pause, or bulk-cancel subscriptions matching a filter via a single admin endpoint that returns a per-id succeeded/failed result shape
BRD.md §US-21.5: Given I filter and select rows, When I click "Bulk action → Skip next," Then I confirm scope and the action is queued and progress-tracked
- compliant
brd-us-21-6-subscription-detail-full-contextSubscription detail shows state, next charge, payment method health, addresses, charge history, event timeline, and manual-action buttons
BRD.md §US-21.6: Given I click a subscription, When the detail opens, Then I see: state, next charge, payment method health, shipping/billing addresses, charge history with links to BC orders, event timeline, manual-action buttons
- compliant
brd-us-21-7-csv-exportFiltered subscription list exports as CSV via a streaming admin endpoint; admin SPA exposes an "Export to CSV" CTA that downloads the result
BRD.md §US-21.7: Given I click "Export" on a filtered list, When the export builds, Then a background job generates a CSV and I'm notified when it's ready
- compliant
brd-us-21-8-cohort-retention-heatmapAnalytics page shows cohort × cycle retention table with CSS color-scale approximating a heatmap (acquisition month rows × cycle columns, % retained values)
BRD.md §US-21.8: Given I open Analytics, When the cohort tab loads, Then I see a heatmap of acquisition cohort × cycle (% retained)
- compliant
brd-us-21-8-ltv-projectionsAnalytics page shows per-plan estimated 12- and 24-month LTV using geometric series from average charge amount and observed monthly survival rate
BRD.md §US-21.8: Given I open LTV, When it loads, Then I see estimated 12/24-month LTV per plan using cohort-derived survival curves
- compliant
brd-us-22-1-manual-subscription-createSupport can create subscription manually via admin wizard (customer + plan + PM + schedule), marked with manual origin event
BRD.md §US-22.1: Given I click "New subscription," When I pick customer + plan + PM + schedule, Then the subscription is created with an Event marking the manual origin
- compliant
brd-us-22-2-customer-extension-panelBC customer edit page shows Subscriptions extension panel listing all customer subscriptions with deep-links
BRD.md §US-22.2: Given I open a BC customer, When the extension panel loads, Then it lists all the customer's subscriptions with deep-links to our admin
- compliant
brd-us-22-3-impersonate-subscriberSupport can view subscriber portal in read-only impersonation mode with all actions logged as actor=support
BRD.md §US-22.3: Given I click "View as subscriber," When impersonation starts, Then I see the portal in read-only mode and my actions are logged as actor=support
- compliant
brd-us-22-4-bulk-csv-createSupport can create subscriptions in bulk via CSV upload, staging then activating on confirmation
BRD.md §US-22.4: Given I upload a valid CSV, When the job processes, Then subscriptions are created in a staging state and activated on confirmation
- compliant
brd-us-22-5-force-chargeSupport can force an immediate charge with confirmation and audit note
BRD.md §US-22.5: Given I click "Force charge now," When I confirm, Then an immediate charge is enqueued with an audit note
- compliant
brd-us-22-5-force-refundSupport can force an immediate refund with amount, reason, and processor call
BRD.md §US-22.5: Given I click "Force refund," When I confirm the amount and reason, Then the processor is called immediately
- compliant
brd-us-22-6-subscription-noteSupport can add internal notes to a subscription; note appears timestamped and attributed on detail view
BRD.md §US-22.6: Given I add a note, When it saves, Then it appears on the subscription detail, timestamped and attributed to me
- compliant
brd-us-22-7-allotment-grant-createAdmin can issue AllotmentGrant rows with unit_type, amount_per_period, refresh_cadence, rollover_policy, expires_at
BRD.md §US-22.7: Given an admin-grant form, When I select target customer or buyer-org with parameters, Then an AllotmentGrant row is created
- compliant
brd-us-22-7-allotment-grant-detailAllotmentGrant detail shows current_balance, next_refresh_at, debit history, and admin actions (suspend/revoke/manual-refresh/override-balance)
BRD.md §US-22.7: Given an existing grant, When I view the detail page, Then I see current_balance, next_refresh_at, the full debit history, and admin actions
- compliant
brd-us-22-7-allotment-grant-revokeRevoking an AllotmentGrant sets status=revoked, blocks further debits, and hides balance from portal
BRD.md §US-22.7: Given I revoke a grant, When confirmed, Then status='revoked', no further debits accepted, subscriber-facing balance disappears
- compliant
brd-us-22-8-custom-field-view-editAdmin subscription detail renders active CustomFieldDefinitions with current values and type-appropriate inputs
BRD.md §US-22.8: Given a subscription with active CustomFieldDefinitions in scope, When I view the admin detail page, Then each defined field renders with its current value
- compliant
brd-us-22-8-custom-field-validation-blocks-saveInvalid custom field (regex mismatch, type mismatch, required-but-empty) blocks save with inline error
BRD.md §US-22.8: Given I edit a field that fails validation, When I attempt save, Then save is blocked with the field-level error displayed inline
- compliant
brd-us-22-8-custom-field-save-audit-eventSaving custom field updates metadata.custom_fields, fires audit Event with actor_id and value diff
BRD.md §US-22.8: Given I save valid changes, When the API persists, Then subscriptions.metadata.custom_fields updates and an audit Event records actor_id and value diff
- compliant
brd-us-22-8-archived-field-read-onlyArchived CustomFieldDefinition renders read-only on subscription detail with "field archived" hint, data preserved
BRD.md §US-22.8: Given a field is archived after a subscription has data, When I view the page, Then the archived field renders read-only with a "field archived" hint
- compliant
brd-us-22-9-b2b-admin-enrollmentSupport can enroll B2B buyer-org in subscription via admin impersonation with multi-actor assignment (payer, beneficiary, manager)
BRD.md §US-22.9: Given a B2B Edition buyer-org and CS-rep, When I impersonate and navigate to "Create Subscription," Then I can configure and confirm B2B subscription
- compliant
brd-us-23-1-transactional-emails-lifecycleLifecycle events (welcome, upcoming charge, charge succeeded/failed, paused/resumed/cancelled, shipment) trigger emails via Resend with merchant template + variables
BRD.md §US-23.1: Given each lifecycle event fires, When it matches an enabled notification type, Then an email is sent via Resend with merchant's template + variables
- manual review
brd-us-23-2-sms-notificationsSubscriber opt-in SMS for time-sensitive events (charge failed, shipment shipped) via Twilio
BRD.md §US-23.2: Given I opt into SMS in the portal, When relevant events fire, Then SMS is sent via Twilio
- compliant
brd-us-23-3-template-editorMerchant edits email templates (subject, body, branding) with Markdown/MJML editor and sample-data preview
BRD.md §US-23.3: Given I open template editor, When I edit Markdown/MJML with variable placeholders, Then I can preview with sample data and save
- manual review
brd-us-23-4-merchant-alerts-digestsConfigurable threshold alerts for high-value events (chargeback, large cancel, dunning spike) and daily digest with key metrics and exception queue summary
BRD.md §US-23.4: Given I set alert thresholds, When events cross them, Then an email/Slack alert fires; daily digest includes key metrics and exception queue summary
- manual review
brd-us-23-5-klaviyo-integrationSubscription lifecycle events synced to Klaviyo (Subscription Created, Cancelled, Charge Succeeded/Failed, etc.) with standard properties
BRD.md §US-23.5: Given Klaviyo is connected, When subscription events fire, Then Klaviyo profile events are pushed with standard properties
- manual review
brd-us-23-6-helpdesk-integrationCancel with reason "Product issue" auto-creates helpdesk ticket in Gorgias/Zendesk with subscription + order context
BRD.md §US-23.6: Given a cancel with reason "Product issue" occurs, When the event fires, Then a ticket is auto-created in Gorgias/Zendesk with subscription + order context
- manual review
brd-us-23-7-accounting-integrationsSubscription revenue posted to NetSuite/QuickBooks/Xero as journal entries with subscription metadata on charge success
BRD.md §US-23.7: Given an accounting integration is connected, When charges succeed, Then journal entries post with subscription metadata
- compliant
brd-us-23-8-outbound-webhooksDeveloper-registered webhook endpoints receive subscription lifecycle events POST with HMAC signature, retried with exponential backoff on failure
BRD.md §US-23.8: Given I register a webhook endpoint, When matching events fire, Then they're POSTed with signature header and retried on failure with exponential backoff
- compliant
brd-us-23-9-custom-sending-domainMerchant configures custom domain for email sending; Resend domain provisioned with DKIM; emails send from merchant domain with DMARC alignment
BRD.md §US-23.9: Given I configure notifications.{merchant.com} in settings, When I trigger DNS verification, Then we provision a Resend domain + DKIM record
- manual review
brd-us-23-12-magic-link-email-pipelinePortal login magic-link sent with 32-byte opaque token, 15-min TTL, single-use bcrypt lookup; dunning PM-update link has 7-day TTL; domain mismatch rejected
BRD.md §US-23.12: Given a subscriber requests portal login, When the magic-link email is sent, Then the link contains a 32-byte opaque token with TTL 15 min, single-use, bcrypt-lookup server-side
- compliant
brd-us-23-16-resend-webhook-ingestionResend send-outcome webhooks (bounced, complained, delivered, opened, clicked) ingested, verified by signature, update email_sends and suppression tables
BRD.md §US-23.16: Given Resend POSTs email.bounced / email.complained / email.delivered / email.opened / email.clicked, When verified by signature, Then we update email_sends and downstream tables
- partial
brd-us-23-10-deliverability-monitoringPer-merchant-domain bounce/complaint rolling-window threshold monitoring drives send throttling + alerts; recovery auto-resumes send
BRD.md §US-23.10: Given a merchant domain crosses bounce > 5% (or complaint 0.1%/0.3% warn/hard), When detected, Then sending throttles and an alert fires; recovery auto-resumes.
- manual review
brd-us-23-11-suppression-list-managementSubscriber email opt-out + hard-bounce / complaint events suppress recipient across the merchant; transactional bypasses marketing-only opt-out per PRD §9.8 routing rule; re-opt-in via portal removes the suppression and emits subscriber.resubscribed
BRD.md §US-23.11: Given a subscriber unsubscribes or hard-bounces, When the event lands, Then we suppress the recipient for that merchant; re-opt-in via portal removes the entry and emits subscriber.resubscribed.
- compliant
brd-us-23-13-decline-reason-translated-dunningDunning email subject + body translate issuer decline codes (card_declined, insufficient_funds, expired_card, do_not_honor, requires_action) to subscriber-friendly copy; SCA failures route to magic-link SCA-completion flow; hard-fraud cancels without email (US-23.6)
BRD.md §US-23.13: Given a charge fails with card_declined / insufficient_funds / expired_card / do_not_honor / requires_action, When the dunning email fires, Then the subject + body translate the issuer code to subscriber-friendly copy.
- compliant
brd-us-23-14-template-render-fallbackMerchant template compile/render failures fall back to a system template + emit template.render_failed event with merchant alert; unallowed-variable references substitute safe placeholders
BRD.md §US-23.14: Given a merchant template fails to compile, When the render runs, Then we render with a system fallback template + send + emit template.render_failed event with merchant alert.
- compliant
brd-us-23-15-idempotent-send-dedupePer-send idempotency key {template_key}:{recipient}:{source_event_id} guards workflow-restart re-sends via the email_idempotency table; Resend Idempotency-Key header round-trips our send key so Resend collapses any race that slips past the pre-send DB check
BRD.md §US-23.15: Given a workflow restart re-enters the send step, When the per-send key is presented, Then the second attempt returns the original send record without re-sending.
- manual review
brd-us-23-17-email-localization-frameworkPer-locale email templates resolved from subscriber language preference (portal pref > store default > Accept-Language); Intl.NumberFormat / DateTimeFormat for numeric/date/currency; RTL languages emit dir="rtl" MJML; renderer accepts locale parameter from MVP onward (architecture-now, content-later)
BRD.md §US-23.17: Given a subscriber language preference, When a template fires, Then we render the matching (merchant × template × locale) cell, falling back to merchant default locale.
- manual review
brd-us-24-1-company-account-subscriptionB2B Edition subscriptions attached to company_id not customer_id; all authorized company users see company subscriptions in portal
BRD.md §US-24.1: Given B2B Edition is detected, When a subscription is created via a company-buyer checkout, Then it is associated with company_id, not customer_id
- compliant
brd-us-24-2-role-based-portal-permissionsB2B buyer with view_only role sees empty allowed_actions list; buyer/admin roles see full action set; effective_role + allowed_actions returned from server so UI needs no role logic
BRD.md §US-24.2: Given company roles are defined in BC B2B Edition, When a buyer with view_only role opens the portal, Then action buttons are disabled and labeled "Requires higher permissions."
- partial
brd-us-24-3-subscription-change-approvalB2B buyer submits change request via portal; pending → approved/rejected by admin; subscription_change_requests table tracks state; view_only buyers blocked from submitting
BRD.md §US-24.3: Given approval workflows are enabled, When a buyer initiates a change, Then the change enters a pending state awaiting approval
- compliant
brd-us-24-4-po-payment-methodPO-payment subscriptions have payment_method_id=null + metadata.payment_method_type=po + po_number + net_terms_days; renewal worker should generate BC orders with payment_status: pending (renewal worker update is an [Operator-Task])
BRD.md §US-24.4: Given BC B2B supports PO payments, When a company subscribes with PO, Then payment_method_type: po is stored and renewal charges generate BC orders with payment_status: pending
- compliant
brd-us-24-9-b2b-admin-only-enrollmentB2B Edition PDP shows B2B widget variant with "contact CS rep" CTA; CS rep enrollment via Epic 22 admin tools; subscriptions appear in Buyer Portal with managed-by annotation
BRD.md §US-24.9: Given a B2B-Edition store, When a buyer browses a subscription-enabled PDP, Then the standard subscription widget renders a B2B variant with "contact your CS rep" CTA
- manual review
brd-us-24-11-catalyst-b2b-composite-pdpCatalyst PDP with B2B Edition Buyer Portal SDK shows B2C widget for non-B2B sessions and B2B variant for B2B sessions; window.B2B SDK presence probed by widget init
BRD.md §US-24.11: Given a Catalyst storefront with the B2B Edition Buyer Portal SDK loaded, When a B2C shopper views the PDP, Then the standard B2C subscription widget renders; B2B buyer sees B2B variant
- manual review
brd-us-24-5-buyer-hierarchy-mrr-rollupDashboard MRR rolls up across BC B2B parent-company hierarchy; merchant filtering by parent company aggregates child-company subscriptions
BRD.md §US-24.5: Given BC B2B hierarchy is defined, When I view the dashboard filtered by a parent company, Then MRR rolls up across all child companies.
- manual review
brd-us-24-6-volume-tier-renewal-pricingRenewals read negotiated volume-tier pricing from BC B2B at charge time; applicable tier applies to enterprise-deal renewals
BRD.md §US-24.6: Given a company has volume-tier pricing in BC, When renewals compute, Then the applicable tier is read at renewal time.
- manual review
brd-us-24-7-contract-term-commitmentsContract-term commitments with auto-renewal + notice-period semantics; B2B buyer cancellation within notice window is blocked or routes to merchant-configured legal workflow
BRD.md §US-24.7: Given a contract term of 12 months with 60-day notice, When a buyer tries to cancel with < 60 days until term end, Then cancellation is blocked or routes to legal workflow per merchant config.
- manual review
brd-us-24-8-multi-location-shippingSingle B2B subscription ships to multiple company locations on a cycle (e.g., monthly delivery to 5 offices); each cycle creates one order per location with agreed quantity; total charge = sum of all locations
BRD.md §US-24.8: Given a multi-location subscription is configured, When a cycle renews, Then one order per location is created with the agreed quantity; total charge equals the sum.
- manual review
brd-us-24-10-org-admin-actor-roleorg_admin role reserved in multi-actor extension schema; Phase 1 permission-resolution path treats org_admin as a recognised role enum value; per-org processor_connection_ref column reserved for marketplace MoR v2 routing (ADR-0022)
BRD.md §US-24.10: Given a B2B subscription with actors[].role=org_admin, When the org_admin views the Buyer Portal, Then they see all subscriptions for their org and can act per merchant-configured permissions.
- partial
brd-us-25-1-subscription-only-couponsMerchant can mark a promotion as subscription_only via admin UI toggle; flag stored in promotion_settings table; storefront visibility endpoint gates coupon display by cart composition
BRD.md §US-25.1: Given I create a coupon with applies_to: subscription_only, When a shopper applies it in cart, Then it discounts only subscription line items; recurring: true carries through renewals
- compliant
brd-us-25-2-subscribe-and-save-badgePlan whose amount_cents is below the one-time product price renders a "Subscribe and save X%" badge in the storefront widget; the discount applies at checkout without coupon-code entry because the cart line uses plan.amount_cents directly.
BRD.md §US-25.2: Given a plan's pricing strategy is fixed_discount_pct: 10, When a shopper views a subscribable PDP, Then the widget shows "Subscribe and save 10%" prominently; the discount applies without coupon code at checkout.
- partial
brd-us-25-3-cycle-scoped-discountPlan-level cycle-scoped discount declaration (cycle_discount_pct/scope/count) snapshots onto subscription_discounts at creation with cycle_min/cycle_max bounds. Scheduler applies the discount only when current renewal cycle is within range. Plan edits do NOT affect in-flight subscriptions (lock-at-creation per ADR-0052).
BRD.md §US-25.3: Given a plan with cycle_discount_scope=first_n_cycles, count=3, pct=50, When cycle 1/2/3 charges compute, Then discount applies; cycle 4+ does not. Given a merchant edits the plan after sub creation, Then in-flight subscriptions keep the original discount.
- compliant
brd-us-25-4-tenure-loyalty-discountTenure loyalty tiers ride the subscription_promotions table as `tiered_cycles` window; tier ladder reconfigurable mid-flight via re_resolve lock_policy; scheduler emits `loyalty.tier_reached` on boundary cycle and `loyalty.discount_applied` every cycle the discount fires; cycles_completed preservation via reactivate.ts implements the AC2 rejoin path.
BRD.md §US-25.4: Given tenure tiers are configured, When a renewal computes, Then the discount matching current cycle count is applied; Given subscriber cancels and rejoins within N days, Then prior cycle count is restored
- partial
brd-us-25-5-promotion-stacking-rulesTwo-layer stacking rule enforcement: resolveDiscountStack (subscription_discounts) and evaluateSubscriptionPromotions (subscription_promotions) each implement exclusive (highest computed value wins) + stackable (additive, capped). Exclusive disambiguation per Hive #900: highest computed amount_cents for the specific charge, not abstract discount_percent.
BRD.md §US-25.5: Given I set stacking_rule: exclusive on a coupon, When another discount is present on a renewal, Then only the higher-value one applies; Given stacking_rule: stackable, Then they are combined additively
- partial
brd-us-25-6-promotional-free-shippingFree-shipping rule with cycle-condition (from_cycle) and/or order-value threshold (min_amount_cents) zeros the shipping line on the materialized BC order (single + bundle paths).
BRD.md §US-25.6 + Hive #901 body
- partial
brd-us-25-7-cycle-modulo-gift-promogift_item promo with application_window=all_cycles + config.cycle_modulo:N fires on every Nth cycle, injecting a $0 product line into the BC order on those cycles. OOS fallback (skip/substitute) configurable per promo.
BRD.md §US-25.7 + Hive #901 body §US-25.7
- compliant
brd-us-25-8-gift-with-subscriptiongift_item promo with application_window=first_cycle_only injects a $0 product line into the first BC order on subscription creation; no further fires.
BRD.md §US-25.8 + Hive #901 body §US-25.8
- partial
brd-us-25-1-subscription-promotion-projection-substrateTwo-table subscription-promotion substrate: subscription_promotions (coupon definition) + subscription_promotion_applications (per-charge attribution). Replaces spike #117 jsonb-on-charges proposal with relational tables for FK integrity, reporting joins, and refund-on-cancel joins.
BRD.md §US-25.1 + promo-projection.md §3.1, §3.3 + ADR-0053
- compliant
brd-us-25-1-promotion-lock-policy-derivationlock_policy is derived from application_window at insert time (not merchant-settable): first_cycle_only/first_n_cycles -> lock_at_creation; all_cycles/tiered_cycles/event_triggered -> re_resolve. Enforces promo-projection.md §2.1 invariant mechanically.
promo-projection.md §2.1 + ADR-0053 (lock policy is DERIVED)
- compliant
brd-us-25-1-promotion-crud-apiPromotion CRUD routes: POST/GET/PUT/DELETE /api/v1/admin/subscription-promotions and storefront POST /api/v1/storefront/subscription-promotions/validate-coupon. All 4xx use typed-error helpers.
BRD.md §US-25.1 + Hive #891 §What to implement
- compliant
brd-us-25-1-scheduler-promo-evaluatorScheduler evaluates subscription_promotions on every renewal via evaluateSubscriptionPromotions(); writes subscription_promotion_applications attribution rows in success branch; emits subscription.promotion_applied audit event per attribution. Zero-regression on empty active_promotion_ids.
promo-projection.md §4 (charge-compute algorithm) + Hive #891 step 7+8
- compliant
brd-us-25-1-coupon-attach-on-subscription-creationcreateSubscription() accepts optional coupon_code; resolves against subscription_promotions, validates (active+window+cap+applies_to), attaches to active_promotion_ids, increments current_redemptions. Best-effort: a miss does not roll back the subscription.
BRD.md §US-25.1 + Hive #891 step 6
- partial
brd-us-25-9-promotion-reportingPer-promotion report endpoint + admin UI: redemptions, charges_discounted, total_discount_cents (with subs/bc source breakdown), active subs carrying promo, attributed MRR (from plans.amount_cents), and retention lift (avg cycles_to_cancel partitioned by promo_applied_ever, suppressed below 30 cancellations).
BRD.md §US-25.9 (P2): "Per-promo: redemptions, total discount given, attributable MRR, estimated lift, retention lift" + Hive #903 response schema
- compliant
brd-us-25-portal-discount-interventionPortal cancel-flow discount intervention records one-time percent discount on next charge via subscription_discounts table
BRD.md §US-20.3 / §US-25: Given a cancel-flow "Too expensive" discount intervention is accepted, When saved, Then a subscription_discounts row is created for the next charge
- compliant
brd-us-26-1-category-inclusion-ruleinclude_categories rule causes subscribe widget to render only on products in those categories
BRD.md §US-26.1: Given a rule include_categories: [5, 12], When I enable a plan with this rule, Then the subscribe widget renders only on products in those categories
- compliant
brd-us-26-1-late-added-product-auto-eligibleProduct added to an included category later automatically becomes subscribable without rule update
BRD.md §US-26.1: Given a product is added to an included category later, When I view it on storefront, Then it automatically becomes subscribable
- compliant
brd-us-26-2-product-exclusion-no-widgetExcluded products (by product, category, or brand rule) show no subscribe widget on storefront
BRD.md §US-26.2: Given a rule exclude_products/categories/brands, When a shopper views an excluded product, Then no subscribe widget renders
- compliant
brd-us-26-2-exclusion-wins-over-inclusionExclusion wins when a product matches both inclusion and exclusion lists
BRD.md §US-26.2: Given exclusion lists + inclusion lists are both set, When a product matches both, Then exclusion wins
- manual review
brd-us-26-3-variant-excluded-widget-hidesSelecting excluded variant switches widget to "one-time only" and hides subscribe option
BRD.md §US-26.3: Given a per-variant eligibility list, When a shopper picks an excluded variant, Then the widget switches to "one-time only"
- compliant
brd-us-26-4-custom-field-eligibilityrequire_custom_field rule controls widget visibility based on BC product custom field value
BRD.md §US-26.4: Given a rule require_custom_field: {name:subscribable, value:true}, When a shopper views a product, Then the widget only renders if the custom field matches
- compliant
brd-us-26-5-customer-group-gatedinclude_customer_groups rule hides subscribe option for non-eligible customer groups
BRD.md §US-26.5: Given a rule include_customer_groups: [vip], When a non-VIP browses, Then the subscribe option is hidden
- compliant
brd-us-26-5-guest-sign-in-cta-apiAPI contract: guest (customer_group_id=0) hitting only-group-gated plans returns excluded:true + excluded_by:customer_group so the widget can switch to a sign-in CTA
BRD.md §US-26.5 (API contract slice): Given a guest shopper and a group-restricted plan, When the storefront plans endpoint resolves, Then it returns plans:[], excluded:true, excluded_by:"customer_group"
- manual review
brd-us-26-5-guest-sign-in-cta-uiStorefront widget renders a sign-in CTA (not a plain empty state) when the API returns excluded_by:customer_group for a guest
BRD.md §US-26.5 (UI rendering slice): Given the API signals excluded_by=customer_group, When the widget renders, Then a sign-in CTA replaces the subscribe surface
- compliant
brd-us-26-6-geo-channel-restrictionallowed_countries rule hides or shows "Not available in your region" for shoppers outside allowed countries
BRD.md §US-26.6: Given a rule allowed_countries: [US], When a shopper browses from outside the US, Then the widget is hidden or shows "Not available in your region"
- partial
brd-us-26-7-qty-min-max-enforcedmin_qty/max_qty rules block subscribe action with clear message when quantity is out of range
BRD.md §US-26.7: Given min_qty: 2, max_qty: 10, When a shopper attempts to subscribe with qty 1 or qty 11, Then the widget blocks the action with a clear message
- compliant
brd-us-26-8-mutex-rule-blocks-conflictingSubscriber with active plan A is blocked from subscribing to mutex-conflicting plan B
BRD.md §US-26.8: Given a mutex rule between products A and B, When a shopper with active A subscription tries to subscribe to B, Then they're blocked with "Already subscribed to a conflicting plan"
- manual review
brd-us-26-9-requires-prior-purchaserequires_prior_purchase rule redirects shopper to prerequisite product flow if they lack required prior purchase
BRD.md §US-26.9: Given a rule requires_prior_purchase: [starter-kit], When a shopper without prior purchase tries to subscribe to Y, Then they're redirected to the prerequisite flow
- compliant
brd-us-26-10-eligibility-audit-toolSupport audit tool shows which eligibility rule evaluated which way for a customer + plan combination
BRD.md §US-26.10: Given a shopper was denied, When Support opens the audit tool and enters customer + plan, Then the tool shows which rule evaluated which way with input values visible
- compliant
brd-us-27-1-public-rest-apiDocumented REST API for subscriptions, charges, plans, events; paginated, filterable; rate-limited with 429 + Retry-After; X-RateLimit-* informational headers on all /api/v1/ responses; OpenAPI spec at /openapi.json (BRD §US-27.1)
BRD.md §US-27.1: Given I have an API key, When I call GET /api/v1/subscriptions?filter[status]=active, Then I get paginated JSON; rate limits return 429 with Retry-After; OpenAPI schema published at /openapi.json
- manual review
brd-us-27-2-api-key-managementMerchant creates scoped API keys (read-subs, write-subs, etc.); revoked keys immediately fail 401
BRD.md §US-27.2: Given I create an API key and pick scopes, When I use the key, Then it works only for those scopes; revoked keys fail 401
- compliant
brd-us-27-3-webhook-subscriptionsDeveloper registers endpoint with event-type filters; events POSTed with HMAC signature; non-2xx retried with exponential backoff up to 24h then dead-lettered
BRD.md §US-27.3: Given I register an endpoint for subscription.created, When the event fires, Then a POST is sent with payload and HMAC signature; non-2xx triggers exponential backoff retries for 24h
- compliant
brd-us-27-4-typescript-sdk@bc-subscriptions/subscriber-sdk exports typed createClient with methods for all subscriber actions; breaking changes announced with deprecation warnings
BRD.md §US-27.4: Given I install @bc-subscriptions/subscriber-sdk, When I import createClient({ token }), Then I get typed methods for all supported subscriber actions
- manual review
brd-us-27-5-graphql-apiGraphQL endpoint at /graphql supports complex reads with nested subscription+charge+order in one request; only requested fields resolved
BRD.md §US-27.5: Given I POST a query to /graphql, When it includes nested resources, Then only requested fields are resolved
- manual review
brd-us-27-7-sandbox-test-storesDeveloper sandboxes provisioned with isolated store, API keys, and test subscriptions without affecting production
BRD.md §US-27.7: Given I request a sandbox, When it's provisioned, Then I get isolated store + API keys + test subscriptions
- compliant
sdk-substrate-scaffold@bc-subscriptions/subscriber-sdk + @bc-subscriptions/react 2-package workspace scaffold + type-generation pipeline + npm release workflow (ADR-0034)
ADR-0034 §Build sequence step 1
- manual review
brd-us-27-6-app-marketplace-extensionsThird-party partner extensions installable into a merchant tenant; sandboxed permission model registers webhooks + admin UI panels per extension (retention apps, analytics, custom interventions)
BRD.md §US-27.6: Given an approved partner extension, When I install it, Then it registers webhooks and admin UI panels under a sandboxed permission model.
- compliant
brd-us-28-1-dsar-data-exportDSAR generates JSON+CSV bundle of all subscriber-scoped data within 30 days; encrypted with passphrase, uploaded with expiring secure link
BRD.md §US-28.1: Given a DSAR is submitted, When processed, Then a downloadable JSON + CSV bundle is generated within 30 days and delivered via secure link
- compliant
brd-us-28-2-data-erasureErasure request anonymizes PII within 30 days (email hashed, name "Erased User", addresses null); financial records retained with PII stripped; processor PM deletion at erasure is an operator task requiring live processor adapter
BRD.md §US-28.2: Given an erasure request, When processed, Then all PII is deleted/anonymized within 30 days; financial records retained with PII stripped per legal basis
- partial
brd-us-28-3-audit-log-exportGET /api/v1/admin/audit-log?from=&to= returns every Event row in date range with actor, action, resource, Convention-12 SCA columns; paginated with cursor
BRD.md §US-28.3: Given I request an export for a date range, When it generates, Then every Event row is included with actor, action, subject, before/after and SCA-defense fields
- compliant
brd-us-28-4-pci-saq-a-complianceCard data collected only by processor hosted element (Stripe Elements/Braintree Hosted Fields); our DB stores only opaque payment_method_ref tokens; no PAN/CVV in schema or API
BRD.md §US-28.4: 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
- manual review
brd-us-28-5-soc2-readinessSOC 2 Type II evidence captured automatically from controls (access reviews, backup tests, incident drills) via compliance tooling
BRD.md §US-28.5: Given SOC 2 controls are defined, When operations run, Then evidence is captured and stored in our compliance tool
- manual review
brd-us-28-6-data-residencyEU-region stores have Postgres and Redis primaries in EU regions
BRD.md §US-28.6: Given a store declares region: eu, When data is stored, Then Postgres and Redis primaries are EU-region
- compliant
bdd-scenario-frameworkVitest scenario layer with defineScenario story helper + separate CI run (Hive #583)
Hive #583 — BDD scenario test layer scaffolding + first seed flow
- compliant
scenario-subscription-lifecycleLifecycle scenario covers create → pause → resume → cancel with event timeline assertions
Hive #583 / BRD §lifecycle flows (US-18.x)
- compliant
scenario-p1-maya-subscription-boxP1 Maya scenarios: annual wine subscription happy-path with U-20 calendar anchor + OOS exception queue surfacing (U-18 partial) + mid-year signup calendar anchor edge case
Hive task fe8d3bd5 / parent synthesis 5cff25f1 / A3 ratification fa3dc5f3 / BRD §US-9.2 + US-10.3 + US-15.1
- compliant
scenario-p2-alex-digital-contentP2 Alex persona BDD scenarios: AllotmentGrant lifecycle (debit + refresh), cancel, mid-cycle upgrade with proration
Hive #8834674a / BRD §US-6.9 allotment, US-5.x cancel, US-7.x upgrade
- compliant
scenario-p3-jordan-giftingP3 Jordan persona BDD scenarios: gift purchase (status=paused + extension + event), gift claim (recipient active), expired claim window (412)
Hive #e9d7f5dc / BRD §US-6.1 gift subscriptions, US-6.2 prepaid
- compliant
scenario-p4-sam-b2b-enterpriseP4 Sam B2B persona BDD scenarios: CS-rep enrollment with PO, commitment_cycles data model (U-17 foundation), buyer-org AllotmentGrant quarterly refresh
Hive #63e46415 / BRD §US-22.9 B2B enrollment (ADR-0023), US-5.7 commitment, US-6.9 allotment
- compliant
scenario-p5-taylor-consumablesP5 Taylor persona BDD scenarios: subscribe-and-save renewal, dunning retry self-heal, dunning exhaustion → cancel
Hive #b980c6b7 / BRD §US-11.1 dunning policy, US-11.2 retry logic, US-5.7 commitment
- compliant
auto-detect-stored-payments-enabled-checkprocessor-auto-detect returns storedPaymentsEnabled field — probes GET /v3/payments/stored-instruments and surfaces warning when disabled
ADR-0037 / P-comp Sub-deliverable B
- compliant
portal-stored-instruments-picker-shippedPortal stored-instruments picker: GET /api/v1/portal/stored-instruments route + StoredInstrumentsPicker.svelte — shoppers pick from vaulted instruments (ADR-0037)
ADR-0037 / P-comp Sub-deliverable C
- compliant
portal-add-stored-instrument-iat-flow-shippedPortal add-PM IAT flow: POST /api/v1/portal/stored-instruments/iat route + AddStoredInstrumentButton.svelte — shopper adds new card via BC hosted flow (ADR-0037)
ADR-0037 / P-comp Sub-deliverable C
- compliant
cross-sub-instrument-selection-fixedPortal stored-instrument select: backend rejects empty subscription_id with invalidBody; UpdatePaymentForm no longer passes subscriptionId="" to picker in cross-sub mode (proposal #774)
proposal #774 / PR follows
- compliant
charge-retry-sweep-async-correctcharge-retry-sweep uses await getAdapter(..., storeHash, env) — 4-arg async call per T1S adapter-registry contract (Hive #799). Sync 3-arg call removed.
Hive #799 / PR #798 sibling fix
- compliant
brd-us-23-1-renewal-email-sentsubscription.renewed event triggers a renewal confirmation email via Resend — email consumer Worker subscribes to subs-events queue and sends to the customer email on file
BRD.md §US-23.1: Given a subscription.renewed event fires, When the email consumer Worker processes the queue message, Then a renewal confirmation email is sent via Resend to the subscriber's email address
- compliant
brd-us-23-email-consumer-queue-bindingEmail consumer Worker is wired to the subs-events queue via [[queues.consumers]] in wrangler.toml
BRD.md §US-23.1 + ADR-0010: email consumer must subscribe to subs-events to receive event outbox messages
- compliant
brd-us-17-1-magic-link-email-consumerportal.magic_link_requested event triggers a magic-link sign-in email via Resend — worker.ts routes the queue message to processMagicLinkMessage; link_url + email come from the queue payload (no DB lookup required)
BRD.md §US-17.1: Given a subscriber requests portal access, When the API enqueues portal.magic_link_requested, Then a sign-in email with the magic-link URL is sent via Resend within seconds
- compliant
brd-us-23-resend-api-integrationRenewal confirmation uses Resend transactional email API (POST /emails); platform-level RESEND_API_KEY secret for v0.1
BRD.md §US-23.1: emails sent via Resend with merchant template + variables; v0.1 uses platform API key, v0.2 adds per-merchant key
- compliant
brd-us-18-1-admin-cancel-on-behalfMerchant admin can cancel a subscription on behalf of the subscriber, stamping actor_user_id on the events row
BRD.md §US-18.1, §US-18.5: Given admin views a subscription, When they click "Cancel on behalf", Then status becomes cancelled and event records actor_user_id + actor_kind=merchant_user
- compliant
brd-us-1-7-admin-cancel-actor-auditCancel-on-behalf action stamps actor_user_id + actor_kind=merchant_user on the events row per US-1.7
BRD.md §US-1.7: Given any merchant-actor mutation, Then events row carries actor_user_id and actor_kind=merchant_user
- compliant
brd-us-18-1-admin-charge-retry-by-idMerchant admin can retry a specific failed charge by ID from the subscription detail view
BRD.md §US-18.1: Given a failed charge row, When admin clicks "Retry", Then a new pending charge is queued and charge.retry_queued event is emitted
- compliant
brd-us-18-1-admin-charge-retry-uiSubscription detail page shows per-charge Retry button for failed charges
BRD.md §US-18.1: Given a failed charge in the history panel, When rendered, Then a Retry button appears that calls retryCharge()
- compliant
brd-us-18-1-admin-subscriptions-api-clientAdmin API client module for subscription cancel-on-behalf and per-charge retry
BRD.md §US-18.1: Admin SPA calls typed API client functions rather than raw fetch
- compliant
brd-us-17-portal-subscription-detail-pageMagic-link gated subscription detail page at /portal/subscriptions/[id] — shows plan info, charge history, next charge date, and cancel CTA. Client-side rendered; auth token sourced from sessionStorage.
BRD.md §US-17.x: subscriber portal v0.1.1 — view subscription detail + hard cancel (no churn-prevention flow in v0.1)
- compliant
brd-us-18-portal-hard-cancel-ctaHard-cancel CTA in subscriber portal — CancelSubscriptionButton.svelte shows inline confirmation then calls POST /api/portal/subscriptions/:id/cancel. Churn-prevention (US-18.5) deferred to v0.2.
BRD.md §US-18: subscriber-initiated cancel; v0.1.1 scope is hard-cancel only (no offers, no retention flow)
- compliant
brd-us-17-portal-subscription-detail-apiGET /api/v1/portal/subscriptions/:id — portal-session JWT gated detail endpoint. Returns subscription row with plan info. Anti-IDOR: ownership verified via customer_id from JWT payload.
BRD.md §US-17 + ADR-0014: portal-session JWT HS256 over BC_CLIENT_SECRET; ownership check collapses unknown-id and cross-tenant into 404
- compliant
brd-us-17-subscription-detail-view-componentSubscriptionDetailView.svelte — displays plan info, status, charge history table, and hosts CancelSubscriptionButton for active subscriptions
BRD.md §US-17.x: portal detail view — plan name, amount, cadence, next charge, charge history
- non-compliant
demo-try-end-to-end-flowScripted end-to-end demo at /try/end-to-end orchestrates subscribe → cart → confirm → portal → admin → CTA against the visitor's connected store; mute-by-default Web Speech narration, manual + auto-advance controls
Hive #1259 (AC-5e), dossier docs/handoffs/hive-1143-ac5e-demo-end-to-end-flow.md, parent Hive #1143 AC-5
adr-commitment
29 rows- compliant
adr-0007-uninstall-sweepADR-0007: GDPR-driven uninstall retention sweep (30-day tombstone before hard delete)
ADR-0007 / PR #557 (sweep) + PR #558 (runbook)
- compliant
adr-0009-tenant-scope-guardsADR-0009: tenant data isolation via store_hash scope guards on every owned-entity read
ADR-0009 / db.ts scope-guarded helpers
- compliant
adr-0011-failed-permanentlyADR-0011: failed_permanently terminal state — scheduler ratchets stuck charges
ADR-0011 / PR #163 (scheduler poison-row ratchet)
- compliant
adr-0014-typed-error-formatADR-0014: typed-error wire format (couples with BigEng convention #4)
ADR-0014 / Slice B PR #542
- compliant
adr-0024-per-instance-overridesADR-0024: per-delivery-instance override mutation API (skip / swap_product / reschedule_to)
ADR-0024 / PRs #556 #559 #560
- compliant
adr-0028-eligibility-engineADR-0028: eligibility-engine architecture pattern (Epic 26 inclusion + exclusion rules)
ADR-0028 / PRs #482 #483 #484 #485 #486 #487 + #475 #476 #478 #479 #480
- compliant
adr-0029-marketplace-firstADR-0029: marketplace-first, native-ready posture
ADR-0029 / PRs #539 #540
- compliant
adr-0030-bigeng-alignmentADR-0030: BigEng pattern alignment — ratify the 12-convention catalog
ADR-0030 / PR #555
- non-compliant
ops-ledger-max-attemptsADR-0031: operations.max_attempts column for per-row retry budget
ADR-0031
- partial
ops-ledger-locked-until-reaperADR-0031: operations.locked_until column + reaper cron
ADR-0031
- compliant
job-configuration-tableADR-0031: job_configuration sidecar table for per-cron per-store config
ADR-0031
- compliant
deprecation-handler-registeredDeprecationRegistry + middleware wired at app boot (ADR-0032)
ADR-0032
- compliant
api-version-namespace-v1All API routes mounted under /api/v1/ prefix (ADR-0032)
ADR-0032
- compliant
deprecation-headers-emit-on-requestDeprecation, Sunset, Link headers emit on deprecated route calls (ADR-0032)
ADR-0032
- compliant
subscription-extensions-tableADR-0033: polymorphic subscription_extensions substrate for Epic 6 extension types
ADR-0033
- compliant
extensions-scheduler-hooksADR-0033: scheduler invokes extension lifecycle hooks before each renewal
ADR-0033
- compliant
processor-adapter-bc-payments-productionADR-0035: BC Payments standard-rail adapter — production branch implemented (PAT mint + method lookup + payments.bigcommerce.com charge; throw at bc-payments.ts:70 replaced)
ADR-0035 / task cbb818e4 / synthesis 7f0691f9
- partial
processor-adapter-bc-payments-webhook-extractionADR-0035: BC Payments stored-instrument extraction at order.created webhook — real BigPay token persisted via GET /v3/customers/{id}/stored-instruments
ADR-0035 / task cbb818e4 / synthesis 7f0691f9
- compliant
processor-adapter-stripe-productionADR-0025: Stripe secondary-rail adapter — production branch implemented (PaymentIntent.create with off_session MIT; no pre-existing BC order required)
ADR-0025 / task daf2aa64 / synthesis 7f0691f9
- compliant
processor-adapter-stripe-decline-classificationStripe adapter classifies hard vs soft declines via HARD_DECLINE_CODES set; StripeChargeError exposes isHardDecline for scheduler dunning logic
task daf2aa64 / synthesis 7f0691f9
- compliant
adr-0037-stored-instruments-canonical-railADR-0037: stored-instruments vault ratified as the canonical charge rail — ADR file exists and bc-vault-client.ts service ships
ADR-0037 / P-comp 2026-05-15
- compliant
processor-adapter-registryAdapter discovery registry (adapter-registry.ts) exists and scheduler routes charges through getAdapter() rather than hardcoded createBcPaymentsAdapter()
synthesis 9457c80c / task 12ef8d88
- compliant
stripe-rail-sandbox-validatedStripe adapter end-to-end sandbox scenario exists — validates charge + off-session renewal + idempotency against Stripe test-mode API (Payments P-1)
ADR-0025 / proposal 632c6a55 / GH #771
- compliant
authorize-stripeADR-0038 P2-A: Stripe adapter implements authorize() via PaymentIntent.create with capture_method=manual, returns AuthorizeResult with processorTransactionId. authWindowDays=7 per Stripe documented card auth window.
ADR-0038 / Hive #796 track A
- compliant
capture-stripeADR-0038 P2-A: Stripe adapter implements capture() via PaymentIntent capture endpoint with amount_to_capture and Idempotency-Key (ADR-0011). voidAuth() implemented via PaymentIntent cancel.
ADR-0038 / Hive #796 track A / ADR-0011
- non-compliant
authorize-bc-paymentsADR-0038 P2-A: BC Payments authorize() stub — documents that payments.bigcommerce.com is atomic and does not support per-request auth-only mode. Throws not_implemented_phase_2_requires_bc_partner_track. Verified 2026-05-15 against BC payments overview + transactions-api docs.
ADR-0038 / Hive #796 track A
- partial
capture-bc-paymentsADR-0038 P2-A: BC Payments capture() and voidAuth() stubs — documents that BC Transactions API capture/void endpoints are "planned for future phases" as of 2026-05-15. authWindowDays=5 (TBC, conservative default).
ADR-0038 / Hive #796 track A
- partial
capture-timing-stored-on-storeADR-0038 Phase 1: stores.capture_timing column exists — migration 0026 (schema/D1) + capture_timing field on StoreRow in db.ts + CaptureTiming type in @bc-subscriptions/types. Advisory only in Phase 1; Phase 2 wires enforcement.
ADR-0038 / proposal #789 / synthesis #840 / Hive task 9f812cc8 / 2026-05-15
- partial
capture-timing-resolver-shippedADR-0038 Phase 2-D: capture-timing-resolver service ships — resolveCaptureTiming() exported, EU_COUNTRY_CODES constant present, migration 0028 adds stores.country_code column
ADR-0038 / Hive #822 / synthesis #840 (b87f5678)
scenario-coverage
19 rows- compliant
scenario-p1-maya-happy-pathP1 Maya: annual subscription signup → active → calendar-anchored next_charge_at (BRD §US-1.1 / US-9.2 / US-10.3)
PR #690 / BRD §US-1.1 US-9.2 US-10.3 / Hive task fe8d3bd5
- compliant
scenario-p1-maya-oos-exception-queueP1 Maya: OOS block at renewal → Exception Queue entry surfaced (BRD §US-15.1)
PR #690 / BRD §US-15.1 / Hive task fe8d3bd5
- compliant
scenario-p1-maya-calendar-anchorP1 Maya: mid-year signup → next_charge_at snaps to Jan 1, not signup+1yr (BRD §US-10.3)
PR #690 / BRD §US-10.3 / Hive task fe8d3bd5
- compliant
scenario-p2-alex-allotment-grantP2 Alex: AllotmentGrant create → 4 debits → balance=0 → month-boundary refresh → balance=4 (BRD §US-6.9)
PR #691 / BRD §US-6.9 / Hive task 8834674a
- compliant
scenario-p2-alex-cancelP2 Alex: subscriber cancel → status=cancelled + subscription.cancelled event (BRD §US-18.5 / US-6.5)
PR #691 / BRD §US-18.5 US-6.5 / Hive task 8834674a
- compliant
scenario-p2-alex-upgradeP2 Alex: mid-cycle Standard→Premium upgrade → proration charge + plan_id updated + plan_upgraded event (BRD §US-14.6)
PR #691 / BRD §US-14.6 / Hive task 8834674a
- compliant
scenario-p3-jordan-gift-purchaseP3 Jordan: Gift purchase → status=paused + gift extension + subscription.gift_purchased event (BRD §US-6.1)
PR #692 / BRD §US-6.1 / Hive task e9d7f5dc
- compliant
scenario-p3-jordan-gift-claimP3 Jordan: Gift claim → recipient subscription status=active, gift marked claimed (BRD §US-6.1)
PR #692 / BRD §US-6.1 / Hive task e9d7f5dc
- compliant
scenario-p3-jordan-expired-gift-claimP3 Jordan: Expired gift claim window → 412 precondition_failed (BRD §US-6.1)
PR #692 / BRD §US-6.1 / Hive task e9d7f5dc
- compliant
scenario-p4-sam-b2b-enrollmentP4 Sam: CS-rep B2B enrollment with PO reference → status=active + subscription.b2b_enrolled event (BRD §US-22.9 / ADR-0023)
PR #693 / BRD §US-22.9 / ADR-0023 / Hive task 63e46415
- compliant
scenario-p4-sam-commitment-cyclesP4 Sam: commitment_cycles=12 persisted on plan — data-model foundation for U-17 cancel lock (BRD §US-5.7)
PR #693 / BRD §US-5.7 / Hive task 63e46415
- compliant
scenario-p4-sam-allotment-quarterly-refreshP4 Sam: Buyer-org AllotmentGrant 50 wellness_credits/quarter → balance drained → quarterly refresh → balance=50 (BRD §US-6.9)
PR #693 / BRD §US-6.9 / Hive task 63e46415
- compliant
scenario-p5-taylor-renewal-chargeP5 Taylor: Subscribe-and-save 3-cycle commitment + successful renewal charge (BRD §US-5.7 / US-11.1)
PR #694 / BRD §US-5.7 US-11.1 / Hive task b980c6b7
- compliant
scenario-p5-taylor-dunning-retryP5 Taylor: Dunning retry scheduled on failure → charge succeeds on retry → subscription stays active (BRD §US-11.2)
PR #694 / BRD §US-11.2 / Hive task b980c6b7
- compliant
scenario-p5-taylor-dunning-exhaustionP5 Taylor: Dunning all 3 stages exhaust → subscription.status=cancelled + charge.failed_permanently (BRD §US-11.2)
PR #694 / BRD §US-11.2 / Hive task b980c6b7
- compliant
scenario-u17-cancel-lock-p4-samU-17 P4 Sam: cancel lock 409 enforcement + CS-rep override with audit trail (BRD §US-5.7 + synthesis fa3dc5f3)
synthesis fa3dc5f3 / BRD §US-5.7
- compliant
scenario-u17-cancel-lock-p5-taylorU-17 P5 Taylor: cancel lock 409 for 3-cycle commitment (BRD §US-5.7 + synthesis fa3dc5f3)
synthesis fa3dc5f3 / BRD §US-5.7
- compliant
scenario-u19-future-start-dateU-19 P3 Jordan: future start date → pending_start → scheduler activation (BRD §US-N.M + synthesis fa3dc5f3)
synthesis fa3dc5f3 / BRD §US-N.M
- compliant
scenario-u20-calendar-billing-anchorU-20 P4 Sam: calendar billing anchor billing_anchor_month/day → April 1 fiscal-year anchor (BRD §US-N.M + synthesis fa3dc5f3)
synthesis fa3dc5f3 / BRD §US-N.M
bigeng-convention
12 rows- compliant
bigeng-01-snake-case-wiresnake_case wire fields — JSON request/response keys are snake_case on the wire
ADR-0030 / BigEng audit §Convention 1
- compliant
bigeng-02-store-id-in-payloadstore_hash present as payload field, not header-only
ADR-0030 / BigEng audit §Convention 2
- compliant
bigeng-03-offset-cursor-paginationOffset + cursor pagination, mutually exclusive — bigeng convention #3
ADR-0030 / BigEng audit §Convention 3
- compliant
bigeng-04-typed-errorsTyped domain ADTs for errors (vs ad-hoc { error: string } shapes)
ADR-0030 / Slice B PR #542
- compliant
bigeng-05-principal-envelopeAuth principal envelope {id, type} on requests
ADR-0030 / BigEng audit §Convention 5
- compliant
bigeng-06-uuid-owned-entitiesUUID for owned entities; integer for legacy BC FKs
ADR-0030 / BigEng audit §Convention 6
- compliant
bigeng-07-soft-delete-flagSoft-delete via is_deleted INTEGER (vs hard-delete + cascade)
ADR-0030 / Slice C PR #543
- compliant
bigeng-08-timestamp-defaultsTIMESTAMP DEFAULT CURRENT_TIMESTAMP on created_at / updated_at columns
ADR-0030 / BigEng audit §Convention 8
- compliant
bigeng-09-operations-ledgerOperations-table saga ledger (replaces inline cron-driven orchestration)
ADR-0030 / Slice C PR #543
- compliant
bigeng-10-scope-entitlementsScope / resource entitlements (per-subscriber, not just store-wide)
ADR-0030 / BigEng audit §Convention 10
- compliant
bigeng-11-audit-decoratorAudit emission via decorator wrapping (vs inline INSERT INTO events)
ADR-0030 / Slice D PR #545
- compliant
bigeng-12-audit-payload-shapeAudit payload shape — target/resource/summary/operation_type + request-scoped MDC
ADR-0030 / Slice D PR #545
nfr
8 rows- manual review
brd-nfr-7-1-charge-latency-p99P99 charge execution latency < 3s from scheduler pickup to processor call initiated
BRD.md §7.1: P99 charge execution latency < 3s from scheduler pickup → processor call initiated
- manual review
brd-nfr-7-1-webhook-ingestion-latencyP99 webhook ingestion < 250ms to 2xx response (processing is async)
BRD.md §7.1: P99 webhook ingestion < 250ms to 2xx response (processing is async)
- manual review
brd-nfr-7-2-platform-uptime-sla99.95% platform uptime SLA; at-least-once delivery on external webhook deliveries; at-most-once charge execution enforced via idempotency key = charge.id
BRD.md §7.2: 99.95% platform uptime; at-least-once webhook delivery; at-most-once charge execution (idempotency key = charge.id)
- compliant
brd-nfr-7-3-pci-scope-zeroNo PAN, CVV, or full card data ever stored or transmitted through our servers; processor adapters hold tokens only; TLS 1.3 in transit; AES-256-GCM for secrets at rest
BRD.md §7.3: PCI scope: zero — we never hold PAN, CVV, or full card data; processor adapters hold tokens only; TLS 1.3; AES-256-GCM for secrets
- manual review
brd-nfr-7-4-rls-tenant-isolationSingle-tenant resource isolation via Postgres row-level security (RLS) partitioned by store_hash
BRD.md §7.4: Single-tenant resource isolation via Postgres RLS by store_hash
- manual review
brd-nfr-7-5-event-row-per-state-transitionEvery state transition emits an Event row (append-only, per-store partitioned); every external call logged with latency, status, correlation ID
BRD.md §7.5: Every state transition emits an Event row; every external call logged with latency, status, correlation ID
- manual review
brd-nfr-7-6-wcag-aaWCAG 2.2 AA for merchant admin and subscriber portal; keyboard-navigable; screen-reader tested
BRD.md §7.6: WCAG 2.2 AA for both merchant admin and subscriber portal; keyboard-navigable; screen-reader tested
- manual review
brd-nfr-7-8-gdpr-ccpa-complianceGDPR data subject access + erasure within 30 days; CCPA with "do not sell"; PCI DSS SAQ-A maintained; EU data residency by Phase 2
BRD.md §7.8: GDPR erasure 30-day fulfillment; CCPA "do not sell"; PCI DSS SAQ-A; EU data residency Phase 2
feature_flag_inactive
4 rows- compliant
bc-payments-standard-rail-activeBC Payments adapter: HACKATHON_FIXTURE fixture guard fully removed from charge() — standard-rail (PAT/charge calls) is the only path per Payments P-2
ADR-0035 / task cbb818e4 / synthesis 7f0691f9 / proposal b1374e5a
- compliant
stripe-adapter-activeStripe adapter: HACKATHON_FIXTURE fixture guard fully removed from charge() — PaymentIntent.create is the only path per ADR-0025 / Payments P-2
ADR-0025 / task daf2aa64 / synthesis 7f0691f9 / proposal b1374e5a
- compliant
fixture-payment-method-retired-from-productionensureFixturePaymentMethod removed from all production route handlers — subscriptions.ts + webhooks.ts use real instrument resolution; db.ts export retained for tests
task 743c215e / synthesis 7f0691f9 / proposal b1374e5a
- compliant
fixture-mode-gated-to-testHACKATHON_FIXTURE is absent from all production route handlers and adapter charge() bodies — fixture mode is test-only (db.ts export and types.ts optional field remain)
proposal b1374e5a (Payments P-2) / decision 4e872518 (D-17 sweep)
security
2 rows- compliant
processor-config-encrypted-at-restTier α T1S: processor_connections.config stripe_secret_key is encrypted at write (encryptForStore in processor-connections.ts create handler) and decrypted at read (adapter-registry.ts getAdapter Stripe case).
Tier α decision e21ea786 / Hive proposal e7e81fb0
- compliant
per-store-key-derivationTier α T1S: per-store AES-256 keys derived via HKDF from deploy-time master (CREDENTIAL_ENCRYPTION_KEY) using store_hash as HKDF info. deriveStoreKey + encryptForStore + decryptForStore in crypto.ts.
Tier α decision e21ea786 / Hive proposal e7e81fb0
feature
1 rows- partial
demo-try-connect-formDemo "connect your store" connect form — store_hash + Storefront API token, validated client-side, session stored in sessionStorage only (spec #1254 AC-2)
Hive #1143 AC-5a, gating spec #1254. Dossier: docs/handoffs/hive-1143-ac5a-demo-connect-form.md