Files
orion/docs/features/subscription-billing.md
Samir Boulahtit 4118d22101 docs: add Stripe setup guide with webhook secret instructions
Add step-by-step setup guide covering:
- Getting API keys from Stripe Dashboard
- Creating webhook endpoint and getting signing secret
- Local development with Stripe CLI
- Creating products and prices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:31:58 +01:00

18 KiB

Subscription & Billing System

The platform provides a comprehensive subscription and billing system for managing vendor subscriptions, usage limits, and payments through Stripe.

Overview

The billing system enables:

  • Subscription Tiers: Database-driven tier definitions with configurable limits
  • Usage Tracking: Orders, products, and team member limits per tier
  • Stripe Integration: Checkout sessions, customer portal, and webhook handling
  • Self-Service Billing: Vendor-facing billing page for subscription management
  • Add-ons: Optional purchasable items (domains, SSL, email packages)
  • Capacity Forecasting: Growth trends and scaling recommendations
  • Background Jobs: Automated subscription lifecycle management

Architecture

Database Models

All subscription models are defined in models/database/subscription.py:

Model Purpose
SubscriptionTier Tier definitions with limits and Stripe price IDs
VendorSubscription Per-vendor subscription status and usage
AddOnProduct Purchasable add-ons (domains, SSL, email)
VendorAddOn Add-ons purchased by each vendor
StripeWebhookEvent Idempotency tracking for webhooks
BillingHistory Invoice and payment history
CapacitySnapshot Daily platform capacity metrics for forecasting

Services

Service Location Purpose
BillingService app/services/billing_service.py Subscription operations, checkout, portal
SubscriptionService app/services/subscription_service.py Limit checks, usage tracking, tier info
StripeService app/services/stripe_service.py Core Stripe API operations
CapacityForecastService app/services/capacity_forecast_service.py Growth trends, projections
PlatformHealthService app/services/platform_health_service.py Subscription capacity aggregation

Background Tasks

Task Location Schedule Purpose
reset_period_counters app/tasks/subscription_tasks.py Daily Reset order counters at period end
check_trial_expirations app/tasks/subscription_tasks.py Daily Expire trials without payment method
sync_stripe_status app/tasks/subscription_tasks.py Hourly Sync status with Stripe
cleanup_stale_subscriptions app/tasks/subscription_tasks.py Weekly Clean up old cancelled subscriptions
capture_capacity_snapshot app/tasks/subscription_tasks.py Daily Capture capacity metrics snapshot

API Endpoints

Vendor Billing API

All billing endpoints are under /api/v1/vendor/billing:

Endpoint Method Purpose
/billing/subscription GET Current subscription status & usage
/billing/tiers GET Available tiers for upgrade
/billing/checkout POST Create Stripe checkout session
/billing/portal POST Create Stripe customer portal session
/billing/invoices GET Invoice history
/billing/upcoming-invoice GET Preview next invoice
/billing/change-tier POST Upgrade/downgrade tier
/billing/addons GET Available add-on products
/billing/my-addons GET Vendor's purchased add-ons
/billing/addons/purchase POST Purchase an add-on
/billing/addons/{id} DELETE Cancel an add-on
/billing/cancel POST Cancel subscription
/billing/reactivate POST Reactivate cancelled subscription

Admin Platform Health API

Capacity endpoints under /api/v1/admin/platform-health:

Endpoint Method Purpose
/platform-health/health GET Full platform health report
/platform-health/capacity GET Capacity-focused metrics
/platform-health/subscription-capacity GET Subscription-based capacity vs usage
/platform-health/trends GET Growth trends over time
/platform-health/recommendations GET Scaling recommendations
/platform-health/snapshot POST Manually capture capacity snapshot

Subscription Tiers

Default Tiers

Tier Price Products Orders/mo Team
Essential €49/mo 200 100 1
Professional €99/mo Unlimited 500 3
Business €199/mo Unlimited 2000 10
Enterprise Custom Unlimited Unlimited Unlimited

Database-Driven Tiers

Tiers are stored in the subscription_tiers table and queried via SubscriptionService:

# Get tier from database with legacy fallback
tier_info = subscription_service.get_tier_info("professional", db=db)

# Query all active tiers
all_tiers = subscription_service.get_all_tiers(db=db)

The service maintains backward compatibility with a legacy fallback:

def get_tier_info(self, tier_code: str, db: Session | None = None) -> TierInfo:
    """Get full tier information. Queries database if db session provided."""
    if db is not None:
        db_tier = self.get_tier_by_code(db, tier_code)
        if db_tier:
            return TierInfo(...)

    # Fallback to hardcoded TIER_LIMITS during migration
    return self._get_tier_from_legacy(tier_code)

Tier Features

Each tier includes specific features stored in the features JSON column:

tier.features = [
    "basic_support",       # Essential
    "priority_support",    # Professional+
    "analytics",           # Business+
    "api_access",          # Business+
    "white_label",         # Enterprise
    "custom_integrations", # Enterprise
]

Limit Enforcement

Limits are enforced at the service layer:

Orders

# app/services/order_service.py
subscription_service.check_order_limit(db, vendor_id)

Products

# app/api/v1/vendor/products.py
subscription_service.check_product_limit(db, vendor_id)

Team Members

# app/services/vendor_team_service.py
subscription_service.can_add_team_member(db, vendor_id)

Stripe Integration

Configuration

Required environment variables:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_TRIAL_DAYS=30  # Optional, default trial period

Setup Guide

Step 1: Get API Keys

  1. Go to Stripe Dashboard
  2. Copy your Publishable key (pk_test_... or pk_live_...)
  3. Copy your Secret key (sk_test_... or sk_live_...)

Step 2: Create Webhook Endpoint

  1. Go to Stripe Webhooks
  2. Click Add endpoint
  3. Enter your endpoint URL: https://yourdomain.com/api/v1/webhooks/stripe
  4. Select events to listen to:
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.paid
    • invoice.payment_failed
  5. Click Add endpoint
  6. Copy the Signing secret (whsec_...) - this is your STRIPE_WEBHOOK_SECRET

Step 3: Local Development with Stripe CLI

For local testing, use the Stripe CLI:

# Install Stripe CLI
brew install stripe/stripe-cli/stripe  # macOS
# or download from https://github.com/stripe/stripe-cli/releases

# Login to Stripe
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe

# The CLI will display a webhook signing secret (whsec_...)
# Use this as STRIPE_WEBHOOK_SECRET for local development

Step 4: Create Products & Prices in Stripe

Create subscription products for each tier:

  1. Go to Stripe Products
  2. Create products for each tier (Starter, Professional, Business, Enterprise)
  3. Add monthly and annual prices for each
  4. Copy the Price IDs (price_...) and update your tier configuration

Webhook Events

The system handles these Stripe events:

Event Handler
checkout.session.completed Activates subscription, links customer
customer.subscription.updated Updates tier, status, period
customer.subscription.deleted Marks subscription cancelled
invoice.paid Records payment, resets counters
invoice.payment_failed Marks past due, increments retry count

Webhook Endpoint

Webhooks are received at /api/v1/webhooks/stripe:

# Uses signature verification for security
event = stripe_service.construct_event(payload, stripe_signature)

Vendor Billing Page

The vendor billing page is at /vendor/{vendor_code}/billing:

Page Sections

  1. Current Plan: Tier name, status, next billing date
  2. Usage Meters: Products, orders, team members with limits
  3. Change Plan: Upgrade/downgrade options
  4. Payment Method: Link to Stripe portal
  5. Invoice History: Recent invoices with PDF links
  6. Add-ons: Available and purchased add-ons

JavaScript Component

The billing page uses Alpine.js (static/vendor/js/billing.js):

function billingData() {
    return {
        subscription: null,
        tiers: [],
        invoices: [],
        addons: [],
        myAddons: [],

        async init() {
            await this.loadData();
        },

        async selectTier(tier) {
            const response = await this.apiPost('/billing/checkout', {
                tier_code: tier.code,
                is_annual: false
            });
            window.location.href = response.checkout_url;
        },

        async openPortal() {
            const response = await this.apiPost('/billing/portal', {});
            window.location.href = response.portal_url;
        },

        async purchaseAddon(addon) {
            const response = await this.apiPost('/billing/addons/purchase', {
                addon_code: addon.code
            });
            window.location.href = response.checkout_url;
        },

        async cancelAddon(addon) {
            await this.apiDelete(`/billing/addons/${addon.id}`);
            await this.loadMyAddons();
        }
    };
}

Add-ons

Available Add-ons

Code Name Category Price
domain Custom Domain domain €15/year
ssl_premium Premium SSL ssl €49/year
email_5 5 Email Addresses email €5/month
email_10 10 Email Addresses email €9/month
email_25 25 Email Addresses email €19/month

Purchase Flow

  1. Vendor selects add-on on billing page
  2. For domains: enter domain name, validate availability
  3. Create Stripe checkout session with add-on price
  4. On webhook success: create VendorAddOn record

Background Tasks

Task Definitions

# app/tasks/subscription_tasks.py

async def reset_period_counters():
    """
    Reset order counters for subscriptions whose billing period has ended.
    Should run daily. Resets orders_this_period to 0 and updates period dates.
    """

async def check_trial_expirations():
    """
    Check for expired trials and update their status.
    Trials without payment method -> expired
    Trials with payment method -> active
    """

async def sync_stripe_status():
    """
    Sync subscription status with Stripe.
    Fetches current status and updates local records.
    """

async def cleanup_stale_subscriptions():
    """
    Clean up subscriptions in inconsistent states.
    Marks old cancelled subscriptions as expired.
    """

async def capture_capacity_snapshot():
    """
    Capture a daily snapshot of platform capacity metrics.
    Used for growth trending and capacity forecasting.
    """

Scheduling

Configure with your scheduler of choice (Celery, APScheduler, cron):

# Example with APScheduler
scheduler.add_job(reset_period_counters, 'cron', hour=0, minute=5)
scheduler.add_job(check_trial_expirations, 'cron', hour=1, minute=0)
scheduler.add_job(sync_stripe_status, 'cron', minute=0)  # Every hour
scheduler.add_job(cleanup_stale_subscriptions, 'cron', day_of_week=0)  # Weekly
scheduler.add_job(capture_capacity_snapshot, 'cron', hour=0, minute=0)

Capacity Forecasting

Subscription-Based Capacity

Track theoretical vs actual capacity:

capacity = platform_health_service.get_subscription_capacity(db)

# Returns:
{
    "total_subscriptions": 150,
    "tier_distribution": {
        "essential": 80,
        "professional": 50,
        "business": 18,
        "enterprise": 2
    },
    "products": {
        "actual": 125000,
        "theoretical_limit": 500000,
        "utilization_percent": 25.0,
        "headroom": 375000
    },
    "orders_monthly": {
        "actual": 45000,
        "theoretical_limit": 300000,
        "utilization_percent": 15.0
    },
    "team_members": {
        "actual": 320,
        "theoretical_limit": 1500,
        "utilization_percent": 21.3
    }
}

Analyze growth over time:

trends = capacity_forecast_service.get_growth_trends(db, days=30)

# Returns:
{
    "period_days": 30,
    "snapshots_available": 30,
    "trends": {
        "vendors": {
            "start_value": 140,
            "current_value": 150,
            "change": 10,
            "growth_rate_percent": 7.14,
            "daily_growth_rate": 0.238,
            "monthly_projection": 161
        },
        "products": {
            "start_value": 115000,
            "current_value": 125000,
            "change": 10000,
            "growth_rate_percent": 8.7,
            "monthly_projection": 136000
        }
    }
}

Scaling Recommendations

Get automated scaling advice:

recommendations = capacity_forecast_service.get_scaling_recommendations(db)

# Returns:
[
    {
        "category": "capacity",
        "severity": "warning",
        "title": "Product capacity approaching limit",
        "description": "Currently at 85% of theoretical product capacity",
        "action": "Consider upgrading vendor tiers or adding capacity"
    },
    {
        "category": "infrastructure",
        "severity": "info",
        "title": "Current tier: Medium",
        "description": "Next upgrade trigger: 300 vendors",
        "action": "Monitor growth and plan for infrastructure scaling"
    }
]

Infrastructure Scaling Reference

Clients vCPU RAM Storage Database Monthly Cost
1-50 2 4GB 100GB SQLite €30
50-100 4 8GB 250GB PostgreSQL €80
100-300 4 16GB 500GB PostgreSQL €150
300-500 8 32GB 1TB PostgreSQL + Redis €350
500-1000 16 64GB 2TB PostgreSQL + Redis €700
1000+ 32+ 128GB+ 4TB+ PostgreSQL cluster €1,500+

Exception Handling

Custom exceptions for billing operations (app/exceptions/billing.py):

Exception HTTP Status Description
PaymentSystemNotConfiguredException 503 Stripe not configured
TierNotFoundException 404 Invalid tier code
StripePriceNotConfiguredException 400 No Stripe price for tier
NoActiveSubscriptionException 400 Operation requires subscription
SubscriptionNotCancelledException 400 Cannot reactivate active subscription

Testing

Unit tests for the billing system:

# Run billing service tests
pytest tests/unit/services/test_billing_service.py -v

# Run webhook handler tests
pytest tests/unit/services/test_stripe_webhook_handler.py -v

Test Coverage

  • BillingService: Subscription queries, checkout, portal, cancellation
  • StripeWebhookHandler: Event idempotency, checkout completion, invoice handling

Migration

Creating Tiers

Tiers are seeded via migration:

# alembic/versions/xxx_add_subscription_billing_tables.py
def seed_subscription_tiers(op):
    op.bulk_insert(subscription_tiers_table, [
        {
            "code": "essential",
            "name": "Essential",
            "price_monthly_cents": 4900,
            "orders_per_month": 100,
            "products_limit": 200,
            "team_members": 1,
        },
        # ... more tiers
    ])

Capacity Snapshots Table

The capacity_snapshots table stores daily metrics:

# alembic/versions/l0a1b2c3d4e5_add_capacity_snapshots_table.py
class CapacitySnapshot(Base):
    id: int
    snapshot_date: datetime  # Unique per day

    # Vendor metrics
    total_vendors: int
    active_vendors: int
    trial_vendors: int

    # Subscription metrics
    total_subscriptions: int
    active_subscriptions: int

    # Resource metrics
    total_products: int
    total_orders_month: int
    total_team_members: int

    # Storage metrics
    storage_used_gb: Decimal
    db_size_mb: Decimal

    # Capacity metrics
    theoretical_products_limit: int
    theoretical_orders_limit: int
    theoretical_team_limit: int

    # Tier distribution
    tier_distribution: dict  # JSON

Setting Up Stripe

  1. Create products and prices in Stripe Dashboard
  2. Update SubscriptionTier records with Stripe IDs:
tier.stripe_product_id = "prod_xxx"
tier.stripe_price_monthly_id = "price_xxx"
tier.stripe_price_annual_id = "price_yyy"
  1. Configure webhook endpoint in Stripe Dashboard:
    • URL: https://yourdomain.com/api/v1/webhooks/stripe
    • Events: checkout.session.completed, customer.subscription.*, invoice.*

Security Considerations

  • Webhook signatures verified before processing
  • Idempotency keys prevent duplicate event processing
  • Customer portal links are session-based and expire
  • Stripe API key stored securely in environment variables
  • Background tasks run with database session isolation