Files
orion/docs/features/subscription-billing.md
Samir Boulahtit d803e1c911 feat: add feature assignment to admin tier management UI
- Add slide-over panel for assigning features to subscription tiers
- Features grouped by category with select all/deselect all
- Add puzzle-piece icon button in tier table actions
- Add feature management methods to subscription-tiers.js
- Fix JS-006 by adding try/catch to init function

Documentation:
- Update feature-gating-system.md with Admin Tier Management UI section
- Update subscription-billing.md with tier management overview
- Add new admin user guide: subscription-tier-management.md
- Add guide to mkdocs.yml navigation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 12:58:09 +01:00

617 lines
18 KiB
Markdown

# 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`:
```python
# 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:
```python
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:
```python
tier.features = [
"basic_support", # Essential
"priority_support", # Professional+
"analytics", # Business+
"api_access", # Business+
"white_label", # Enterprise
"custom_integrations", # Enterprise
]
```
### Admin Tier Management
Administrators can manage subscription tiers at `/admin/subscription-tiers`:
**Capabilities:**
- View all tiers with stats (total, active, public, MRR)
- Create new tiers with custom pricing and limits
- Edit tier properties (name, pricing, limits, Stripe IDs)
- Activate/deactivate tiers
- Assign features to tiers via slide-over panel
**Feature Assignment:**
1. Click the puzzle-piece icon on any tier row
2. Features are displayed grouped by category
3. Use checkboxes to select/deselect features
4. Use "Select all" / "Deselect all" per category
5. Click "Save Features" to update
See [Feature Gating System](../implementation/feature-gating-system.md#admin-tier-management-ui) for technical details.
## Limit Enforcement
Limits are enforced at the service layer:
### Orders
```python
# app/services/order_service.py
subscription_service.check_order_limit(db, vendor_id)
```
### Products
```python
# app/api/v1/vendor/products.py
subscription_service.check_product_limit(db, vendor_id)
```
### Team Members
```python
# app/services/vendor_team_service.py
subscription_service.can_add_team_member(db, vendor_id)
```
## Stripe Integration
### Configuration
Required environment variables:
```bash
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](https://dashboard.stripe.com/apikeys)
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](https://dashboard.stripe.com/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](https://stripe.com/docs/stripe-cli):
```bash
# 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](https://dashboard.stripe.com/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`:
```python
# 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`):
```javascript
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
```python
# 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):
```python
# 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:
```python
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
}
}
```
### Growth Trends
Analyze growth over time:
```python
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:
```python
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:
```bash
# 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:
```python
# 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:
```python
# 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:
```python
tier.stripe_product_id = "prod_xxx"
tier.stripe_price_monthly_id = "price_xxx"
tier.stripe_price_annual_id = "price_yyy"
```
3. 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
## Related Documentation
- [Capacity Monitoring](../operations/capacity-monitoring.md) - Detailed monitoring guide
- [Capacity Planning](../architecture/capacity-planning.md) - Infrastructure sizing
- [Stripe Integration](../deployment/stripe-integration.md) - Payment setup for vendors