feat: consolidate media service, add merchant users page, fix metrics overlap
- Merge ImageService into MediaService with WebP variant generation, DB-backed storage stats, and module-driven media usage discovery via new MediaUsageProviderProtocol - Add merchant users admin page with scoped user listing, stats endpoint, template, JS, and i18n strings (de/en/fr/lb) - Fix merchant user metrics so Owners and Team Members are mutually exclusive (filter team_members on user_type="member" and exclude owner IDs) ensuring stat cards add up correctly - Update billing and monitoring services to use media_service - Update subscription-billing and feature-gating docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,65 +1,131 @@
|
||||
# Subscription & Billing System
|
||||
|
||||
The platform provides a comprehensive subscription and billing system for managing store subscriptions, usage limits, and payments through Stripe.
|
||||
The platform provides a comprehensive subscription and billing system for managing merchant subscriptions, feature-based 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
|
||||
- **Subscription Tiers**: Database-driven tier definitions with configurable feature limits
|
||||
- **Feature Provider Pattern**: Modules declare features and usage via `FeatureProviderProtocol`, aggregated by `FeatureAggregatorService`
|
||||
- **Dynamic Usage Tracking**: Quantitative features (orders, products, team members) tracked per merchant with dynamic limits from `TierFeatureLimit`
|
||||
- **Binary Feature Gating**: Toggle-based features (analytics, API access, white-label) controlled per tier
|
||||
- **Merchant-Level Billing**: Subscriptions are per merchant+platform, not per store
|
||||
- **Stripe Integration**: Checkout sessions, customer portal, and webhook handling
|
||||
- **Self-Service Billing**: Store-facing billing page for subscription management
|
||||
- **Add-ons**: Optional purchasable items (domains, SSL, email packages)
|
||||
- **Capacity Forecasting**: Growth trends and scaling recommendations
|
||||
- **Background Jobs**: Automated subscription lifecycle management
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Concepts
|
||||
|
||||
The billing system uses a **feature provider pattern** where:
|
||||
|
||||
1. **`TierFeatureLimit`** replaces hardcoded tier columns (`orders_per_month`, `products_limit`, `team_members`). Each feature limit is a row linking a tier to a feature code with a `limit_value`.
|
||||
2. **`MerchantFeatureOverride`** provides per-merchant exceptions to tier defaults.
|
||||
3. **Module feature providers** implement `FeatureProviderProtocol` to supply current usage data.
|
||||
4. **`FeatureAggregatorService`** collects usage from all providers and combines it with tier limits to produce `FeatureSummary` records.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Frontend Page Request │
|
||||
│ (Store Billing, Admin Subscriptions, Admin Store Detail) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ FeatureAggregatorService │
|
||||
│ (app/modules/billing/services/feature_service.py) │
|
||||
│ │
|
||||
│ • Collects feature providers from all enabled modules │
|
||||
│ • Queries TierFeatureLimit for limit values │
|
||||
│ • Queries MerchantFeatureOverride for per-merchant limits │
|
||||
│ • Calls provider.get_current_usage() for live counts │
|
||||
│ • Returns FeatureSummary[] with current/limit/percentage │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||
│ catalog module │ │ orders module │ │ tenancy module │
|
||||
│ products count │ │ orders count │ │ team members │
|
||||
└────────────────┘ └────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
### Database Models
|
||||
|
||||
All subscription models are defined in `models/database/subscription.py`:
|
||||
All subscription models are in `app/modules/billing/models/`:
|
||||
|
||||
| Model | Purpose |
|
||||
|-------|---------|
|
||||
| `SubscriptionTier` | Tier definitions with limits and Stripe price IDs |
|
||||
| `StoreSubscription` | Per-store subscription status and usage |
|
||||
| `SubscriptionTier` | Tier definitions with Stripe price IDs and feature codes |
|
||||
| `TierFeatureLimit` | Per-tier feature limits (feature_code + limit_value) |
|
||||
| `MerchantSubscription` | Per-merchant+platform subscription status |
|
||||
| `MerchantFeatureOverride` | Per-merchant feature limit overrides |
|
||||
| `AddOnProduct` | Purchasable add-ons (domains, SSL, email) |
|
||||
| `StoreAddOn` | Add-ons purchased by each store |
|
||||
| `StripeWebhookEvent` | Idempotency tracking for webhooks |
|
||||
| `BillingHistory` | Invoice and payment history |
|
||||
| `CapacitySnapshot` | Daily platform capacity metrics for forecasting |
|
||||
|
||||
### Feature Types
|
||||
|
||||
Features come in two types:
|
||||
|
||||
| Type | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| **Quantitative** | Has a numeric limit with usage tracking | `max_products` (limit: 200, current: 150) |
|
||||
| **Binary** | Toggle-based, either enabled or disabled | `analytics_dashboard` (enabled/disabled) |
|
||||
|
||||
### FeatureSummary Dataclass
|
||||
|
||||
The core data structure returned by the feature system:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class FeatureSummary:
|
||||
code: str # e.g., "max_products"
|
||||
name_key: str # i18n key for display name
|
||||
limit: int | None # None = unlimited
|
||||
current: int # Current usage count
|
||||
remaining: int # Remaining before limit
|
||||
percent_used: float # 0.0 to 100.0
|
||||
feature_type: str # "quantitative" or "binary"
|
||||
scope: str # "tier" or "merchant_override"
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `BillingService` | `app/services/billing_service.py` | Subscription operations, checkout, portal |
|
||||
| `SubscriptionService` | `app/services/subscription_service.py` | Limit checks, usage tracking, tier info |
|
||||
| `StripeService` | `app/services/stripe_service.py` | Core Stripe API operations |
|
||||
| `CapacityForecastService` | `app/services/capacity_forecast_service.py` | Growth trends, projections |
|
||||
| `PlatformHealthService` | `app/services/platform_health_service.py` | Subscription capacity aggregation |
|
||||
| `FeatureAggregatorService` | `app/modules/billing/services/feature_service.py` | Aggregates usage from module providers, resolves tier limits + overrides |
|
||||
| `BillingService` | `app/modules/billing/services/billing_service.py` | Subscription operations, checkout, portal |
|
||||
| `SubscriptionService` | `app/modules/billing/services/subscription_service.py` | Subscription CRUD, tier lookups |
|
||||
| `AdminSubscriptionService` | `app/modules/billing/services/admin_subscription_service.py` | Admin subscription management |
|
||||
| `StripeService` | `app/modules/billing/services/stripe_service.py` | Core Stripe API operations |
|
||||
| `CapacityForecastService` | `app/modules/billing/services/capacity_forecast_service.py` | Growth trends, projections |
|
||||
|
||||
### Background Tasks
|
||||
|
||||
| Task | Location | Schedule | Purpose |
|
||||
|------|----------|----------|---------|
|
||||
| `reset_period_counters` | `app/tasks/subscription_tasks.py` | Daily | Reset order counters at period end |
|
||||
| `check_trial_expirations` | `app/tasks/subscription_tasks.py` | Daily | Expire trials without payment method |
|
||||
| `sync_stripe_status` | `app/tasks/subscription_tasks.py` | Hourly | Sync status with Stripe |
|
||||
| `cleanup_stale_subscriptions` | `app/tasks/subscription_tasks.py` | Weekly | Clean up old cancelled subscriptions |
|
||||
| `capture_capacity_snapshot` | `app/tasks/subscription_tasks.py` | Daily | Capture capacity metrics snapshot |
|
||||
| `reset_period_counters` | `app/modules/billing/tasks/subscription.py` | Daily | Reset order counters at period end |
|
||||
| `check_trial_expirations` | `app/modules/billing/tasks/subscription.py` | Daily | Expire trials without payment method |
|
||||
| `sync_stripe_status` | `app/modules/billing/tasks/subscription.py` | Hourly | Sync status with Stripe |
|
||||
| `cleanup_stale_subscriptions` | `app/modules/billing/tasks/subscription.py` | Weekly | Clean up old cancelled subscriptions |
|
||||
| `capture_capacity_snapshot` | `app/modules/billing/tasks/subscription.py` | Daily | Capture capacity metrics snapshot |
|
||||
|
||||
### API Endpoints
|
||||
## API Endpoints
|
||||
|
||||
#### Store Billing API
|
||||
### Store Billing API
|
||||
|
||||
All billing endpoints are under `/api/v1/store/billing`:
|
||||
Base: `/api/v1/store/billing`
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/billing/subscription` | GET | Current subscription status & usage |
|
||||
| `/billing/subscription` | GET | Current subscription status |
|
||||
| `/billing/tiers` | GET | Available tiers for upgrade |
|
||||
| `/billing/usage` | GET | Dynamic usage metrics (from feature providers) |
|
||||
| `/billing/checkout` | POST | Create Stripe checkout session |
|
||||
| `/billing/portal` | POST | Create Stripe customer portal session |
|
||||
| `/billing/invoices` | GET | Invoice history |
|
||||
@@ -72,7 +138,117 @@ All billing endpoints are under `/api/v1/store/billing`:
|
||||
| `/billing/cancel` | POST | Cancel subscription |
|
||||
| `/billing/reactivate` | POST | Reactivate cancelled subscription |
|
||||
|
||||
#### Admin Platform Health API
|
||||
The `/billing/usage` endpoint returns `UsageMetric[]`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Products",
|
||||
"current": 150,
|
||||
"limit": 200,
|
||||
"percentage": 75.0,
|
||||
"is_unlimited": false,
|
||||
"is_at_limit": false,
|
||||
"is_approaching_limit": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Admin Subscription API
|
||||
|
||||
Base: `/api/v1/admin/subscriptions`
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/tiers` | GET | List all subscription tiers |
|
||||
| `/tiers` | POST | Create a new tier |
|
||||
| `/tiers/{code}` | PATCH | Update a tier |
|
||||
| `/tiers/{code}` | DELETE | Delete a tier |
|
||||
| `/stats` | GET | Subscription statistics |
|
||||
| `/merchants/{id}/platforms/{pid}` | GET | Get merchant subscription |
|
||||
| `/merchants/{id}/platforms/{pid}` | PUT | Update merchant subscription |
|
||||
| `/store/{store_id}` | GET | Convenience: get subscription + usage for a store |
|
||||
|
||||
### Admin Feature Management API
|
||||
|
||||
Base: `/api/v1/admin/subscriptions/features`
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/catalog` | GET | Feature catalog grouped by category |
|
||||
| `/tiers/{code}/limits` | GET | Get feature limits for a tier |
|
||||
| `/tiers/{code}/limits` | PUT | Upsert feature limits for a tier |
|
||||
| `/merchants/{id}/overrides` | GET | Get merchant feature overrides |
|
||||
| `/merchants/{id}/overrides` | PUT | Upsert merchant feature overrides |
|
||||
|
||||
The **feature catalog** returns features grouped by category:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"analytics": [
|
||||
{"code": "basic_analytics", "name": "Basic Analytics", "feature_type": "binary", "category": "analytics"},
|
||||
{"code": "analytics_dashboard", "name": "Analytics Dashboard", "feature_type": "binary", "category": "analytics"}
|
||||
],
|
||||
"limits": [
|
||||
{"code": "max_products", "name": "Product Limit", "feature_type": "quantitative", "category": "limits"},
|
||||
{"code": "max_orders_per_month", "name": "Orders per Month", "feature_type": "quantitative", "category": "limits"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tier feature limits** use `TierFeatureLimitEntry[]` format:
|
||||
|
||||
```json
|
||||
[
|
||||
{"feature_code": "max_products", "limit_value": 200, "enabled": true},
|
||||
{"feature_code": "max_orders_per_month", "limit_value": 100, "enabled": true},
|
||||
{"feature_code": "analytics_dashboard", "limit_value": null, "enabled": true}
|
||||
]
|
||||
```
|
||||
|
||||
### Admin Store Convenience Endpoint
|
||||
|
||||
`GET /api/v1/admin/subscriptions/store/{store_id}` resolves a store to its merchant and returns subscription + usage in one call:
|
||||
|
||||
```json
|
||||
{
|
||||
"subscription": {
|
||||
"tier": "professional",
|
||||
"status": "active",
|
||||
"period_start": "2026-01-01T00:00:00Z",
|
||||
"period_end": "2026-02-01T00:00:00Z"
|
||||
},
|
||||
"tier": {
|
||||
"code": "professional",
|
||||
"name": "Professional",
|
||||
"price_monthly_cents": 9900
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"name": "Products",
|
||||
"current": 150,
|
||||
"limit": null,
|
||||
"percentage": 0,
|
||||
"is_unlimited": true,
|
||||
"is_at_limit": false,
|
||||
"is_approaching_limit": false
|
||||
},
|
||||
{
|
||||
"name": "Orders per Month",
|
||||
"current": 320,
|
||||
"limit": 500,
|
||||
"percentage": 64.0,
|
||||
"is_unlimited": false,
|
||||
"is_at_limit": false,
|
||||
"is_approaching_limit": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Platform Health API
|
||||
|
||||
Capacity endpoints under `/api/v1/admin/platform-health`:
|
||||
|
||||
@@ -87,97 +263,140 @@ Capacity endpoints under `/api/v1/admin/platform-health`:
|
||||
|
||||
## Subscription Tiers
|
||||
|
||||
### Default Tiers
|
||||
### Tier Structure
|
||||
|
||||
| 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 |
|
||||
Tiers are stored in the `subscription_tiers` table. Feature limits are stored separately in `tier_feature_limits`:
|
||||
|
||||
### Database-Driven Tiers
|
||||
|
||||
Tiers are stored in the `subscription_tiers` table and queried via `SubscriptionService`:
|
||||
|
||||
```python
|
||||
# Get tier from database with legacy fallback
|
||||
tier_info = subscription_service.get_tier_info("professional", db=db)
|
||||
|
||||
# Query all active tiers
|
||||
all_tiers = subscription_service.get_all_tiers(db=db)
|
||||
```
|
||||
SubscriptionTier (essential)
|
||||
├── TierFeatureLimit: max_products = 200
|
||||
├── TierFeatureLimit: max_orders_per_month = 100
|
||||
├── TierFeatureLimit: max_team_members = 1
|
||||
├── TierFeatureLimit: basic_support (binary, enabled)
|
||||
└── TierFeatureLimit: basic_analytics (binary, enabled)
|
||||
|
||||
The service maintains backward compatibility with a legacy fallback:
|
||||
|
||||
```python
|
||||
def get_tier_info(self, tier_code: str, db: Session | None = None) -> TierInfo:
|
||||
"""Get full tier information. Queries database if db session provided."""
|
||||
if db is not None:
|
||||
db_tier = self.get_tier_by_code(db, tier_code)
|
||||
if db_tier:
|
||||
return TierInfo(...)
|
||||
|
||||
# Fallback to hardcoded TIER_LIMITS during migration
|
||||
return self._get_tier_from_legacy(tier_code)
|
||||
```
|
||||
|
||||
### 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
|
||||
]
|
||||
SubscriptionTier (professional)
|
||||
├── TierFeatureLimit: max_products = NULL (unlimited)
|
||||
├── TierFeatureLimit: max_orders_per_month = 500
|
||||
├── TierFeatureLimit: max_team_members = 3
|
||||
├── TierFeatureLimit: priority_support (binary, enabled)
|
||||
├── TierFeatureLimit: analytics_dashboard (binary, enabled)
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Admin Tier Management
|
||||
|
||||
Administrators can manage subscription tiers at `/admin/subscription-tiers`:
|
||||
Administrators manage tiers at `/admin/subscription-tiers`:
|
||||
|
||||
**Capabilities:**
|
||||
|
||||
- View all tiers with stats (total, active, public, MRR)
|
||||
- Create new tiers with custom pricing and limits
|
||||
- Edit tier properties (name, pricing, limits, Stripe IDs)
|
||||
- Create/edit tiers with pricing and Stripe IDs
|
||||
- Activate/deactivate tiers
|
||||
- Assign features to tiers via slide-over panel
|
||||
- Assign features to tiers via slide-over panel with:
|
||||
- Binary features: checkbox toggles grouped by category
|
||||
- Quantitative features: checkbox + numeric limit input
|
||||
- Select all / Deselect all per category
|
||||
|
||||
**Feature Assignment:**
|
||||
1. Click the puzzle-piece icon on any tier row
|
||||
2. Features are displayed grouped by category
|
||||
3. Use checkboxes to select/deselect features
|
||||
4. Use "Select all" / "Deselect all" per category
|
||||
5. Click "Save Features" to update
|
||||
**Feature Panel API Flow:**
|
||||
|
||||
See [Feature Gating System](../implementation/feature-gating-system.md#admin-tier-management-ui) for technical details.
|
||||
1. Open panel: `GET /admin/subscriptions/features/catalog` (all available features)
|
||||
2. Open panel: `GET /admin/subscriptions/features/tiers/{code}/limits` (current tier limits)
|
||||
3. Save: `PUT /admin/subscriptions/features/tiers/{code}/limits` (upsert limits)
|
||||
|
||||
## Limit Enforcement
|
||||
### Per-Merchant Overrides
|
||||
|
||||
Limits are enforced at the service layer:
|
||||
Admins can override tier limits for individual merchants via the subscription edit modal:
|
||||
|
||||
### Orders
|
||||
```python
|
||||
# app/services/order_service.py
|
||||
subscription_service.check_order_limit(db, store_id)
|
||||
1. Open edit modal for a subscription
|
||||
2. Fetches feature catalog + current merchant overrides
|
||||
3. Shows each quantitative feature with override input (or "Tier default" placeholder)
|
||||
4. Save sends `PUT /admin/subscriptions/features/merchants/{id}/overrides`
|
||||
|
||||
## Frontend Pages
|
||||
|
||||
### Store Billing Page
|
||||
|
||||
**Location:** `/store/{store_code}/billing`
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/store/billing.html`
|
||||
**JS:** `app/modules/billing/static/store/js/billing.js`
|
||||
|
||||
The billing page fetches usage metrics dynamically from `GET /store/billing/usage` and renders them with Alpine.js:
|
||||
|
||||
```html
|
||||
<template x-for="metric in usageMetrics" :key="metric.name">
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span x-text="metric.name"></span>
|
||||
<span x-text="metric.is_unlimited ? metric.current + ' (Unlimited)' : metric.current + ' / ' + metric.limit"></span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="h-2 rounded-full"
|
||||
:class="metric.percentage >= 90 ? 'bg-red-600' : metric.percentage >= 70 ? 'bg-yellow-600' : 'bg-purple-600'"
|
||||
:style="`width: ${Math.min(100, metric.percentage || 0)}%`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Products
|
||||
```python
|
||||
# app/api/v1/store/products.py
|
||||
subscription_service.check_product_limit(db, store_id)
|
||||
**Page sections:**
|
||||
|
||||
1. **Current Plan**: Tier name, status, next billing date
|
||||
2. **Usage Meters**: Dynamic usage bars from feature providers
|
||||
3. **Change Plan**: Tier cards showing `feature_codes` list
|
||||
4. **Payment Method**: Link to Stripe portal
|
||||
5. **Invoice History**: Recent invoices with PDF links
|
||||
6. **Add-ons**: Available and purchased add-ons
|
||||
|
||||
### Admin Subscriptions Page
|
||||
|
||||
**Location:** `/admin/subscriptions`
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/admin/subscriptions.html`
|
||||
**JS:** `app/modules/billing/static/admin/js/subscriptions.js`
|
||||
|
||||
Lists all merchant subscriptions with:
|
||||
|
||||
- Tier, status, merchant info, period dates
|
||||
- Features count column (from `feature_codes.length`)
|
||||
- Edit modal with dynamic feature override editor
|
||||
|
||||
### Admin Subscription Tiers Page
|
||||
|
||||
**Location:** `/admin/subscription-tiers`
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/admin/subscription-tiers.html`
|
||||
**JS:** `app/modules/billing/static/admin/js/subscription-tiers.js`
|
||||
|
||||
Manages tier definitions with:
|
||||
|
||||
- Stats cards (total, active, public, MRR)
|
||||
- Tier table (code, name, pricing, features count, status)
|
||||
- Create/edit modal (pricing, Stripe IDs, description, toggles)
|
||||
- Feature assignment slide-over panel (binary toggles + quantitative limit inputs)
|
||||
|
||||
### Merchant Subscription Detail Page
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/merchant/subscription-detail.html`
|
||||
|
||||
Shows subscription details with plan limits rendered dynamically from `tier.feature_limits`:
|
||||
|
||||
```html
|
||||
<template x-for="fl in (subscription?.tier?.feature_limits || [])" :key="fl.feature_code">
|
||||
<div class="p-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-sm text-gray-500" x-text="fl.feature_code.replace(/_/g, ' ')"></p>
|
||||
<p class="text-xl font-bold" x-text="fl.limit_value || 'Unlimited'"></p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Team Members
|
||||
```python
|
||||
# app/services/store_team_service.py
|
||||
subscription_service.can_add_team_member(db, store_id)
|
||||
```
|
||||
### Admin Store Detail Page
|
||||
|
||||
**Template:** `app/modules/tenancy/templates/tenancy/admin/store-detail.html`
|
||||
**JS:** `app/modules/tenancy/static/admin/js/store-detail.js`
|
||||
|
||||
The subscription section uses the convenience endpoint `GET /admin/subscriptions/store/{store_id}` to load subscription + usage metrics in one call, rendering dynamic usage bars.
|
||||
|
||||
## Stripe Integration
|
||||
|
||||
@@ -239,7 +458,7 @@ stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe
|
||||
Create subscription products for each tier:
|
||||
|
||||
1. Go to [Stripe Products](https://dashboard.stripe.com/products)
|
||||
2. Create products for each tier (Starter, Professional, Business, Enterprise)
|
||||
2. Create products for each tier (Essential, Professional, Business, Enterprise)
|
||||
3. Add monthly and annual prices for each
|
||||
4. Copy the Price IDs (`price_...`) and update your tier configuration
|
||||
|
||||
@@ -252,7 +471,7 @@ The system handles these Stripe events:
|
||||
| `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.paid` | Records payment in billing history |
|
||||
| `invoice.payment_failed` | Marks past due, increments retry count |
|
||||
|
||||
### Webhook Endpoint
|
||||
@@ -262,65 +481,11 @@ Webhooks are received at `/api/v1/webhooks/stripe`:
|
||||
```python
|
||||
# Uses signature verification for security
|
||||
event = stripe_service.construct_event(payload, stripe_signature)
|
||||
handler = StripeWebhookHandler()
|
||||
result = handler.handle_event(db, event)
|
||||
```
|
||||
|
||||
## Store Billing Page
|
||||
|
||||
The store billing page is at `/store/{store_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
|
||||
6. **Add-ons**: Available and purchased add-ons
|
||||
|
||||
### JavaScript Component
|
||||
|
||||
The billing page uses Alpine.js (`static/store/js/billing.js`):
|
||||
|
||||
```javascript
|
||||
function billingData() {
|
||||
return {
|
||||
subscription: null,
|
||||
tiers: [],
|
||||
invoices: [],
|
||||
addons: [],
|
||||
myAddons: [],
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
async purchaseAddon(addon) {
|
||||
const response = await this.apiPost('/billing/addons/purchase', {
|
||||
addon_code: addon.code
|
||||
});
|
||||
window.location.href = response.checkout_url;
|
||||
},
|
||||
|
||||
async cancelAddon(addon) {
|
||||
await this.apiDelete(`/billing/addons/${addon.id}`);
|
||||
await this.loadMyAddons();
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
The handler implements **idempotency** via `StripeWebhookEvent` records. Duplicate events (same `event_id`) are skipped.
|
||||
|
||||
## Add-ons
|
||||
|
||||
@@ -341,58 +506,6 @@ function billingData() {
|
||||
3. Create Stripe checkout session with add-on price
|
||||
4. On webhook success: create `StoreAddOn` record
|
||||
|
||||
## Background Tasks
|
||||
|
||||
### Task Definitions
|
||||
|
||||
```python
|
||||
# app/tasks/subscription_tasks.py
|
||||
|
||||
async def reset_period_counters():
|
||||
"""
|
||||
Reset order counters for subscriptions whose billing period has ended.
|
||||
Should run daily. Resets orders_this_period to 0 and updates period dates.
|
||||
"""
|
||||
|
||||
async def check_trial_expirations():
|
||||
"""
|
||||
Check for expired trials and update their status.
|
||||
Trials without payment method -> expired
|
||||
Trials with payment method -> active
|
||||
"""
|
||||
|
||||
async def sync_stripe_status():
|
||||
"""
|
||||
Sync subscription status with Stripe.
|
||||
Fetches current status and updates local records.
|
||||
"""
|
||||
|
||||
async def cleanup_stale_subscriptions():
|
||||
"""
|
||||
Clean up subscriptions in inconsistent states.
|
||||
Marks old cancelled subscriptions as expired.
|
||||
"""
|
||||
|
||||
async def capture_capacity_snapshot():
|
||||
"""
|
||||
Capture a daily snapshot of platform capacity metrics.
|
||||
Used for growth trending and capacity forecasting.
|
||||
"""
|
||||
```
|
||||
|
||||
### Scheduling
|
||||
|
||||
Configure with your scheduler of choice (Celery, APScheduler, cron):
|
||||
|
||||
```python
|
||||
# Example with APScheduler
|
||||
scheduler.add_job(reset_period_counters, 'cron', hour=0, minute=5)
|
||||
scheduler.add_job(check_trial_expirations, 'cron', hour=1, minute=0)
|
||||
scheduler.add_job(sync_stripe_status, 'cron', minute=0) # Every hour
|
||||
scheduler.add_job(cleanup_stale_subscriptions, 'cron', day_of_week=0) # Weekly
|
||||
scheduler.add_job(capture_capacity_snapshot, 'cron', hour=0, minute=0)
|
||||
```
|
||||
|
||||
## Capacity Forecasting
|
||||
|
||||
### Subscription-Based Capacity
|
||||
@@ -421,11 +534,6 @@ capacity = platform_health_service.get_subscription_capacity(db)
|
||||
"actual": 45000,
|
||||
"theoretical_limit": 300000,
|
||||
"utilization_percent": 15.0
|
||||
},
|
||||
"team_members": {
|
||||
"actual": 320,
|
||||
"theoretical_limit": 1500,
|
||||
"utilization_percent": 21.3
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -437,34 +545,11 @@ Analyze growth over time:
|
||||
```python
|
||||
trends = capacity_forecast_service.get_growth_trends(db, days=30)
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"period_days": 30,
|
||||
"snapshots_available": 30,
|
||||
"trends": {
|
||||
"stores": {
|
||||
"start_value": 140,
|
||||
"current_value": 150,
|
||||
"change": 10,
|
||||
"growth_rate_percent": 7.14,
|
||||
"daily_growth_rate": 0.238,
|
||||
"monthly_projection": 161
|
||||
},
|
||||
"products": {
|
||||
"start_value": 115000,
|
||||
"current_value": 125000,
|
||||
"change": 10000,
|
||||
"growth_rate_percent": 8.7,
|
||||
"monthly_projection": 136000
|
||||
}
|
||||
}
|
||||
}
|
||||
# Returns growth rates, daily projections, monthly projections
|
||||
```
|
||||
|
||||
### Scaling Recommendations
|
||||
|
||||
Get automated scaling advice:
|
||||
|
||||
```python
|
||||
recommendations = capacity_forecast_service.get_scaling_recommendations(db)
|
||||
|
||||
@@ -476,13 +561,6 @@ recommendations = capacity_forecast_service.get_scaling_recommendations(db)
|
||||
"title": "Product capacity approaching limit",
|
||||
"description": "Currently at 85% of theoretical product capacity",
|
||||
"action": "Consider upgrading store tiers or adding capacity"
|
||||
},
|
||||
{
|
||||
"category": "infrastructure",
|
||||
"severity": "info",
|
||||
"title": "Current tier: Medium",
|
||||
"description": "Next upgrade trigger: 300 stores",
|
||||
"action": "Monitor growth and plan for infrastructure scaling"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -500,15 +578,15 @@ recommendations = capacity_forecast_service.get_scaling_recommendations(db)
|
||||
|
||||
## Exception Handling
|
||||
|
||||
Custom exceptions for billing operations (`app/exceptions/billing.py`):
|
||||
Custom exceptions for billing operations (`app/modules/billing/exceptions.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 |
|
||||
| `PaymentSystemNotConfiguredError` | 503 | Stripe not configured |
|
||||
| `TierNotFoundError` | 404 | Invalid tier code |
|
||||
| `StripePriceNotConfiguredError` | 400 | No Stripe price for tier |
|
||||
| `NoActiveSubscriptionError` | 400 | Operation requires subscription |
|
||||
| `SubscriptionNotCancelledError` | 400 | Cannot reactivate active subscription |
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -520,86 +598,20 @@ pytest tests/unit/services/test_billing_service.py -v
|
||||
|
||||
# Run webhook handler tests
|
||||
pytest tests/unit/services/test_stripe_webhook_handler.py -v
|
||||
|
||||
# Run feature service tests
|
||||
pytest tests/unit/services/test_feature_service.py -v
|
||||
|
||||
# Run usage service tests
|
||||
pytest tests/unit/services/test_usage_service.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
|
||||
])
|
||||
```
|
||||
|
||||
### Capacity Snapshots Table
|
||||
|
||||
The `capacity_snapshots` table stores daily metrics:
|
||||
|
||||
```python
|
||||
# alembic/versions/l0a1b2c3d4e5_add_capacity_snapshots_table.py
|
||||
class CapacitySnapshot(Base):
|
||||
id: int
|
||||
snapshot_date: datetime # Unique per day
|
||||
|
||||
# Store metrics
|
||||
total_stores: int
|
||||
active_stores: int
|
||||
trial_stores: int
|
||||
|
||||
# Subscription metrics
|
||||
total_subscriptions: int
|
||||
active_subscriptions: int
|
||||
|
||||
# Resource metrics
|
||||
total_products: int
|
||||
total_orders_month: int
|
||||
total_team_members: int
|
||||
|
||||
# Storage metrics
|
||||
storage_used_gb: Decimal
|
||||
db_size_mb: Decimal
|
||||
|
||||
# Capacity metrics
|
||||
theoretical_products_limit: int
|
||||
theoretical_orders_limit: int
|
||||
theoretical_team_limit: int
|
||||
|
||||
# Tier distribution
|
||||
tier_distribution: dict # JSON
|
||||
```
|
||||
|
||||
### 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.*`
|
||||
- `BillingService`: Tier queries, invoices, add-ons
|
||||
- `StripeWebhookHandler`: Event idempotency, checkout completion, status mapping
|
||||
- `FeatureService`: Feature aggregation, tier limits, merchant overrides
|
||||
- `UsageService`: Usage tracking, limit checks
|
||||
|
||||
## Security Considerations
|
||||
|
||||
@@ -611,6 +623,9 @@ tier.stripe_price_annual_id = "price_yyy"
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Feature Gating System](../implementation/feature-gating-system.md) - Feature access control and UI integration
|
||||
- [Metrics Provider Pattern](../architecture/metrics-provider-pattern.md) - Protocol-based metrics from modules
|
||||
- [Capacity Monitoring](../operations/capacity-monitoring.md) - Detailed monitoring guide
|
||||
- [Capacity Planning](../architecture/capacity-planning.md) - Infrastructure sizing
|
||||
- [Stripe Integration](../deployment/stripe-integration.md) - Payment setup for stores
|
||||
- [Stripe Integration](../deployment/stripe-integration.md) - Payment setup
|
||||
- [Subscription Tier Management](../guides/subscription-tier-management.md) - User guide for tier management
|
||||
|
||||
@@ -354,7 +354,6 @@ The subscription tiers admin page provides full CRUD functionality for managing
|
||||
- Code (colored badge by tier)
|
||||
- Name
|
||||
- Monthly/Annual pricing
|
||||
- Limits (orders, products, team members)
|
||||
- Feature count
|
||||
- Status (Active/Private/Inactive)
|
||||
- Actions (Edit Features, Edit, Activate/Deactivate)
|
||||
@@ -362,7 +361,6 @@ The subscription tiers admin page provides full CRUD functionality for managing
|
||||
3. **Create/Edit Modal**: Form with all tier fields:
|
||||
- Code and Name
|
||||
- Monthly and Annual pricing (in cents)
|
||||
- Order, Product, and Team member limits
|
||||
- Display order
|
||||
- Stripe IDs (optional)
|
||||
- Description
|
||||
@@ -371,9 +369,10 @@ The subscription tiers admin page provides full CRUD functionality for managing
|
||||
4. **Feature Assignment Slide-over Panel**:
|
||||
- Opens when clicking the puzzle-piece icon
|
||||
- Shows all features grouped by category
|
||||
- Checkbox selection with Select all/Deselect all per category
|
||||
- Binary features: checkbox selection with Select all/Deselect all per category
|
||||
- Quantitative features: checkbox + numeric limit input for `limit_value`
|
||||
- Feature count in footer
|
||||
- Save to update tier's feature assignments
|
||||
- Save to update tier's feature assignments via `TierFeatureLimitEntry[]`
|
||||
|
||||
### Files
|
||||
|
||||
@@ -392,10 +391,9 @@ The subscription tiers admin page provides full CRUD functionality for managing
|
||||
| Create tier | POST | `/api/v1/admin/subscriptions/tiers` |
|
||||
| Update tier | PATCH | `/api/v1/admin/subscriptions/tiers/{code}` |
|
||||
| Delete tier | DELETE | `/api/v1/admin/subscriptions/tiers/{code}` |
|
||||
| Load features | GET | `/api/v1/admin/features` |
|
||||
| Load categories | GET | `/api/v1/admin/features/categories` |
|
||||
| Get tier features | GET | `/api/v1/admin/features/tiers/{code}/features` |
|
||||
| Update tier features | PUT | `/api/v1/admin/features/tiers/{code}/features` |
|
||||
| Load feature catalog | GET | `/api/v1/admin/subscriptions/features/catalog` |
|
||||
| Get tier feature limits | GET | `/api/v1/admin/subscriptions/features/tiers/{code}/limits` |
|
||||
| Update tier feature limits | PUT | `/api/v1/admin/subscriptions/features/tiers/{code}/limits` |
|
||||
|
||||
## Migration
|
||||
|
||||
|
||||
Reference in New Issue
Block a user