# 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