- 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>
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
- Current Plan: Tier name, status, next billing date
- Usage Meters: Products, orders, team members with limits
- Change Plan: Upgrade/downgrade options
- Payment Method: Link to Stripe portal
- 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 | €5/month | |
email_10 |
10 Email Addresses | €9/month | |
email_25 |
25 Email Addresses | €19/month |
Purchase Flow
- Vendor selects add-on on billing page
- For domains: enter domain name, validate availability
- Create Stripe checkout session with add-on price
- On webhook success: create
VendorAddOnrecord
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, cancellationStripeWebhookHandler: 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
- Create products and prices in Stripe Dashboard
- Update
SubscriptionTierrecords with Stripe IDs:
tier.stripe_product_id = "prod_xxx"
tier.stripe_price_monthly_id = "price_xxx"
tier.stripe_price_annual_id = "price_yyy"
- Configure webhook endpoint in Stripe Dashboard:
- URL:
https://yourdomain.com/api/v1/webhooks/stripe - Events:
checkout.session.completed,customer.subscription.*,invoice.*
- URL:
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