feat: add subscription and billing system with Stripe integration

- 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>
This commit is contained in:
2025-12-25 20:29:44 +01:00
parent b98c7c553b
commit 9d8d5e7138
27 changed files with 4558 additions and 23 deletions

View File

@@ -0,0 +1,270 @@
# 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