# 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
