- Add database models for subscription tiers, vendor subscriptions, add-ons, billing history, and webhook events - Implement BillingService for subscription operations - Implement StripeService for Stripe API operations - Implement StripeWebhookHandler for webhook event processing - Add vendor billing API endpoints for subscription management - Create vendor billing page with Alpine.js frontend - Add limit enforcement for products and team members - Add billing exceptions for proper error handling - Create comprehensive unit tests (40 tests passing) - Add subscription billing documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
271 lines
7.9 KiB
Markdown
271 lines
7.9 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)
|
|
|
|
## 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 |
|
|
| `StripeWebhookHandler` | `app/services/stripe_webhook_handler.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:
|
|
|
|
```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=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`:
|
|
|
|
```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
|
|
|
|
### JavaScript Component
|
|
|
|
The billing page uses Alpine.js (`static/vendor/js/billing.js`):
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```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
|
|
])
|
|
```
|
|
|
|
### 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
|