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:
2026-02-07 21:17:11 +01:00
parent 4cb2bda575
commit 2250054ba2
30 changed files with 1220 additions and 805 deletions

View File

@@ -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

View File

@@ -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