Files
orion/docs/features/subscription-billing.md
Samir Boulahtit 3b67515bc2 refactor: move stripe webhook handler to app/handlers/
- Create app/handlers/ directory for event handlers
- Move stripe_webhook_handler.py to app/handlers/stripe_webhook.py
- Update imports in webhooks.py, tests, and docs
- Handlers are distinct from services (event-driven vs request-driven)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 21:58:48 +01:00

8.0 KiB

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)

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

Services

Service Location Purpose
BillingService app/services/billing_service.py Subscription operations, checkout, portal
SubscriptionService app/services/subscription_service.py Limit checks, usage tracking
StripeService app/services/stripe_service.py Core Stripe API operations

Handlers

Handler Location Purpose
StripeWebhookHandler app/handlers/stripe_webhook.py Webhook event processing

API Endpoints

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/addons GET Available add-on products
/billing/my-addons GET Vendor's purchased add-ons
/billing/cancel POST Cancel subscription
/billing/reactivate POST Reactivate cancelled subscription

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

Tier Features

Each tier includes specific features stored in the features JSON column:

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

# app/services/order_service.py
subscription_service.check_order_limit(db, vendor_id)

Products

# app/api/v1/vendor/products.py
subscription_service.check_product_limit(db, vendor_id)

Team Members

# app/services/vendor_team_service.py
subscription_service.can_add_team_member(db, vendor_id)

Stripe Integration

Configuration

Required environment variables:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_TRIAL_DAYS=14  # Optional, default trial period

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:

# 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

JavaScript Component

The billing page uses Alpine.js (static/vendor/js/billing.js):

function billingData() {
    return {
        subscription: null,
        tiers: [],
        invoices: [],

        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;
        }
    };
}

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

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:

# 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:

# 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
    ])

Setting Up Stripe

  1. Create products and prices in Stripe Dashboard
  2. Update SubscriptionTier records with Stripe IDs:
tier.stripe_product_id = "prod_xxx"
tier.stripe_price_monthly_id = "price_xxx"
tier.stripe_price_annual_id = "price_yyy"
  1. 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