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>
597 lines
18 KiB
Markdown
597 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
|
|
]
|
|
```
|
|
|
|
## 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
|