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:
270
docs/features/subscription-billing.md
Normal file
270
docs/features/subscription-billing.md
Normal 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
|
||||
Reference in New Issue
Block a user