How do I update the Stripe Billing & Subscription for PACT/Excise Bundle Customers

Overview

This document describes the implementation of automated Stripe catalog and subscription management for Token of Trust's PACT/Excise compliance bundle. The implementation provides a single, authoritative source for pricing configuration and subscription management, replacing manual Stripe Dashboard operations with code-driven, auditable, and repeatable processes.

Problem Statement

Token of Trust offers a compliance bundle with complex, multi-dimensional pricing:

  1. Base Bundle Pricing: Two-dimensional matrix (number of reports × order volume)
    • Up to 15, 25, or 50 reports per month
    • 7 order volume tiers (0-999, 1000-2499, etc.)
    • 21 distinct base price points
  2. Usage-Based Charges: Three metered components
    • Order Volume: Volume tiers with flat fees based on monthly snapshot
    • Active SKUs: Volume tiers with flat fees (26 tiers, 0-16000+)
    • Ad-hoc Reports: Simple per-report charge ($200/report)
  3. Fixed Add-ons: Monthly subscription add-ons
    • Product Sync ($250/mo) - included free with bundle
    • Order Sync ($250/mo) - included free with bundle
  4. Complex Requirements:
    • Manual Stripe Dashboard management was error-prone and time-consuming
    • No version control for pricing changes
    • Difficult to ensure consistency across customers
    • Hard to migrate customers between pricing tiers
    • No audit trail for subscription changes

Solution Architecture

Core Components

  1. stripe_billing.node.js - Main automation script
    • Seed/update Stripe catalog (products, prices, coupons)
    • Create/update customer subscriptions
    • Idempotent operations (safe to run multiple times)
    • Dry-run mode for all operations
  2. stripe_billing.md - Comprehensive documentation
    • Usage guide with scenarios
    • Troubleshooting guide
    • Governance and naming conventions
    • Migration strategies
  3. This document
    • Architecture decisions
    • Design rationale
    • Future roadmap

Key Design Decisions

1. Configuration as Code

Decision: All pricing, products, and business rules live in a single CONFIG object at the top of the script.

Rationale:

  • Single source of truth for pricing
  • Version controlled in Git with PR review
  • Easy to diff pricing changes
  • Can be extracted to JSON for non-engineer access (future roadmap)

Example:

const CONFIG = {
  baseMatrix: {
    reportsBands: ['r15', 'r16_25', 'r26_50'],
    ordersBands: ['o0_999', 'o1000_2499', ...],
    amounts: {
      r15: { o0_999: 3250.00, o1000_2499: 3250.00, ... }
    }
  },
  usage: {
    orders: { /* volume tiers */ },
    activeSkus: { /* 26 volume tiers */ }
  }
}

2. Lookup Keys for Idempotency

Decision: Use Stripe's lookup_key feature for all prices and metadata.lookup_key for products.

Rationale:

  • Enables idempotent operations (running twice doesn't create duplicates)
  • Allows finding items by meaningful names instead of Stripe IDs
  • Supports price versioning (e.g., price-bdl-orders-volume-v1, v2)
  • Prevents accidental overwrites

Naming Convention:

  • Products: product-bdl-<component> (e.g., product-bdl-bundle_base)
  • Prices: price-bdl-<component>-<version> (e.g., price-bdl-base-r15-o0_999)
  • All bundle components use bdl- prefix for consistency

3. Automatic Included Add-ons via Coupon

Decision: Product Sync and Order Sync are added to all subscriptions but made free via a 100% coupon.

Rationale:

  • Shows the value of included add-ons on invoices ($250/mo each = $500/mo value)
  • Single consistent subscription structure for all customers
  • Coupon ("Included with Bundle") clearly communicates the benefit
  • Easier to support and troubleshoot
  • Can remove coupon for specific customers who want to pay separately (future flexibility)

Implementation:

// Coupon automatically created during seed-catalog
{
  id: 'bdl-addons-included',
  name: 'Included with Bundle',
  percent_off: 100,
  applies_to: { products: [product_sync, order_sync] }
}

4. Volume Tiers with flat_amount

Decision: Use Stripe's volume tier pricing with flat_amount (not unit_amount) for Order Volume and Active SKUs.

Rationale:

  • Charges a flat fee based on which tier the customer lands in at end of month
  • Customer pays for the tier reached, not per-unit within the tier
  • Example: Customer with 3,200 SKUs pays $1,345.80 flat (not 3,200 × some rate)
  • Matches contract pricing structure
  • Simplifies invoicing and customer understanding

Stripe API Structure:

{
  tiers_mode: 'volume',
  aggregate_usage: 'last_during_period', // snapshot at month end
  tiers: [
    { up_to: 249, flat_amount: 0 },      // 0-249 SKUs: free
    { up_to: 499, flat_amount: 0 },      // 250-499 SKUs: free
    { up_to: 749, flat_amount: 124.50 }, // 500-749 SKUs: $124.50 flat
    ...
  ]
}

5. Migration-Safe Subscription Updates

Decision: Check for existing subscription items by both product lookup key AND price ID.

Rationale:

  • Handles migration from old product naming (without bdl- prefix) to new
  • Prevents duplicate items when product keys change but prices stay the same
  • Allows gradual migration without breaking existing subscriptions

Implementation:

// Try product lookup key first
let item = currentItems.find(ci => ci.product?.metadata?.lookup_key === wantKey)?.si;
// Fall back to price ID (migration scenario)
if (!item) {
  item = currentItems.find(ci => ci.price.id === wantPriceId)?.si;
}

6. Dry-Run Mode for All Operations

Decision: Every operation supports --dry-run flag that shows what would happen without making changes.

Rationale:

  • Safety: Preview changes before applying
  • Debugging: Understand what script will do
  • Confidence: Run in production without fear
  • Documentation: Dry-run output serves as change documentation

Usage Patterns

Initial Setup

# 1. Install dependencies
npm install stripe config

# 2. Configure Stripe API key
export STRIPE_SECRET=sk_test_...
# OR set in config/default.json: verifications.stripe.secretKey

# 3. Seed catalog (creates all products, prices, coupon)
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog --dry-run
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog

Managing Customer Subscriptions

# Preview what would be created/updated
node scripts/stripe/stripe_billing.node.js ensure-subscription \
  --customer mike@1541retail.com \
  --base-tier price-bdl-base-r15-o0_999 \
  --dry-run

# Create or update subscription
node scripts/stripe/stripe_billing.node.js ensure-subscription \
  --customer mike@1541retail.com \
  --base-tier price-bdl-base-r15-o0_999 \
  --create-if-missing

Updating Pricing

# 1. Update CONFIG in stripe_billing.node.js
# 2. Bump version (e.g., price-bdl-orders-volume-v1 → v2)
# 3. Seed catalog
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog

# 4. Migrate customers to new prices (repeat for each)
node scripts/stripe/stripe_billing.node.js ensure-subscription \
  --customer cus_XXX \
  --base-tier price-bdl-base-v2-r15-o0_999

What Gets Created in Stripe

Products (6 total)

  1. PACT/Excise Bundle Base Subscription (product-bdl-bundle_base)
  2. Order Volume (product-bdl-orders_volume)
  3. Active SKUs (product-bdl-active_skus)
  4. Ad-hoc Reports (product-bdl-ad_hoc_reports)
  5. Product Sync AddOn (product-bdl-addon-product_sync) - includes "[INCLUDED WITH BUNDLE]" in description
  6. Order Sync AddOn (product-bdl-addon-order_sync) - includes "[INCLUDED WITH BUNDLE]" in description

Prices (50+ total)

  • 21 Base Bundle prices (3 report tiers × 7 order tiers)
    • price-bdl-base-r15-o0_999 through price-bdl-base-r26_50-o7000_8499
  • 3 Usage prices
    • price-bdl-orders-volume-v1 (17 volume tiers)
    • price-bdl-skus-volume-v1 (26 volume tiers)
    • price-bdl-adhoc-per_report-v1 (simple $200/report)
  • 2 Add-on prices
    • price-bdl-addon-product_sync-monthly-v1 ($250/mo)
    • price-bdl-addon-order_sync-monthly-v1 ($250/mo)

Coupon (1 total)

  • bdl-addons-included ("Included with Bundle")
    • 100% off forever
    • Applies to: Product Sync and Order Sync products
    • Promotion Code: BDL-ADDONS-INCLUDED

Typical Subscription Structure

Every customer subscription has 6 items:

  1. Base Bundle (1 of 21 prices, depending on tier)
  2. Order Volume (metered, volume tiers)
  3. Active SKUs (metered, volume tiers)
  4. Ad-hoc Reports (metered, per-report)
  5. Product Sync AddOn (fixed $250/mo, 100% off via coupon)
  6. Order Sync AddOn (fixed $250/mo, 100% off via coupon)

Testing Strategy

Dry-Run First, Always

# Test catalog changes
node scripts/stripe/stripe_billing.node.js seed-catalog --dry-run

# Test subscription changes
node scripts/stripe/stripe_billing.node.js ensure-subscription \
  --customer test@example.com \
  --base-tier price-bdl-base-r15-o0_999 \
  --dry-run

Test Environment

# Use NODE_ENV=test to load test Stripe keys from config
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog

Validation Checklist

After seeding catalog:

  • Check Stripe Dashboard: 6 products exist
  • Check Stripe Dashboard: 50+ prices exist with correct amounts
  • Check Stripe Dashboard: Coupon bdl-addons-included exists
  • Verify promotion code BDL-ADDONS-INCLUDED is active

After creating subscription:

  • Check Stripe Dashboard: Subscription has 6 items
  • Verify base tier price matches expected amount
  • Verify Product Sync and Order Sync show $0 (coupon applied)
  • Verify coupon "Included with Bundle" shows at bottom
  • Check invoice preview shows correct line items

Operational Considerations

When to Run seed-catalog

Run seed-catalog when:

  • Initial setup (first time)
  • Adding new products/add-ons
  • Updating pricing (with new version numbers)
  • Updating product descriptions

Safe to run multiple times - idempotent operations won't create duplicates.

When to Run ensure-subscription

Run ensure-subscription when:

  • Creating new customer subscription
  • Changing customer's base tier
  • Migrating customer to new price version
  • Ensuring subscription matches desired state

Safe to run multiple times - only applies needed changes.

Handling Pricing Changes

Option 1: New Version (Recommended)

  • Update CONFIG with new version (e.g., price-bdl-orders-volume-v2)
  • Run seed-catalog to create new prices
  • Existing customers stay on v1 until explicitly migrated
  • New customers get v2

Option 2: Force Replace (Use Sparingly)

  • Update CONFIG amounts for same lookup_key
  • Run seed-catalog --force-replace-lookup-keys
  • Script re-keys old price and creates new one with same lookup_key
  • Requires explicit customer migration

Monitoring and Alerts

Recommended monitoring:

  • Track seed-catalog runs in deployment logs
  • Alert on ensure-subscription failures
  • Monitor for pricing mismatches (script warns on dry-run)
  • Track coupon application (script logs when applied)

Future Enhancements

Planned

  1. Externalize CONFIG to JSON
    • Allow non-engineers to propose pricing changes
    • Schema validation for safety
    • CLI: --config /path/to/billing.json
  2. Subscription ID Support
    • Add --subscription sub_XXX parameter
    • Support customers with multiple subscriptions
    • More precise targeting for migrations
  3. Bulk Migration Tool
    • Migrate cohorts of customers to new prices
    • Scheduling windows to avoid rate limits
    • Progress reporting and rollback capability
  4. Strict Mode for Add-ons
    • --strict-addons flag to remove unlisted add-ons
    • Enforce canonical subscription structure
    • Cleanup legacy items
  5. Usage Reporting Integration
    • Automated posting of Order Volume usage
    • Automated posting of Active SKUs snapshot
    • Automated posting of Ad-hoc Report count

Considered but Deferred

  1. Two-way Sync - Export Stripe state back to JSON (auditing drift)
  2. Web UI - RevOps-friendly interface for subscription management
  3. Webhook Handlers - React to Stripe events (invoice.payment_failed, etc.)

Governance

Approvals Required

  • Pricing Changes: Product + Finance approval
  • Catalog Seeds: DevOps/Engineering execution
  • Subscription Changes: Success/RevOps via script or Dashboard

Code Review Process

All changes to stripe_billing.node.js require:

  1. PR with clear description of pricing changes
  2. Reference this implementation doc in PR
  3. Finance approval for pricing updates
  4. Product approval for structural changes
  5. DevOps review for technical correctness

Documentation Updates

When updating pricing:

  • Update CONFIG in stripe_billing.node.js
  • Update examples in stripe_billing.md
  • Update version history in CHANGELOG (if maintained)
  • Create migration plan for existing customers
  • Notify Finance/RevOps of changes

Troubleshooting

Common Issues

"Price not found" Error

  • Run seed-catalog first to create prices
  • Verify lookup_key matches CONFIG

"Can't add Price, already exists" Error

  • Script now handles this via price ID fallback
  • If persists, check for duplicate items in Stripe Dashboard
  • Manually remove duplicate items or use dry-run to debug

"Coupon not found" Warning

  • Run seed-catalog to create coupon
  • Verify coupon bdl-addons-included exists in Dashboard

Subscription Has Old Items

  • Script detects by price ID now, should skip duplicates
  • If issues persist, manually clean old items via Dashboard
  • Then run ensure-subscription to add missing items

Debug Process

  1. Run with --dry-run to see planned changes
  2. Check script output for warnings/errors
  3. Verify Stripe Dashboard state matches expectations
  4. Check Stripe API logs for detailed error messages
  5. Review script's item detection logic (by product key + price ID)

Security Considerations

API Keys

  • Never commit STRIPE_SECRET to Git
  • Use environment variables or secure config management
  • Separate test/production keys (use NODE_ENV)
  • Rotate keys periodically

Access Control

  • Limit who can run ensure-subscription (financial impact)
  • Require --dry-run review before production runs
  • Log all script executions with user/timestamp
  • Consider wrapper script with additional auth

Audit Trail

Every operation logs:

  • Timestamp of execution
  • Customer ID affected
  • Changes made (create/update/skip)
  • Dry-run vs. actual execution

Consider capturing this to structured logs or database.

Performance

Rate Limits

Stripe API rate limits:

  • 100 requests/second in test mode
  • 25 requests/second in live mode

Script design:

  • Sequential operations (no parallel API calls)
  • Respects Stripe rate limits naturally
  • For bulk migrations, add delays between customers

Execution Time

Typical times:

  • seed-catalog: 30-60 seconds (50+ API calls)
  • ensure-subscription: 5-10 seconds per customer

References

Change History

  • 2025-01-XX: Initial implementation
    • Created automation script for catalog and subscriptions
    • Implemented product-bdl-* and price-bdl-* naming convention
    • Added automatic included add-ons via coupon
    • Migration-safe subscription updates (check by price ID)
    • Comprehensive documentation (this file + stripe_billing.md)

Document Maintained By: Engineering Team Last Updated: 2025-01-13 Version: 1.0

Read more

How do you setup identity or age verification so that only certain locations need to get verified?

Answer: Use Location-Based Identity Verification. This is enabled via the locationRestrictions.requiresVerification attribute in your application configuration to define which countries and regions require verification. Summary Token of Trust now supports location-based verification requirements through the locationRestrictions configuration. This feature allows you to specify which geographic locations (countries and regions)

By darrin