Clean up accumulated backward-compat shims, deprecated wrappers, unused aliases, and legacy code across the codebase. Since the platform is not live yet, this establishes a clean baseline. Changes: - Delete deprecated middleware/context.py (RequestContext, get_request_context) - Remove unused factory get_store_email_settings_service() - Remove deprecated pagination_full macro, /admin/platform-homepage route - Remove ConversationResponse, InvoiceSettings* unprefixed aliases - Simplify celery_config.py (remove empty LEGACY_TASK_MODULES) - Standardize billing exceptions: *Error aliases → *Exception names - Consolidate duplicate TierNotFoundError/FeatureNotFoundError classes - Remove deprecated is_admin_request() from Store/PlatformContextManager - Remove is_platform_default field, MediaUploadResponse legacy flat fields - Remove MediaItemResponse.url alias, update JS to use file_url - Update all affected tests and documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 byFeatureAggregatorService - 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:
TierFeatureLimitreplaces hardcoded tier columns (orders_per_month,products_limit,team_members). Each feature limit is a row linking a tier to a feature code with alimit_value.MerchantFeatureOverrideprovides per-merchant exceptions to tier defaults.- Module feature providers implement
FeatureProviderProtocolto supply current usage data. FeatureAggregatorServicecollects usage from all providers and combines it with tier limits to produceFeatureSummaryrecords.
┌──────────────────────────────────────────────────────────────┐
│ 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:
- Open panel:
GET /admin/subscriptions/features/catalog(all available features) - Open panel:
GET /admin/subscriptions/features/tiers/{code}/limits(current tier limits) - 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:
- Open edit modal for a subscription
- Fetches feature catalog + current merchant overrides
- Shows each quantitative feature with override input (or "Tier default" placeholder)
- 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:
- Current Plan: Tier name, status, next billing date
- Usage Meters: Dynamic usage bars from feature providers
- Change Plan: Tier cards showing
feature_codeslist - Payment Method: Link to Stripe portal
- Invoice History: Recent invoices with PDF links
- 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
- Go to Stripe Dashboard
- Copy your Publishable key (
pk_test_...orpk_live_...) - Copy your Secret key (
sk_test_...orsk_live_...)
Step 2: Create Webhook Endpoint
- Go to Stripe Webhooks
- Click Add endpoint
- Enter your endpoint URL:
https://yourdomain.com/api/v1/webhooks/stripe - Select events to listen to:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failed
- Click Add endpoint
- Copy the Signing secret (
whsec_...) - this is yourSTRIPE_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:
- Go to Stripe Products
- Create products for each tier (Essential, Professional, Business, Enterprise)
- Add monthly and annual prices for each
- 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 | €5/month | |
email_10 |
10 Email Addresses | €9/month | |
email_25 |
25 Email Addresses | €19/month |
Purchase Flow
- Store selects add-on on billing page
- For domains: enter domain name, validate availability
- Create Stripe checkout session with add-on price
- On webhook success: create
StoreAddOnrecord
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
}
}
Growth Trends
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 |
|---|---|---|
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
# 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-onsStripeWebhookHandler: Event idempotency, checkout completion, status mappingFeatureService: Feature aggregation, tier limits, merchant overridesUsageService: 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
Related Documentation
- Feature Gating System - Feature access control and UI integration
- Metrics Provider Pattern - Protocol-based metrics from modules
- Capacity Monitoring - Detailed monitoring guide
- Capacity Planning - Infrastructure sizing
- Stripe Integration - Payment setup
- Subscription Tier Management - User guide for tier management