Files
orion/docs/features/subscription-billing.md
Samir Boulahtit 2250054ba2 feat: consolidate media service, add merchant users page, fix metrics overlap
- Merge ImageService into MediaService with WebP variant generation,
  DB-backed storage stats, and module-driven media usage discovery
  via new MediaUsageProviderProtocol
- Add merchant users admin page with scoped user listing, stats
  endpoint, template, JS, and i18n strings (de/en/fr/lb)
- Fix merchant user metrics so Owners and Team Members are mutually
  exclusive (filter team_members on user_type="member" and exclude
  owner IDs) ensuring stat cards add up correctly
- Update billing and monitoring services to use media_service
- Update subscription-billing and feature-gating docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 21:17:11 +01:00

24 KiB

Subscription & Billing System

The platform provides a comprehensive subscription and billing system for managing merchant subscriptions, feature-based usage limits, and payments through Stripe.

Overview

The billing system enables:

  • Subscription Tiers: Database-driven tier definitions with configurable feature limits
  • Feature Provider Pattern: Modules declare features and usage via FeatureProviderProtocol, aggregated by FeatureAggregatorService
  • Dynamic Usage Tracking: Quantitative features (orders, products, team members) tracked per merchant with dynamic limits from TierFeatureLimit
  • Binary Feature Gating: Toggle-based features (analytics, API access, white-label) controlled per tier
  • Merchant-Level Billing: Subscriptions are per merchant+platform, not per store
  • Stripe Integration: Checkout sessions, customer portal, and webhook handling
  • Add-ons: Optional purchasable items (domains, SSL, email packages)
  • Capacity Forecasting: Growth trends and scaling recommendations
  • Background Jobs: Automated subscription lifecycle management

Architecture

Key Concepts

The billing system uses a feature provider pattern where:

  1. TierFeatureLimit replaces hardcoded tier columns (orders_per_month, products_limit, team_members). Each feature limit is a row linking a tier to a feature code with a limit_value.
  2. MerchantFeatureOverride provides per-merchant exceptions to tier defaults.
  3. Module feature providers implement FeatureProviderProtocol to supply current usage data.
  4. FeatureAggregatorService collects usage from all providers and combines it with tier limits to produce FeatureSummary records.
┌──────────────────────────────────────────────────────────────┐
│                   Frontend Page Request                        │
│   (Store Billing, Admin Subscriptions, Admin Store Detail)    │
└──────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────┐
│                  FeatureAggregatorService                      │
│    (app/modules/billing/services/feature_service.py)          │
│                                                                │
│    • Collects feature providers from all enabled modules       │
│    • Queries TierFeatureLimit for limit values                 │
│    • Queries MerchantFeatureOverride for per-merchant limits   │
│    • Calls provider.get_current_usage() for live counts        │
│    • Returns FeatureSummary[] with current/limit/percentage    │
└──────────────────────────────────────────────────────────────┘
             │                    │                    │
             ▼                    ▼                    ▼
    ┌────────────────┐  ┌────────────────┐  ┌────────────────┐
    │ catalog module  │  │ orders module   │  │ tenancy module  │
    │ products count  │  │ orders count    │  │ team members    │
    └────────────────┘  └────────────────┘  └────────────────┘

Database Models

All subscription models are in app/modules/billing/models/:

Model Purpose
SubscriptionTier Tier definitions with Stripe price IDs and feature codes
TierFeatureLimit Per-tier feature limits (feature_code + limit_value)
MerchantSubscription Per-merchant+platform subscription status
MerchantFeatureOverride Per-merchant feature limit overrides
AddOnProduct Purchasable add-ons (domains, SSL, email)
StoreAddOn Add-ons purchased by each store
StripeWebhookEvent Idempotency tracking for webhooks
BillingHistory Invoice and payment history
CapacitySnapshot Daily platform capacity metrics for forecasting

Feature Types

Features come in two types:

Type Description Example
Quantitative Has a numeric limit with usage tracking max_products (limit: 200, current: 150)
Binary Toggle-based, either enabled or disabled analytics_dashboard (enabled/disabled)

FeatureSummary Dataclass

The core data structure returned by the feature system:

@dataclass
class FeatureSummary:
    code: str           # e.g., "max_products"
    name_key: str       # i18n key for display name
    limit: int | None   # None = unlimited
    current: int        # Current usage count
    remaining: int      # Remaining before limit
    percent_used: float # 0.0 to 100.0
    feature_type: str   # "quantitative" or "binary"
    scope: str          # "tier" or "merchant_override"

Services

Service Location Purpose
FeatureAggregatorService app/modules/billing/services/feature_service.py Aggregates usage from module providers, resolves tier limits + overrides
BillingService app/modules/billing/services/billing_service.py Subscription operations, checkout, portal
SubscriptionService app/modules/billing/services/subscription_service.py Subscription CRUD, tier lookups
AdminSubscriptionService app/modules/billing/services/admin_subscription_service.py Admin subscription management
StripeService app/modules/billing/services/stripe_service.py Core Stripe API operations
CapacityForecastService app/modules/billing/services/capacity_forecast_service.py Growth trends, projections

Background Tasks

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

API Endpoints

Store Billing API

Base: /api/v1/store/billing

Endpoint Method Purpose
/billing/subscription GET Current subscription status
/billing/tiers GET Available tiers for upgrade
/billing/usage GET Dynamic usage metrics (from feature providers)
/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 Store'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

The /billing/usage endpoint returns UsageMetric[]:

[
    {
        "name": "Products",
        "current": 150,
        "limit": 200,
        "percentage": 75.0,
        "is_unlimited": false,
        "is_at_limit": false,
        "is_approaching_limit": true
    }
]

Admin Subscription API

Base: /api/v1/admin/subscriptions

Endpoint Method Purpose
/tiers GET List all subscription tiers
/tiers POST Create a new tier
/tiers/{code} PATCH Update a tier
/tiers/{code} DELETE Delete a tier
/stats GET Subscription statistics
/merchants/{id}/platforms/{pid} GET Get merchant subscription
/merchants/{id}/platforms/{pid} PUT Update merchant subscription
/store/{store_id} GET Convenience: get subscription + usage for a store

Admin Feature Management API

Base: /api/v1/admin/subscriptions/features

Endpoint Method Purpose
/catalog GET Feature catalog grouped by category
/tiers/{code}/limits GET Get feature limits for a tier
/tiers/{code}/limits PUT Upsert feature limits for a tier
/merchants/{id}/overrides GET Get merchant feature overrides
/merchants/{id}/overrides PUT Upsert merchant feature overrides

The feature catalog returns features grouped by category:

{
    "features": {
        "analytics": [
            {"code": "basic_analytics", "name": "Basic Analytics", "feature_type": "binary", "category": "analytics"},
            {"code": "analytics_dashboard", "name": "Analytics Dashboard", "feature_type": "binary", "category": "analytics"}
        ],
        "limits": [
            {"code": "max_products", "name": "Product Limit", "feature_type": "quantitative", "category": "limits"},
            {"code": "max_orders_per_month", "name": "Orders per Month", "feature_type": "quantitative", "category": "limits"}
        ]
    }
}

Tier feature limits use TierFeatureLimitEntry[] format:

[
    {"feature_code": "max_products", "limit_value": 200, "enabled": true},
    {"feature_code": "max_orders_per_month", "limit_value": 100, "enabled": true},
    {"feature_code": "analytics_dashboard", "limit_value": null, "enabled": true}
]

Admin Store Convenience Endpoint

GET /api/v1/admin/subscriptions/store/{store_id} resolves a store to its merchant and returns subscription + usage in one call:

{
    "subscription": {
        "tier": "professional",
        "status": "active",
        "period_start": "2026-01-01T00:00:00Z",
        "period_end": "2026-02-01T00:00:00Z"
    },
    "tier": {
        "code": "professional",
        "name": "Professional",
        "price_monthly_cents": 9900
    },
    "features": [
        {
            "name": "Products",
            "current": 150,
            "limit": null,
            "percentage": 0,
            "is_unlimited": true,
            "is_at_limit": false,
            "is_approaching_limit": false
        },
        {
            "name": "Orders per Month",
            "current": 320,
            "limit": 500,
            "percentage": 64.0,
            "is_unlimited": false,
            "is_at_limit": false,
            "is_approaching_limit": false
        }
    ]
}

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

Tier Structure

Tiers are stored in the subscription_tiers table. Feature limits are stored separately in tier_feature_limits:

SubscriptionTier (essential)
    ├── TierFeatureLimit: max_products = 200
    ├── TierFeatureLimit: max_orders_per_month = 100
    ├── TierFeatureLimit: max_team_members = 1
    ├── TierFeatureLimit: basic_support (binary, enabled)
    └── TierFeatureLimit: basic_analytics (binary, enabled)

SubscriptionTier (professional)
    ├── TierFeatureLimit: max_products = NULL (unlimited)
    ├── TierFeatureLimit: max_orders_per_month = 500
    ├── TierFeatureLimit: max_team_members = 3
    ├── TierFeatureLimit: priority_support (binary, enabled)
    ├── TierFeatureLimit: analytics_dashboard (binary, enabled)
    └── ...

Admin Tier Management

Administrators manage tiers at /admin/subscription-tiers:

Capabilities:

  • View all tiers with stats (total, active, public, MRR)
  • Create/edit tiers with pricing and Stripe IDs
  • Activate/deactivate tiers
  • Assign features to tiers via slide-over panel with:
    • Binary features: checkbox toggles grouped by category
    • Quantitative features: checkbox + numeric limit input
    • Select all / Deselect all per category

Feature Panel API Flow:

  1. Open panel: GET /admin/subscriptions/features/catalog (all available features)
  2. Open panel: GET /admin/subscriptions/features/tiers/{code}/limits (current tier limits)
  3. Save: PUT /admin/subscriptions/features/tiers/{code}/limits (upsert limits)

Per-Merchant Overrides

Admins can override tier limits for individual merchants via the subscription edit modal:

  1. Open edit modal for a subscription
  2. Fetches feature catalog + current merchant overrides
  3. Shows each quantitative feature with override input (or "Tier default" placeholder)
  4. Save sends PUT /admin/subscriptions/features/merchants/{id}/overrides

Frontend Pages

Store Billing Page

Location: /store/{store_code}/billing

Template: app/modules/billing/templates/billing/store/billing.html JS: app/modules/billing/static/store/js/billing.js

The billing page fetches usage metrics dynamically from GET /store/billing/usage and renders them with Alpine.js:

<template x-for="metric in usageMetrics" :key="metric.name">
    <div class="mb-4">
        <div class="flex justify-between text-sm mb-1">
            <span x-text="metric.name"></span>
            <span x-text="metric.is_unlimited ? metric.current + ' (Unlimited)' : metric.current + ' / ' + metric.limit"></span>
        </div>
        <div class="w-full bg-gray-200 rounded-full h-2">
            <div class="h-2 rounded-full"
                 :class="metric.percentage >= 90 ? 'bg-red-600' : metric.percentage >= 70 ? 'bg-yellow-600' : 'bg-purple-600'"
                 :style="`width: ${Math.min(100, metric.percentage || 0)}%`"></div>
        </div>
    </div>
</template>

Page sections:

  1. Current Plan: Tier name, status, next billing date
  2. Usage Meters: Dynamic usage bars from feature providers
  3. Change Plan: Tier cards showing feature_codes list
  4. Payment Method: Link to Stripe portal
  5. Invoice History: Recent invoices with PDF links
  6. Add-ons: Available and purchased add-ons

Admin Subscriptions Page

Location: /admin/subscriptions

Template: app/modules/billing/templates/billing/admin/subscriptions.html JS: app/modules/billing/static/admin/js/subscriptions.js

Lists all merchant subscriptions with:

  • Tier, status, merchant info, period dates
  • Features count column (from feature_codes.length)
  • Edit modal with dynamic feature override editor

Admin Subscription Tiers Page

Location: /admin/subscription-tiers

Template: app/modules/billing/templates/billing/admin/subscription-tiers.html JS: app/modules/billing/static/admin/js/subscription-tiers.js

Manages tier definitions with:

  • Stats cards (total, active, public, MRR)
  • Tier table (code, name, pricing, features count, status)
  • Create/edit modal (pricing, Stripe IDs, description, toggles)
  • Feature assignment slide-over panel (binary toggles + quantitative limit inputs)

Merchant Subscription Detail Page

Template: app/modules/billing/templates/billing/merchant/subscription-detail.html

Shows subscription details with plan limits rendered dynamically from tier.feature_limits:

<template x-for="fl in (subscription?.tier?.feature_limits || [])" :key="fl.feature_code">
    <div class="p-4 bg-gray-50 rounded-lg">
        <p class="text-sm text-gray-500" x-text="fl.feature_code.replace(/_/g, ' ')"></p>
        <p class="text-xl font-bold" x-text="fl.limit_value || 'Unlimited'"></p>
    </div>
</template>

Admin Store Detail Page

Template: app/modules/tenancy/templates/tenancy/admin/store-detail.html JS: app/modules/tenancy/static/admin/js/store-detail.js

The subscription section uses the convenience endpoint GET /admin/subscriptions/store/{store_id} to load subscription + usage metrics in one call, rendering dynamic usage bars.

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 (Essential, 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 in billing history
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)
handler = StripeWebhookHandler()
result = handler.handle_event(db, event)

The handler implements idempotency via StripeWebhookEvent records. Duplicate events (same event_id) are skipped.

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. Store 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 StoreAddOn record

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
    }
}

Analyze growth over time:

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

# Returns growth rates, daily projections, monthly projections

Scaling Recommendations

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 store tiers or adding capacity"
    }
]

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/modules/billing/exceptions.py):

Exception HTTP Status Description
PaymentSystemNotConfiguredError 503 Stripe not configured
TierNotFoundError 404 Invalid tier code
StripePriceNotConfiguredError 400 No Stripe price for tier
NoActiveSubscriptionError 400 Operation requires subscription
SubscriptionNotCancelledError 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

# Run feature service tests
pytest tests/unit/services/test_feature_service.py -v

# Run usage service tests
pytest tests/unit/services/test_usage_service.py -v

Test Coverage

  • BillingService: Tier queries, invoices, add-ons
  • StripeWebhookHandler: Event idempotency, checkout completion, status mapping
  • FeatureService: Feature aggregation, tier limits, merchant overrides
  • UsageService: Usage tracking, limit checks

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