docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
138
app/modules/billing/docs/data-model.md
Normal file
138
app/modules/billing/docs/data-model.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Billing Data Model
|
||||
|
||||
## Entity Relationship Overview
|
||||
|
||||
```
|
||||
┌───────────────────┐
|
||||
│ SubscriptionTier │
|
||||
└────────┬──────────┘
|
||||
│ 1:N
|
||||
▼
|
||||
┌───────────────────┐ ┌──────────────────────┐
|
||||
│ TierFeatureLimit │ │ MerchantSubscription │
|
||||
│ (feature limits) │ │ (per merchant+plat) │
|
||||
└───────────────────┘ └──────────┬───────────┘
|
||||
│
|
||||
┌──────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌──────────┐ ┌─────────────┐
|
||||
│ BillingHist│ │StoreAddOn│ │FeatureOverride│
|
||||
└────────────┘ └──────────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────┐
|
||||
│AddOnProduct│
|
||||
└────────────┘
|
||||
|
||||
┌──────────────────────┐
|
||||
│StripeWebhookEvent │ (idempotency tracking)
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## Core Entities
|
||||
|
||||
### SubscriptionTier
|
||||
|
||||
Defines available subscription plans with pricing and Stripe integration.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | Integer | Primary key |
|
||||
| `code` | String | Unique tier code (`essential`, `professional`, `business`, `enterprise`) |
|
||||
| `name` | String | Display name |
|
||||
| `price_monthly_cents` | Integer | Monthly price in cents |
|
||||
| `price_annual_cents` | Integer | Annual price in cents (optional) |
|
||||
| `stripe_product_id` | String | Stripe product ID |
|
||||
| `stripe_price_monthly_id` | String | Stripe monthly price ID |
|
||||
| `stripe_price_annual_id` | String | Stripe annual price ID |
|
||||
| `display_order` | Integer | Sort order on pricing pages |
|
||||
| `is_active` | Boolean | Available for subscription |
|
||||
| `is_public` | Boolean | Visible to stores |
|
||||
|
||||
### TierFeatureLimit
|
||||
|
||||
Per-tier feature limits — each row links a tier to a feature code with a limit value.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | Integer | Primary key |
|
||||
| `tier_id` | Integer | FK to SubscriptionTier |
|
||||
| `feature_code` | String | Feature identifier (e.g., `max_products`) |
|
||||
| `limit_value` | Integer | Numeric limit (NULL = unlimited) |
|
||||
| `enabled` | Boolean | Whether feature is enabled for this tier |
|
||||
|
||||
### MerchantSubscription
|
||||
|
||||
Per-merchant+platform subscription state. Subscriptions are merchant-level, not store-level.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | Integer | Primary key |
|
||||
| `merchant_id` | Integer | FK to Merchant |
|
||||
| `platform_id` | Integer | FK to Platform |
|
||||
| `tier_id` | Integer | FK to SubscriptionTier |
|
||||
| `tier_code` | String | Tier code (denormalized for convenience) |
|
||||
| `status` | SubscriptionStatus | `trial`, `active`, `past_due`, `cancelled`, `expired` |
|
||||
| `stripe_customer_id` | String | Stripe customer ID |
|
||||
| `stripe_subscription_id` | String | Stripe subscription ID |
|
||||
| `trial_ends_at` | DateTime | Trial expiry |
|
||||
| `period_start` | DateTime | Current billing period start |
|
||||
| `period_end` | DateTime | Current billing period end |
|
||||
|
||||
### MerchantFeatureOverride
|
||||
|
||||
Per-merchant exceptions to tier defaults (e.g., enterprise custom limits).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | Integer | Primary key |
|
||||
| `merchant_id` | Integer | FK to Merchant |
|
||||
| `feature_code` | String | Feature identifier |
|
||||
| `limit_value` | Integer | Override limit (NULL = unlimited) |
|
||||
|
||||
## Add-on Entities
|
||||
|
||||
### AddOnProduct
|
||||
|
||||
Purchasable add-on items (domains, SSL, email packages).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | Integer | Primary key |
|
||||
| `code` | String | Unique add-on code |
|
||||
| `name` | String | Display name |
|
||||
| `category` | AddOnCategory | `domain`, `ssl`, `email` |
|
||||
| `price_cents` | Integer | Price in cents |
|
||||
| `billing_period` | BillingPeriod | `monthly` or `yearly` |
|
||||
|
||||
### StoreAddOn
|
||||
|
||||
Add-ons purchased by individual stores.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | Integer | Primary key |
|
||||
| `store_id` | Integer | FK to Store |
|
||||
| `addon_product_id` | Integer | FK to AddOnProduct |
|
||||
| `config` | JSON | Configuration (e.g., domain name) |
|
||||
| `stripe_subscription_item_id` | String | Stripe subscription item ID |
|
||||
| `status` | String | `active`, `cancelled`, `pending_setup` |
|
||||
|
||||
## Supporting Entities
|
||||
|
||||
### BillingHistory
|
||||
|
||||
Invoice and payment history records.
|
||||
|
||||
### StripeWebhookEvent
|
||||
|
||||
Idempotency tracking for Stripe webhook events. Prevents duplicate event processing.
|
||||
|
||||
## Key Relationships
|
||||
|
||||
- A **SubscriptionTier** has many **TierFeatureLimits** (one per feature)
|
||||
- A **Merchant** has one **MerchantSubscription** per Platform
|
||||
- A **MerchantSubscription** references one **SubscriptionTier**
|
||||
- A **Merchant** can have many **MerchantFeatureOverrides** (per-feature)
|
||||
- A **Store** can purchase many **StoreAddOns**
|
||||
- Feature limits are resolved: MerchantFeatureOverride > TierFeatureLimit > default
|
||||
434
app/modules/billing/docs/feature-gating.md
Normal file
434
app/modules/billing/docs/feature-gating.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# Feature Gating System
|
||||
|
||||
## Overview
|
||||
|
||||
The feature gating system provides tier-based access control for platform features. It allows restricting functionality based on store subscription tiers (Essential, Professional, Business, Enterprise) with contextual upgrade prompts when features are locked.
|
||||
|
||||
**Implemented:** December 31, 2025
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Models
|
||||
|
||||
Located in `models/database/feature.py`:
|
||||
|
||||
| Model | Purpose |
|
||||
|-------|---------|
|
||||
| `Feature` | Feature definitions with tier requirements |
|
||||
| `StoreFeatureOverride` | Per-store feature overrides (enable/disable) |
|
||||
|
||||
### Feature Model Structure
|
||||
|
||||
```python
|
||||
class Feature(Base):
|
||||
__tablename__ = "features"
|
||||
|
||||
id: int # Primary key
|
||||
code: str # Unique feature code (e.g., "analytics_dashboard")
|
||||
name: str # Display name
|
||||
description: str # User-facing description
|
||||
category: str # Feature category
|
||||
minimum_tier_code: str # Minimum tier required (essential/professional/business/enterprise)
|
||||
minimum_tier_order: int # Tier order for comparison (1-4)
|
||||
is_active: bool # Whether feature is available
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
### Tier Ordering
|
||||
|
||||
| Tier | Order | Code |
|
||||
|------|-------|------|
|
||||
| Essential | 1 | `essential` |
|
||||
| Professional | 2 | `professional` |
|
||||
| Business | 3 | `business` |
|
||||
| Enterprise | 4 | `enterprise` |
|
||||
|
||||
## Feature Categories
|
||||
|
||||
30 features organized into 8 categories:
|
||||
|
||||
### 1. Analytics
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `basic_analytics` | Basic Analytics | Essential |
|
||||
| `analytics_dashboard` | Analytics Dashboard | Professional |
|
||||
| `advanced_analytics` | Advanced Analytics | Business |
|
||||
| `custom_reports` | Custom Reports | Enterprise |
|
||||
|
||||
### 2. Product Management
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `basic_products` | Product Management | Essential |
|
||||
| `bulk_product_edit` | Bulk Product Edit | Professional |
|
||||
| `product_variants` | Product Variants | Professional |
|
||||
| `product_bundles` | Product Bundles | Business |
|
||||
| `inventory_alerts` | Inventory Alerts | Professional |
|
||||
|
||||
### 3. Order Management
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `basic_orders` | Order Management | Essential |
|
||||
| `order_automation` | Order Automation | Professional |
|
||||
| `advanced_fulfillment` | Advanced Fulfillment | Business |
|
||||
| `multi_warehouse` | Multi-Warehouse | Enterprise |
|
||||
|
||||
### 4. Marketing
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `discount_codes` | Discount Codes | Professional |
|
||||
| `abandoned_cart` | Abandoned Cart Recovery | Business |
|
||||
| `email_marketing` | Email Marketing | Business |
|
||||
| `loyalty_program` | Loyalty Program | Enterprise |
|
||||
|
||||
### 5. Support
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `basic_support` | Email Support | Essential |
|
||||
| `priority_support` | Priority Support | Professional |
|
||||
| `phone_support` | Phone Support | Business |
|
||||
| `dedicated_manager` | Dedicated Account Manager | Enterprise |
|
||||
|
||||
### 6. Integration
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `basic_api` | Basic API Access | Professional |
|
||||
| `advanced_api` | Advanced API Access | Business |
|
||||
| `webhooks` | Webhooks | Business |
|
||||
| `custom_integrations` | Custom Integrations | Enterprise |
|
||||
|
||||
### 7. Branding
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `basic_theme` | Theme Customization | Essential |
|
||||
| `custom_domain` | Custom Domain | Professional |
|
||||
| `white_label` | White Label | Enterprise |
|
||||
| `custom_checkout` | Custom Checkout | Enterprise |
|
||||
|
||||
### 8. Team
|
||||
| Feature Code | Name | Min Tier |
|
||||
|-------------|------|----------|
|
||||
| `team_management` | Team Management | Professional |
|
||||
| `role_permissions` | Role Permissions | Business |
|
||||
| `audit_logs` | Audit Logs | Business |
|
||||
|
||||
## Services
|
||||
|
||||
### FeatureService
|
||||
|
||||
Located in `app/services/feature_service.py`:
|
||||
|
||||
```python
|
||||
class FeatureService:
|
||||
"""Service for managing tier-based feature access."""
|
||||
|
||||
# In-memory caching (refreshed every 5 minutes)
|
||||
_feature_cache: dict[str, Feature] = {}
|
||||
_cache_timestamp: datetime | None = None
|
||||
CACHE_TTL_SECONDS = 300
|
||||
|
||||
def has_feature(self, db: Session, store_id: int, feature_code: str) -> bool:
|
||||
"""Check if store has access to a feature."""
|
||||
|
||||
def get_available_features(self, db: Session, store_id: int) -> list[str]:
|
||||
"""Get list of feature codes available to store."""
|
||||
|
||||
def get_all_features_with_status(self, db: Session, store_id: int) -> list[dict]:
|
||||
"""Get all features with availability status for store."""
|
||||
|
||||
def get_feature_info(self, db: Session, feature_code: str) -> dict | None:
|
||||
"""Get full feature information including tier requirements."""
|
||||
```
|
||||
|
||||
### UsageService
|
||||
|
||||
Located in `app/services/usage_service.py`:
|
||||
|
||||
```python
|
||||
class UsageService:
|
||||
"""Service for tracking and managing store usage against tier limits."""
|
||||
|
||||
def get_usage_summary(self, db: Session, store_id: int) -> dict:
|
||||
"""Get comprehensive usage summary with limits and upgrade info."""
|
||||
|
||||
def check_limit(self, db: Session, store_id: int, limit_type: str) -> dict:
|
||||
"""Check specific limit with detailed info."""
|
||||
|
||||
def get_upgrade_info(self, db: Session, store_id: int) -> dict:
|
||||
"""Get upgrade recommendations based on current usage."""
|
||||
```
|
||||
|
||||
## Backend Enforcement
|
||||
|
||||
### Decorator Pattern
|
||||
|
||||
```python
|
||||
from app.core.feature_gate import require_feature
|
||||
|
||||
@router.get("/analytics/advanced")
|
||||
@require_feature("advanced_analytics")
|
||||
async def get_advanced_analytics(
|
||||
db: Session = Depends(get_db),
|
||||
store_id: int = Depends(get_current_store_id)
|
||||
):
|
||||
# Only accessible if store has advanced_analytics feature
|
||||
pass
|
||||
```
|
||||
|
||||
### Dependency Pattern
|
||||
|
||||
```python
|
||||
from app.core.feature_gate import RequireFeature
|
||||
|
||||
@router.get("/marketing/loyalty")
|
||||
async def get_loyalty_program(
|
||||
db: Session = Depends(get_db),
|
||||
_: None = Depends(RequireFeature("loyalty_program"))
|
||||
):
|
||||
# Only accessible if store has loyalty_program feature
|
||||
pass
|
||||
```
|
||||
|
||||
### Exception Handling
|
||||
|
||||
When a feature is not available, `FeatureNotAvailableException` is raised:
|
||||
|
||||
```python
|
||||
class FeatureNotAvailableException(Exception):
|
||||
def __init__(self, feature_code: str, required_tier: str):
|
||||
self.feature_code = feature_code
|
||||
self.required_tier = required_tier
|
||||
super().__init__(f"Feature '{feature_code}' requires {required_tier} tier")
|
||||
```
|
||||
|
||||
HTTP Response (403):
|
||||
```json
|
||||
{
|
||||
"detail": "Feature 'advanced_analytics' requires Professional tier or higher",
|
||||
"feature_code": "advanced_analytics",
|
||||
"required_tier": "Professional",
|
||||
"upgrade_url": "/store/orion/billing"
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Store Features API
|
||||
|
||||
Base: `/api/v1/store/features`
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/features/available` | GET | List available feature codes |
|
||||
| `/features` | GET | All features with availability status |
|
||||
| `/features/{code}` | GET | Single feature info |
|
||||
| `/features/{code}/check` | GET | Quick availability check |
|
||||
|
||||
### Store Usage API
|
||||
|
||||
Base: `/api/v1/store/usage`
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/usage` | GET | Full usage summary with limits |
|
||||
| `/usage/check/{limit_type}` | GET | Check specific limit (orders/products/team_members) |
|
||||
| `/usage/upgrade-info` | GET | Upgrade recommendations |
|
||||
|
||||
### Admin Features API
|
||||
|
||||
Base: `/api/v1/admin/features`
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/features` | GET | List all features |
|
||||
| `/features/{id}` | GET | Get feature details |
|
||||
| `/features/{id}` | PUT | Update feature |
|
||||
| `/features/{id}/toggle` | POST | Toggle feature active status |
|
||||
| `/features/stores/{store_id}/overrides` | GET | Get store overrides |
|
||||
| `/features/stores/{store_id}/overrides` | POST | Create override |
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Alpine.js Feature Store
|
||||
|
||||
Located in `static/shared/js/feature-store.js`:
|
||||
|
||||
```javascript
|
||||
// Usage in templates
|
||||
$store.features.has('analytics_dashboard') // Check feature
|
||||
$store.features.loaded // Loading state
|
||||
$store.features.getFeature('advanced_api') // Get feature details
|
||||
```
|
||||
|
||||
### Alpine.js Upgrade Store
|
||||
|
||||
Located in `static/shared/js/upgrade-prompts.js`:
|
||||
|
||||
```javascript
|
||||
// Usage in templates
|
||||
$store.upgrade.shouldShowLimitWarning('orders')
|
||||
$store.upgrade.getUsageString('products')
|
||||
$store.upgrade.hasUpgradeRecommendation
|
||||
```
|
||||
|
||||
### Jinja2 Macros
|
||||
|
||||
Located in `app/templates/shared/macros/feature_gate.html`:
|
||||
|
||||
#### Feature Gate Container
|
||||
```jinja2
|
||||
{% from "shared/macros/feature_gate.html" import feature_gate %}
|
||||
|
||||
{% call feature_gate("analytics_dashboard") %}
|
||||
<div>Analytics content here - only visible if feature available</div>
|
||||
{% endcall %}
|
||||
```
|
||||
|
||||
#### Feature Locked Card
|
||||
```jinja2
|
||||
{% from "shared/macros/feature_gate.html" import feature_locked %}
|
||||
|
||||
{{ feature_locked("advanced_analytics", "Advanced Analytics", "Get deeper insights") }}
|
||||
```
|
||||
|
||||
#### Upgrade Banner
|
||||
```jinja2
|
||||
{% from "shared/macros/feature_gate.html" import upgrade_banner %}
|
||||
|
||||
{{ upgrade_banner("custom_domain") }}
|
||||
```
|
||||
|
||||
#### Usage Limit Warning
|
||||
```jinja2
|
||||
{% from "shared/macros/feature_gate.html" import limit_warning %}
|
||||
|
||||
{{ limit_warning("orders") }} {# Shows warning when approaching limit #}
|
||||
```
|
||||
|
||||
#### Usage Progress Bar
|
||||
```jinja2
|
||||
{% from "shared/macros/feature_gate.html" import usage_bar %}
|
||||
|
||||
{{ usage_bar("products", "Products") }}
|
||||
```
|
||||
|
||||
#### Tier Badge
|
||||
```jinja2
|
||||
{% from "shared/macros/feature_gate.html" import tier_badge %}
|
||||
|
||||
{{ tier_badge() }} {# Shows current tier as colored badge #}
|
||||
```
|
||||
|
||||
## Store Dashboard Integration
|
||||
|
||||
The store dashboard (`/store/{code}/dashboard`) now includes:
|
||||
|
||||
1. **Tier Badge**: Shows current subscription tier in header
|
||||
2. **Usage Bars**: Visual progress bars for orders, products, team members
|
||||
3. **Upgrade Prompts**: Contextual upgrade recommendations when approaching limits
|
||||
4. **Feature Gates**: Locked sections for premium features
|
||||
|
||||
## Admin Features Page
|
||||
|
||||
Located at `/admin/features`:
|
||||
|
||||
- View all 30 features in categorized table
|
||||
- Toggle features on/off globally
|
||||
- Filter by category
|
||||
- Search by name/code
|
||||
- View tier requirements
|
||||
|
||||
## Admin Tier Management UI
|
||||
|
||||
Located at `/admin/subscription-tiers`:
|
||||
|
||||
### Overview
|
||||
|
||||
The subscription tiers admin page provides full CRUD functionality for managing subscription tiers and their feature assignments.
|
||||
|
||||
### Features
|
||||
|
||||
1. **Stats Cards**: Display total tiers, active tiers, public tiers, and estimated MRR
|
||||
2. **Tier Table**: Sortable list of all tiers with:
|
||||
- Display order
|
||||
- Code (colored badge by tier)
|
||||
- Name
|
||||
- Monthly/Annual pricing
|
||||
- Feature count
|
||||
- Status (Active/Private/Inactive)
|
||||
- Actions (Edit Features, Edit, Activate/Deactivate)
|
||||
|
||||
3. **Create/Edit Modal**: Form with all tier fields:
|
||||
- Code and Name
|
||||
- Monthly and Annual pricing (in cents)
|
||||
- Display order
|
||||
- Stripe IDs (optional)
|
||||
- Description
|
||||
- Active/Public toggles
|
||||
|
||||
4. **Feature Assignment Slide-over Panel**:
|
||||
- Opens when clicking the puzzle-piece icon
|
||||
- Shows all features grouped by 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 via `TierFeatureLimitEntry[]`
|
||||
|
||||
### Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/templates/admin/subscription-tiers.html` | Page template |
|
||||
| `static/admin/js/subscription-tiers.js` | Alpine.js component |
|
||||
| `app/routes/admin_pages.py` | Route registration |
|
||||
|
||||
### API Endpoints Used
|
||||
|
||||
| Action | Method | Endpoint |
|
||||
|--------|--------|----------|
|
||||
| Load tiers | GET | `/api/v1/admin/subscriptions/tiers` |
|
||||
| Load stats | GET | `/api/v1/admin/subscriptions/stats` |
|
||||
| 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 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
|
||||
|
||||
The features are seeded via Alembic migration:
|
||||
|
||||
```
|
||||
alembic/versions/n2c3d4e5f6a7_add_features_table.py
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `features` table with 30 default features
|
||||
- `store_feature_overrides` table for per-store exceptions
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests located in:
|
||||
- `tests/unit/services/test_feature_service.py`
|
||||
- `tests/unit/services/test_usage_service.py`
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
pytest tests/unit/services/test_feature_service.py -v
|
||||
pytest tests/unit/services/test_usage_service.py -v
|
||||
```
|
||||
|
||||
## Architecture Compliance
|
||||
|
||||
All JavaScript files follow architecture rules:
|
||||
- JS-003: Alpine components use `store*` naming convention
|
||||
- JS-005: Init guards prevent duplicate initialization
|
||||
- JS-006: Async operations have try/catch error handling
|
||||
- JS-008: API calls use `apiClient` (not raw `fetch()`)
|
||||
- JS-009: Notifications use `Utils.showToast()`
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Subscription Billing](subscription-system.md) - Core subscription system
|
||||
- [Subscription Workflow Plan](subscription-workflow.md) - Implementation roadmap
|
||||
74
app/modules/billing/docs/index.md
Normal file
74
app/modules/billing/docs/index.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Billing & Subscriptions
|
||||
|
||||
Core subscription management, tier limits, store billing, and invoice history. Provides tier-based feature gating used throughout the platform. Uses the payments module for actual payment processing.
|
||||
|
||||
## Overview
|
||||
|
||||
| Aspect | Detail |
|
||||
|--------|--------|
|
||||
| Code | `billing` |
|
||||
| Classification | Core |
|
||||
| Dependencies | `payments` |
|
||||
| Status | Active |
|
||||
|
||||
## Features
|
||||
|
||||
- `subscription_management` — Subscription lifecycle management
|
||||
- `billing_history` — Billing and payment history
|
||||
- `invoice_generation` — Automatic invoice generation
|
||||
- `subscription_analytics` — Subscription metrics and analytics
|
||||
- `trial_management` — Free trial period management
|
||||
- `limit_overrides` — Per-store tier limit overrides
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `billing.view_tiers` | View subscription tiers |
|
||||
| `billing.manage_tiers` | Manage subscription tiers |
|
||||
| `billing.view_subscriptions` | View subscriptions |
|
||||
| `billing.manage_subscriptions` | Manage subscriptions |
|
||||
| `billing.view_invoices` | View invoices |
|
||||
|
||||
## Data Model
|
||||
|
||||
See [Data Model](data-model.md) for full entity relationships.
|
||||
|
||||
- **SubscriptionTier** — Tier definitions with Stripe price IDs
|
||||
- **TierFeatureLimit** — Per-tier feature limits (feature_code + limit_value)
|
||||
- **MerchantSubscription** — Per-merchant+platform subscription state
|
||||
- **MerchantFeatureOverride** — Per-merchant feature limit overrides
|
||||
- **AddOnProduct / StoreAddOn** — Purchasable add-ons
|
||||
- **BillingHistory** — Invoice and payment records
|
||||
- **StripeWebhookEvent** — Webhook idempotency tracking
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `*` | `/api/v1/admin/billing/*` | Admin billing management |
|
||||
| `*` | `/api/v1/admin/features/*` | Feature/tier management |
|
||||
| `*` | `/api/v1/merchant/billing/*` | Merchant billing endpoints |
|
||||
| `*` | `/api/v1/platform/billing/*` | Platform-wide billing stats |
|
||||
|
||||
## Scheduled Tasks
|
||||
|
||||
| Task | Schedule | Description |
|
||||
|------|----------|-------------|
|
||||
| `billing.reset_period_counters` | Daily 00:05 | Reset period-based usage counters |
|
||||
| `billing.check_trial_expirations` | Daily 01:00 | Check and handle expired trials |
|
||||
| `billing.sync_stripe_status` | Hourly :30 | Sync subscription status with Stripe |
|
||||
| `billing.cleanup_stale_subscriptions` | Weekly Sunday 03:00 | Clean up stale subscription records |
|
||||
|
||||
## Configuration
|
||||
|
||||
Configured via Stripe environment variables and tier definitions in the admin panel.
|
||||
|
||||
## Additional Documentation
|
||||
|
||||
- [Data Model](data-model.md) — Entity relationships and database schema
|
||||
- [Subscription System](subscription-system.md) — Architecture, feature providers, API reference
|
||||
- [Feature Gating](feature-gating.md) — Tier-based feature access control and UI integration
|
||||
- [Tier Management](tier-management.md) — Admin guide for managing subscription tiers
|
||||
- [Subscription Workflow](subscription-workflow.md) — Subscription lifecycle and implementation phases
|
||||
- [Stripe Integration](stripe-integration.md) — Stripe Connect setup, webhooks, payment flow
|
||||
617
app/modules/billing/docs/stripe-integration.md
Normal file
617
app/modules/billing/docs/stripe-integration.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# Stripe Payment Integration - Multi-Tenant Ecommerce Platform
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The payment integration uses **Stripe Connect** to handle multi-store payments, enabling:
|
||||
- Each store to receive payments directly
|
||||
- Platform to collect fees/commissions
|
||||
- Proper financial isolation between stores
|
||||
- Compliance with financial regulations
|
||||
|
||||
## Payment Models
|
||||
|
||||
### Database Models
|
||||
|
||||
```python
|
||||
# models/database/payment.py
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
from .base import TimestampMixin
|
||||
|
||||
|
||||
class StorePaymentConfig(Base, TimestampMixin):
|
||||
"""Store-specific payment configuration."""
|
||||
__tablename__ = "store_payment_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False, unique=True)
|
||||
|
||||
# Stripe Connect configuration
|
||||
stripe_account_id = Column(String(255)) # Stripe Connect account ID
|
||||
stripe_account_status = Column(String(50)) # pending, active, restricted, inactive
|
||||
stripe_onboarding_url = Column(Text) # Onboarding link for store
|
||||
stripe_dashboard_url = Column(Text) # Store's Stripe dashboard
|
||||
|
||||
# Payment settings
|
||||
accepts_payments = Column(Boolean, default=False)
|
||||
currency = Column(String(3), default="EUR")
|
||||
platform_fee_percentage = Column(Numeric(5, 2), default=2.5) # Platform commission
|
||||
|
||||
# Payout settings
|
||||
payout_schedule = Column(String(20), default="weekly") # daily, weekly, monthly
|
||||
minimum_payout = Column(Numeric(10, 2), default=20.00)
|
||||
|
||||
# Relationships
|
||||
store = relationship("Store", back_populates="payment_config")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StorePaymentConfig(store_id={self.store_id}, stripe_account_id='{self.stripe_account_id}')>"
|
||||
|
||||
|
||||
class Payment(Base, TimestampMixin):
|
||||
"""Payment records for orders."""
|
||||
__tablename__ = "payments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
||||
|
||||
# Stripe payment details
|
||||
stripe_payment_intent_id = Column(String(255), unique=True, index=True)
|
||||
stripe_charge_id = Column(String(255), index=True)
|
||||
stripe_transfer_id = Column(String(255)) # Transfer to store account
|
||||
|
||||
# Payment amounts (in cents to avoid floating point issues)
|
||||
amount_total = Column(Integer, nullable=False) # Total customer payment
|
||||
amount_store = Column(Integer, nullable=False) # Amount to store
|
||||
amount_platform_fee = Column(Integer, nullable=False) # Platform commission
|
||||
currency = Column(String(3), default="EUR")
|
||||
|
||||
# Payment status
|
||||
status = Column(String(50), nullable=False) # pending, succeeded, failed, refunded
|
||||
payment_method = Column(String(50)) # card, bank_transfer, etc.
|
||||
|
||||
# Metadata
|
||||
stripe_metadata = Column(Text) # JSON string of Stripe metadata
|
||||
failure_reason = Column(Text)
|
||||
refund_reason = Column(Text)
|
||||
|
||||
# Timestamps
|
||||
paid_at = Column(DateTime)
|
||||
refunded_at = Column(DateTime)
|
||||
|
||||
# Relationships
|
||||
store = relationship("Store")
|
||||
order = relationship("Order", back_populates="payment")
|
||||
customer = relationship("Customer")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Payment(id={self.id}, order_id={self.order_id}, status='{self.status}')>"
|
||||
|
||||
@property
|
||||
def amount_total_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
return self.amount_total / 100
|
||||
|
||||
@property
|
||||
def amount_store_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
return self.amount_store / 100
|
||||
|
||||
|
||||
class PaymentMethod(Base, TimestampMixin):
|
||||
"""Saved customer payment methods."""
|
||||
__tablename__ = "payment_methods"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
||||
|
||||
# Stripe payment method details
|
||||
stripe_payment_method_id = Column(String(255), nullable=False, index=True)
|
||||
payment_method_type = Column(String(50), nullable=False) # card, sepa_debit, etc.
|
||||
|
||||
# Card details (if applicable)
|
||||
card_brand = Column(String(50)) # visa, mastercard, etc.
|
||||
card_last4 = Column(String(4))
|
||||
card_exp_month = Column(Integer)
|
||||
card_exp_year = Column(Integer)
|
||||
|
||||
# Settings
|
||||
is_default = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
store = relationship("Store")
|
||||
customer = relationship("Customer")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PaymentMethod(id={self.id}, customer_id={self.customer_id}, type='{self.payment_method_type}')>"
|
||||
```
|
||||
|
||||
### Updated Order Model
|
||||
|
||||
```python
|
||||
# Update models/database/order.py
|
||||
class Order(Base, TimestampMixin):
|
||||
# ... existing fields ...
|
||||
|
||||
# Payment integration
|
||||
payment_status = Column(String(50), default="pending") # pending, paid, failed, refunded
|
||||
payment_intent_id = Column(String(255)) # Stripe PaymentIntent ID
|
||||
total_amount_cents = Column(Integer, nullable=False) # Amount in cents
|
||||
|
||||
# Relationships
|
||||
payment = relationship("Payment", back_populates="order", uselist=False)
|
||||
|
||||
@property
|
||||
def total_amount_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
return self.total_amount_cents / 100 if self.total_amount_cents else 0
|
||||
```
|
||||
|
||||
## Payment Service Integration
|
||||
|
||||
### Stripe Service
|
||||
|
||||
```python
|
||||
# services/payment_service.py
|
||||
import stripe
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from models.database.payment import Payment, StorePaymentConfig
|
||||
from models.database.order import Order
|
||||
from models.database.store import Store
|
||||
from app.exceptions.payment import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configure Stripe
|
||||
stripe.api_key = settings.stripe_secret_key
|
||||
|
||||
|
||||
class PaymentService:
|
||||
"""Service for handling Stripe payments in multi-tenant environment."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def create_payment_intent(
|
||||
self,
|
||||
store_id: int,
|
||||
order_id: int,
|
||||
amount_euros: Decimal,
|
||||
customer_email: str,
|
||||
metadata: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""Create Stripe PaymentIntent for store order."""
|
||||
|
||||
# Get store payment configuration
|
||||
payment_config = self.get_store_payment_config(store_id)
|
||||
if not payment_config.accepts_payments:
|
||||
raise PaymentNotConfiguredException(f"Store {store_id} not configured for payments")
|
||||
|
||||
# Calculate amounts
|
||||
amount_cents = int(amount_euros * 100)
|
||||
platform_fee_cents = int(amount_cents * (payment_config.platform_fee_percentage / 100))
|
||||
store_amount_cents = amount_cents - platform_fee_cents
|
||||
|
||||
try:
|
||||
# Create PaymentIntent with Stripe Connect
|
||||
payment_intent = stripe.PaymentIntent.create(
|
||||
amount=amount_cents,
|
||||
currency=payment_config.currency.lower(),
|
||||
application_fee_amount=platform_fee_cents,
|
||||
transfer_data={
|
||||
'destination': payment_config.stripe_account_id,
|
||||
},
|
||||
metadata={
|
||||
'store_id': str(store_id),
|
||||
'order_id': str(order_id),
|
||||
'platform': 'multi_tenant_ecommerce',
|
||||
**(metadata or {})
|
||||
},
|
||||
receipt_email=customer_email,
|
||||
description=f"Order payment for store {store_id}"
|
||||
)
|
||||
|
||||
# Create payment record
|
||||
payment = Payment(
|
||||
store_id=store_id,
|
||||
order_id=order_id,
|
||||
customer_id=self.get_order_customer_id(order_id),
|
||||
stripe_payment_intent_id=payment_intent.id,
|
||||
amount_total=amount_cents,
|
||||
amount_store=store_amount_cents,
|
||||
amount_platform_fee=platform_fee_cents,
|
||||
currency=payment_config.currency,
|
||||
status='pending',
|
||||
stripe_metadata=json.dumps(payment_intent.metadata)
|
||||
)
|
||||
|
||||
self.db.add(payment)
|
||||
|
||||
# Update order
|
||||
order = self.db.query(Order).filter(Order.id == order_id).first()
|
||||
if order:
|
||||
order.payment_intent_id = payment_intent.id
|
||||
order.payment_status = 'pending'
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return {
|
||||
'payment_intent_id': payment_intent.id,
|
||||
'client_secret': payment_intent.client_secret,
|
||||
'amount_total': amount_euros,
|
||||
'amount_store': store_amount_cents / 100,
|
||||
'platform_fee': platform_fee_cents / 100,
|
||||
'currency': payment_config.currency
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating PaymentIntent: {e}")
|
||||
raise PaymentProcessingException(f"Payment processing failed: {str(e)}")
|
||||
|
||||
def confirm_payment(self, payment_intent_id: str) -> Payment:
|
||||
"""Confirm payment and update records."""
|
||||
|
||||
try:
|
||||
# Retrieve PaymentIntent from Stripe
|
||||
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
|
||||
|
||||
# Find payment record
|
||||
payment = self.db.query(Payment).filter(
|
||||
Payment.stripe_payment_intent_id == payment_intent_id
|
||||
).first()
|
||||
|
||||
if not payment:
|
||||
raise PaymentNotFoundException(f"Payment not found for intent {payment_intent_id}")
|
||||
|
||||
# Update payment status based on Stripe status
|
||||
if payment_intent.status == 'succeeded':
|
||||
payment.status = 'succeeded'
|
||||
payment.stripe_charge_id = payment_intent.charges.data[0].id if payment_intent.charges.data else None
|
||||
payment.paid_at = datetime.utcnow()
|
||||
|
||||
# Update order status
|
||||
order = self.db.query(Order).filter(Order.id == payment.order_id).first()
|
||||
if order:
|
||||
order.payment_status = 'paid'
|
||||
order.status = 'processing' # Move order to processing
|
||||
|
||||
elif payment_intent.status == 'payment_failed':
|
||||
payment.status = 'failed'
|
||||
payment.failure_reason = payment_intent.last_payment_error.message if payment_intent.last_payment_error else "Unknown error"
|
||||
|
||||
# Update order status
|
||||
order = self.db.query(Order).filter(Order.id == payment.order_id).first()
|
||||
if order:
|
||||
order.payment_status = 'failed'
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return payment
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error confirming payment: {e}")
|
||||
raise PaymentProcessingException(f"Payment confirmation failed: {str(e)}")
|
||||
|
||||
def create_store_stripe_account(self, store_id: int, store_data: Dict) -> str:
|
||||
"""Create Stripe Connect account for store."""
|
||||
|
||||
try:
|
||||
# Create Stripe Connect Express account
|
||||
account = stripe.Account.create(
|
||||
type='express',
|
||||
country='LU', # Luxembourg
|
||||
email=store_data.get('business_email'),
|
||||
capabilities={
|
||||
'card_payments': {'requested': True},
|
||||
'transfers': {'requested': True},
|
||||
},
|
||||
business_type='merchant',
|
||||
merchant={
|
||||
'name': store_data.get('business_name'),
|
||||
'phone': store_data.get('business_phone'),
|
||||
'address': {
|
||||
'line1': store_data.get('address_line1'),
|
||||
'city': store_data.get('city'),
|
||||
'postal_code': store_data.get('postal_code'),
|
||||
'country': 'LU'
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
'store_id': str(store_id),
|
||||
'platform': 'multi_tenant_ecommerce'
|
||||
}
|
||||
)
|
||||
|
||||
# Update or create payment configuration
|
||||
payment_config = self.get_or_create_store_payment_config(store_id)
|
||||
payment_config.stripe_account_id = account.id
|
||||
payment_config.stripe_account_status = account.charges_enabled and account.payouts_enabled and 'active' or 'pending'
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return account.id
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating account: {e}")
|
||||
raise PaymentConfigurationException(f"Failed to create payment account: {str(e)}")
|
||||
|
||||
def create_onboarding_link(self, store_id: int) -> str:
|
||||
"""Create Stripe onboarding link for store."""
|
||||
|
||||
payment_config = self.get_store_payment_config(store_id)
|
||||
if not payment_config.stripe_account_id:
|
||||
raise PaymentNotConfiguredException("Store does not have Stripe account")
|
||||
|
||||
try:
|
||||
account_link = stripe.AccountLink.create(
|
||||
account=payment_config.stripe_account_id,
|
||||
refresh_url=f"{settings.frontend_url}/store/admin/payments/refresh",
|
||||
return_url=f"{settings.frontend_url}/store/admin/payments/success",
|
||||
type='account_onboarding',
|
||||
)
|
||||
|
||||
# Update onboarding URL
|
||||
payment_config.stripe_onboarding_url = account_link.url
|
||||
self.db.commit()
|
||||
|
||||
return account_link.url
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating onboarding link: {e}")
|
||||
raise PaymentConfigurationException(f"Failed to create onboarding link: {str(e)}")
|
||||
|
||||
def get_store_payment_config(self, store_id: int) -> StorePaymentConfig:
|
||||
"""Get store payment configuration."""
|
||||
config = self.db.query(StorePaymentConfig).filter(
|
||||
StorePaymentConfig.store_id == store_id
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
raise PaymentNotConfiguredException(f"No payment configuration for store {store_id}")
|
||||
|
||||
return config
|
||||
|
||||
def webhook_handler(self, event_type: str, event_data: Dict) -> None:
|
||||
"""Handle Stripe webhook events."""
|
||||
|
||||
if event_type == 'payment_intent.succeeded':
|
||||
payment_intent_id = event_data['object']['id']
|
||||
self.confirm_payment(payment_intent_id)
|
||||
|
||||
elif event_type == 'payment_intent.payment_failed':
|
||||
payment_intent_id = event_data['object']['id']
|
||||
self.confirm_payment(payment_intent_id)
|
||||
|
||||
elif event_type == 'account.updated':
|
||||
# Update store account status
|
||||
account_id = event_data['object']['id']
|
||||
self.update_store_account_status(account_id, event_data['object'])
|
||||
|
||||
# Add more webhook handlers as needed
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Payment APIs
|
||||
|
||||
```python
|
||||
# app/api/v1/store/payments.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from middleware.store_context import require_store_context
|
||||
from models.database.store import Store
|
||||
from services.payment_service import PaymentService
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["store-payments"])
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def get_payment_config(
|
||||
store: Store = Depends(require_store_context()),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get store payment configuration."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
try:
|
||||
config = payment_service.get_store_payment_config(store.id)
|
||||
return {
|
||||
"stripe_account_id": config.stripe_account_id,
|
||||
"account_status": config.stripe_account_status,
|
||||
"accepts_payments": config.accepts_payments,
|
||||
"currency": config.currency,
|
||||
"platform_fee_percentage": float(config.platform_fee_percentage),
|
||||
"needs_onboarding": config.stripe_account_status != 'active'
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"stripe_account_id": None,
|
||||
"account_status": "not_configured",
|
||||
"accepts_payments": False,
|
||||
"needs_setup": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/setup")
|
||||
async def setup_payments(
|
||||
setup_data: dict,
|
||||
store: Store = Depends(require_store_context()),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Set up Stripe payments for store."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
store_data = {
|
||||
"business_name": store.name,
|
||||
"business_email": store.business_email,
|
||||
"business_phone": store.business_phone,
|
||||
**setup_data
|
||||
}
|
||||
|
||||
account_id = payment_service.create_store_stripe_account(store.id, store_data)
|
||||
onboarding_url = payment_service.create_onboarding_link(store.id)
|
||||
|
||||
return {
|
||||
"stripe_account_id": account_id,
|
||||
"onboarding_url": onboarding_url,
|
||||
"message": "Payment setup initiated. Complete onboarding to accept payments."
|
||||
}
|
||||
|
||||
|
||||
# app/api/v1/platform/stores/payments.py
|
||||
@router.post("/{store_id}/payments/create-intent")
|
||||
async def create_payment_intent(
|
||||
store_id: int,
|
||||
payment_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create payment intent for customer order."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
payment_intent = payment_service.create_payment_intent(
|
||||
store_id=store_id,
|
||||
order_id=payment_data['order_id'],
|
||||
amount_euros=Decimal(str(payment_data['amount'])),
|
||||
customer_email=payment_data['customer_email'],
|
||||
metadata=payment_data.get('metadata', {})
|
||||
)
|
||||
|
||||
return payment_intent
|
||||
|
||||
|
||||
@router.post("/webhooks/stripe")
|
||||
async def stripe_webhook(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle Stripe webhook events."""
|
||||
import stripe
|
||||
|
||||
payload = await request.body()
|
||||
sig_header = request.headers.get('stripe-signature')
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, settings.stripe_webhook_secret
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid payload")
|
||||
except stripe.error.SignatureVerificationError:
|
||||
raise HTTPException(status_code=400, detail="Invalid signature")
|
||||
|
||||
payment_service = PaymentService(db)
|
||||
payment_service.webhook_handler(event['type'], event['data'])
|
||||
|
||||
return {"status": "success"}
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Checkout Process
|
||||
|
||||
```javascript
|
||||
// frontend/js/storefront/checkout.js
|
||||
class CheckoutManager {
|
||||
constructor(storeId) {
|
||||
this.storeId = storeId;
|
||||
this.stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
|
||||
this.elements = this.stripe.elements();
|
||||
this.paymentElement = null;
|
||||
}
|
||||
|
||||
async initializePayment(orderData) {
|
||||
// Create payment intent
|
||||
const response = await fetch(`/api/v1/platform/stores/${this.storeId}/payments/create-intent`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
order_id: orderData.orderId,
|
||||
amount: orderData.total,
|
||||
customer_email: orderData.customerEmail
|
||||
})
|
||||
});
|
||||
|
||||
const { client_secret, amount_total, platform_fee } = await response.json();
|
||||
|
||||
// Display payment breakdown
|
||||
this.displayPaymentBreakdown(amount_total, platform_fee);
|
||||
|
||||
// Create payment element
|
||||
this.paymentElement = this.elements.create('payment', {
|
||||
clientSecret: client_secret
|
||||
});
|
||||
|
||||
this.paymentElement.mount('#payment-element');
|
||||
}
|
||||
|
||||
async confirmPayment(orderData) {
|
||||
const { error } = await this.stripe.confirmPayment({
|
||||
elements: this.elements,
|
||||
confirmParams: {
|
||||
return_url: `${window.location.origin}/storefront/order-confirmation`,
|
||||
receipt_email: orderData.customerEmail
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
this.showPaymentError(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Updated Workflow Integration
|
||||
|
||||
### Enhanced Customer Purchase Workflow
|
||||
|
||||
```
|
||||
Customer adds products to cart
|
||||
↓
|
||||
Customer proceeds to checkout
|
||||
↓
|
||||
System creates Order (payment_status: pending)
|
||||
↓
|
||||
Frontend calls POST /api/v1/platform/stores/{store_id}/payments/create-intent
|
||||
↓
|
||||
PaymentService creates Stripe PaymentIntent with store destination
|
||||
↓
|
||||
Customer completes payment with Stripe Elements
|
||||
↓
|
||||
Stripe webhook confirms payment
|
||||
↓
|
||||
PaymentService updates Order (payment_status: paid, status: processing)
|
||||
↓
|
||||
Store receives order for fulfillment
|
||||
```
|
||||
|
||||
### Payment Configuration Workflow
|
||||
|
||||
```
|
||||
Store accesses payment settings
|
||||
↓
|
||||
POST /api/v1/store/payments/setup
|
||||
↓
|
||||
System creates Stripe Connect account
|
||||
↓
|
||||
Store completes Stripe onboarding
|
||||
↓
|
||||
Webhook updates account status to 'active'
|
||||
↓
|
||||
Store can now accept payments
|
||||
```
|
||||
|
||||
This integration provides secure, compliant payment processing while maintaining store isolation and enabling proper revenue distribution between stores and the platform.
|
||||
182
app/modules/billing/docs/subscription-system.md
Normal file
182
app/modules/billing/docs/subscription-system.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Subscription & Billing System
|
||||
|
||||
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 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
|
||||
- **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 │
|
||||
└────────────────┘ └────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
### Feature 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
|
||||
|
||||
```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 | Purpose |
|
||||
|---------|---------|
|
||||
| `FeatureAggregatorService` | Aggregates usage from module providers, resolves tier limits + overrides |
|
||||
| `BillingService` | Subscription operations, checkout, portal |
|
||||
| `SubscriptionService` | Subscription CRUD, tier lookups |
|
||||
| `AdminSubscriptionService` | Admin subscription management |
|
||||
| `StripeService` | Core Stripe API operations |
|
||||
| `CapacityForecastService` | Growth trends, projections |
|
||||
|
||||
### Background Tasks
|
||||
|
||||
| Task | Schedule | Purpose |
|
||||
|------|----------|---------|
|
||||
| `reset_period_counters` | Daily | Reset order counters at period end |
|
||||
| `check_trial_expirations` | Daily | Expire trials without payment method |
|
||||
| `sync_stripe_status` | Hourly | Sync status with Stripe |
|
||||
| `cleanup_stale_subscriptions` | Weekly | Clean up old cancelled subscriptions |
|
||||
| `capture_capacity_snapshot` | Daily | Capture capacity metrics snapshot |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Store Billing API (`/api/v1/store/billing`)
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/subscription` | GET | Current subscription status |
|
||||
| `/tiers` | GET | Available tiers for upgrade |
|
||||
| `/usage` | GET | Dynamic usage metrics (from feature providers) |
|
||||
| `/checkout` | POST | Create Stripe checkout session |
|
||||
| `/portal` | POST | Create Stripe customer portal session |
|
||||
| `/invoices` | GET | Invoice history |
|
||||
| `/change-tier` | POST | Upgrade/downgrade tier |
|
||||
| `/addons` | GET | Available add-on products |
|
||||
| `/my-addons` | GET | Store's purchased add-ons |
|
||||
| `/addons/purchase` | POST | Purchase an add-on |
|
||||
| `/cancel` | POST | Cancel subscription |
|
||||
| `/reactivate` | POST | Reactivate cancelled subscription |
|
||||
|
||||
### Admin Subscription API (`/api/v1/admin/subscriptions`)
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/tiers` | GET/POST | List/create tiers |
|
||||
| `/tiers/{code}` | PATCH/DELETE | Update/delete tier |
|
||||
| `/stats` | GET | Subscription statistics |
|
||||
| `/merchants/{id}/platforms/{pid}` | GET/PUT | Get/update merchant subscription |
|
||||
| `/store/{store_id}` | GET | Convenience: subscription + usage for a store |
|
||||
|
||||
### Admin Feature Management API (`/api/v1/admin/subscriptions/features`)
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/catalog` | GET | Feature catalog grouped by category |
|
||||
| `/tiers/{code}/limits` | GET/PUT | Get/upsert feature limits for a tier |
|
||||
| `/merchants/{id}/overrides` | GET/PUT | Get/upsert merchant feature overrides |
|
||||
|
||||
## Subscription Tiers
|
||||
|
||||
Tiers are stored in `subscription_tiers` with feature limits in `tier_feature_limits`:
|
||||
|
||||
```
|
||||
SubscriptionTier (essential)
|
||||
├── TierFeatureLimit: max_products = 200
|
||||
├── TierFeatureLimit: max_orders_per_month = 100
|
||||
├── TierFeatureLimit: max_team_members = 1
|
||||
└── TierFeatureLimit: basic_analytics (binary, enabled)
|
||||
|
||||
SubscriptionTier (professional)
|
||||
├── TierFeatureLimit: max_products = NULL (unlimited)
|
||||
├── TierFeatureLimit: max_orders_per_month = 500
|
||||
├── TierFeatureLimit: max_team_members = 3
|
||||
└── TierFeatureLimit: analytics_dashboard (binary, enabled)
|
||||
```
|
||||
|
||||
## 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 |
|
||||
|
||||
## Exception Handling
|
||||
|
||||
| Exception | HTTP | 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 |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Data Model](data-model.md) — Entity relationships
|
||||
- [Feature Gating](feature-gating.md) — Feature access control and UI integration
|
||||
- [Stripe Integration](stripe-integration.md) — Payment setup
|
||||
- [Tier Management](tier-management.md) — Admin guide for tier management
|
||||
- [Subscription Workflow](subscription-workflow.md) — Subscription lifecycle
|
||||
- [Metrics Provider Pattern](../../architecture/metrics-provider-pattern.md) — Protocol-based metrics
|
||||
- [Capacity Monitoring](../../operations/capacity-monitoring.md) — Monitoring guide
|
||||
- [Capacity Planning](../../architecture/capacity-planning.md) — Infrastructure sizing
|
||||
454
app/modules/billing/docs/subscription-workflow.md
Normal file
454
app/modules/billing/docs/subscription-workflow.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# Subscription Workflow Plan
|
||||
|
||||
## Overview
|
||||
|
||||
End-to-end subscription management workflow for stores on the platform.
|
||||
|
||||
---
|
||||
|
||||
## 1. Store Subscribes to a Tier
|
||||
|
||||
### 1.1 New Store Registration Flow
|
||||
|
||||
```
|
||||
Store Registration → Select Tier → Trial Period → Payment Setup → Active Subscription
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
1. Store creates account (existing flow)
|
||||
2. During onboarding, store selects a tier:
|
||||
- Show tier comparison cards (Essential, Professional, Business, Enterprise)
|
||||
- Highlight features and limits for each tier
|
||||
- Default to 14-day trial on selected tier
|
||||
3. Create `StoreSubscription` record with:
|
||||
- `tier` = selected tier code
|
||||
- `status` = "trial"
|
||||
- `trial_ends_at` = now + 14 days
|
||||
- `period_start` / `period_end` set for trial period
|
||||
4. Before trial ends, prompt store to add payment method
|
||||
5. On payment method added → Create Stripe subscription → Status becomes "active"
|
||||
|
||||
### 1.2 Database Changes Required
|
||||
|
||||
**Add FK relationship to `subscription_tiers`:**
|
||||
```python
|
||||
# StoreSubscription - Add proper FK
|
||||
tier_id = Column(Integer, ForeignKey("subscription_tiers.id"), nullable=True)
|
||||
tier_code = Column(String(20), nullable=False) # Keep for backwards compat
|
||||
|
||||
# Relationship
|
||||
tier_obj = relationship("SubscriptionTier", backref="subscriptions")
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
1. Add `tier_id` column (nullable initially)
|
||||
2. Populate `tier_id` from existing `tier` code values
|
||||
3. Add FK constraint
|
||||
|
||||
### 1.3 API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/v1/store/subscription/tiers` | GET | List available tiers for selection |
|
||||
| `/api/v1/store/subscription/select-tier` | POST | Select tier during onboarding |
|
||||
| `/api/v1/store/subscription/setup-payment` | POST | Create Stripe checkout for payment |
|
||||
|
||||
---
|
||||
|
||||
## 2. Admin Views Subscription on Store Page
|
||||
|
||||
### 2.1 Store Detail Page Enhancement
|
||||
|
||||
**Location:** `/admin/stores/{store_id}`
|
||||
|
||||
**New Subscription Card:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Subscription [Edit] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Tier: Professional Status: Active │
|
||||
│ Price: €99/month Since: Jan 15, 2025 │
|
||||
│ Next Billing: Feb 15, 2025 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Usage This Period │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Orders │ │ Products │ │ Team Members │ │
|
||||
│ │ 234 / 500 │ │ 156 / ∞ │ │ 2 / 3 │ │
|
||||
│ │ ████████░░ │ │ ████████████ │ │ ██████░░░░ │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Add-ons: Custom Domain (mydomain.com), 5 Email Addresses │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Files to Modify
|
||||
|
||||
- `app/templates/admin/store-detail.html` - Add subscription card
|
||||
- `static/admin/js/store-detail.js` - Load subscription data
|
||||
- `app/api/v1/admin/stores.py` - Include subscription in store response
|
||||
|
||||
### 2.3 Admin Quick Actions
|
||||
|
||||
From the store page, admin can:
|
||||
- **Change Tier** - Upgrade/downgrade store
|
||||
- **Override Limits** - Set custom limits (enterprise deals)
|
||||
- **Extend Trial** - Give more trial days
|
||||
- **Cancel Subscription** - With reason
|
||||
- **Manage Add-ons** - Add/remove add-ons
|
||||
|
||||
---
|
||||
|
||||
## 3. Tier Upgrade/Downgrade
|
||||
|
||||
### 3.1 Admin-Initiated Change
|
||||
|
||||
**Location:** Admin store page → Subscription card → [Edit] button
|
||||
|
||||
**Modal: Change Subscription Tier**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Change Subscription Tier [X] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Current: Professional (€99/month) │
|
||||
│ │
|
||||
│ New Tier: │
|
||||
│ ○ Essential (€49/month) - Downgrade │
|
||||
│ ● Business (€199/month) - Upgrade │
|
||||
│ ○ Enterprise (Custom) - Contact required │
|
||||
│ │
|
||||
│ When to apply: │
|
||||
│ ○ Immediately (prorate current period) │
|
||||
│ ● At next billing cycle (Feb 15, 2025) │
|
||||
│ │
|
||||
│ [ ] Notify store by email │
|
||||
│ │
|
||||
│ [Cancel] [Apply Change] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Store-Initiated Change
|
||||
|
||||
**Location:** Store dashboard → Billing page → [Change Plan]
|
||||
|
||||
**Flow:**
|
||||
1. Store clicks "Change Plan" on billing page
|
||||
2. Shows tier comparison with current tier highlighted
|
||||
3. Store selects new tier
|
||||
4. For upgrades:
|
||||
- Show prorated amount for immediate change
|
||||
- Or option to change at next billing
|
||||
- Redirect to Stripe checkout if needed
|
||||
5. For downgrades:
|
||||
- Always schedule for next billing cycle
|
||||
- Show what features they'll lose
|
||||
- Confirmation required
|
||||
|
||||
### 3.3 API Endpoints
|
||||
|
||||
| Endpoint | Method | Actor | Description |
|
||||
|----------|--------|-------|-------------|
|
||||
| `/api/v1/admin/subscriptions/{store_id}/change-tier` | POST | Admin | Change store's tier |
|
||||
| `/api/v1/store/billing/change-tier` | POST | Store | Request tier change |
|
||||
| `/api/v1/store/billing/preview-change` | POST | Store | Preview proration |
|
||||
|
||||
### 3.4 Stripe Integration
|
||||
|
||||
**Upgrade (Immediate):**
|
||||
```python
|
||||
stripe.Subscription.modify(
|
||||
subscription_id,
|
||||
items=[{"price": new_price_id}],
|
||||
proration_behavior="create_prorations"
|
||||
)
|
||||
```
|
||||
|
||||
**Downgrade (Scheduled):**
|
||||
```python
|
||||
stripe.Subscription.modify(
|
||||
subscription_id,
|
||||
items=[{"price": new_price_id}],
|
||||
proration_behavior="none",
|
||||
billing_cycle_anchor="unchanged"
|
||||
)
|
||||
# Store scheduled change in our DB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Add-ons Upselling
|
||||
|
||||
### 4.1 Where Add-ons Are Displayed
|
||||
|
||||
#### A. Store Billing Page
|
||||
```
|
||||
/store/{code}/billing
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Available Add-ons │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ 🌐 Custom Domain │ │ 📧 Email Package │ │
|
||||
│ │ €15/year │ │ From €5/month │ │
|
||||
│ │ Use your own domain │ │ 5, 10, or 25 emails │ │
|
||||
│ │ [Add to Plan] │ │ [Add to Plan] │ │
|
||||
│ └─────────────────────┘ └─────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ 🔒 Premium SSL │ │ 💾 Extra Storage │ │
|
||||
│ │ €49/year │ │ €5/month per 10GB │ │
|
||||
│ │ EV certificate │ │ More product images │ │
|
||||
│ │ [Add to Plan] │ │ [Add to Plan] │ │
|
||||
│ └─────────────────────┘ └─────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### B. Contextual Upsells
|
||||
|
||||
**When store hits a limit:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ ⚠️ You've reached your order limit for this month │
|
||||
│ │
|
||||
│ Upgrade to Professional to get 500 orders/month │
|
||||
│ [Upgrade Now] [Dismiss] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**In settings when configuring domain:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🌐 Custom Domain │
|
||||
│ │
|
||||
│ Your shop is available at: myshop.platform.com │
|
||||
│ │
|
||||
│ Want to use your own domain like www.myshop.com? │
|
||||
│ Add the Custom Domain add-on for just €15/year │
|
||||
│ │
|
||||
│ [Add Custom Domain] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### C. Upgrade Prompts in Tier Comparison
|
||||
|
||||
When showing tier comparison, highlight what add-ons come included:
|
||||
- Professional: Includes 1 custom domain
|
||||
- Business: Includes custom domain + 5 email addresses
|
||||
- Enterprise: All add-ons included
|
||||
|
||||
### 4.2 Add-on Purchase Flow
|
||||
|
||||
```
|
||||
Store clicks [Add to Plan]
|
||||
↓
|
||||
Modal: Configure Add-on
|
||||
- Domain: Enter domain name, check availability
|
||||
- Email: Select package (5/10/25)
|
||||
↓
|
||||
Create Stripe checkout session for add-on price
|
||||
↓
|
||||
On success: Create StoreAddOn record
|
||||
↓
|
||||
Provision add-on (domain registration, email setup)
|
||||
```
|
||||
|
||||
### 4.3 Add-on Management
|
||||
|
||||
**Store can view/manage in Billing page:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Your Add-ons │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Custom Domain myshop.com €15/year [Manage] │
|
||||
│ Email Package 5 addresses €5/month [Manage] │
|
||||
│ │
|
||||
│ Next billing: Feb 15, 2025 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 Database: `store_addons` Table
|
||||
|
||||
```python
|
||||
class StoreAddOn(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"))
|
||||
addon_product_id = Column(Integer, ForeignKey("addon_products.id"))
|
||||
|
||||
# Config (e.g., domain name, email count)
|
||||
config = Column(JSON, nullable=True)
|
||||
|
||||
# Stripe
|
||||
stripe_subscription_item_id = Column(String(100))
|
||||
|
||||
# Status
|
||||
status = Column(String(20)) # active, cancelled, pending_setup
|
||||
provisioned_at = Column(DateTime)
|
||||
|
||||
# Billing
|
||||
quantity = Column(Integer, default=1)
|
||||
|
||||
created_at = Column(DateTime)
|
||||
cancelled_at = Column(DateTime, nullable=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Implementation Phases
|
||||
|
||||
**Last Updated:** December 31, 2025
|
||||
|
||||
### Phase 1: Database & Core (COMPLETED)
|
||||
- [x] Add `tier_id` FK to StoreSubscription
|
||||
- [x] Create migration with data backfill
|
||||
- [x] Update subscription service to use tier relationship
|
||||
- [x] Update admin subscription endpoints
|
||||
- [x] **NEW:** Add Feature model with 30 features across 8 categories
|
||||
- [x] **NEW:** Create FeatureService with caching for tier-based feature checking
|
||||
- [x] **NEW:** Add UsageService for limit tracking and upgrade recommendations
|
||||
|
||||
### Phase 2: Admin Store Page (PARTIALLY COMPLETE)
|
||||
- [x] Add subscription card to store detail page
|
||||
- [x] Show usage meters (orders, products, team)
|
||||
- [ ] Add "Edit Subscription" modal
|
||||
- [ ] Implement tier change API (admin)
|
||||
- [x] **NEW:** Add Admin Features page (`/admin/features`)
|
||||
- [x] **NEW:** Admin features API (list, update, toggle)
|
||||
|
||||
### Phase 3: Store Billing Page (COMPLETED)
|
||||
- [x] Create `/store/{code}/billing` page
|
||||
- [x] Show current plan and usage
|
||||
- [x] Add tier comparison/change UI
|
||||
- [x] Implement tier change API (store)
|
||||
- [x] Add Stripe checkout integration for upgrades
|
||||
- [x] **NEW:** Add feature gate macros for templates
|
||||
- [x] **NEW:** Add Alpine.js feature store
|
||||
- [x] **NEW:** Add Alpine.js upgrade prompts store
|
||||
- [x] **FIX:** Resolved 89 JS architecture violations (JS-005 through JS-009)
|
||||
|
||||
### Phase 4: Add-ons (COMPLETED)
|
||||
- [x] Seed add-on products in database
|
||||
- [x] Add "Available Add-ons" section to billing page
|
||||
- [x] Implement add-on purchase flow
|
||||
- [x] Create StoreAddOn management (via billing page)
|
||||
- [x] Add contextual upsell prompts
|
||||
- [x] **FIX:** Fix Stripe webhook to create StoreAddOn records
|
||||
|
||||
### Phase 5: Polish & Testing (IN PROGRESS)
|
||||
- [ ] Email notifications for tier changes
|
||||
- [x] Webhook handling for Stripe events
|
||||
- [x] Usage limit enforcement updates
|
||||
- [ ] End-to-end testing (manual testing required)
|
||||
- [x] Documentation (feature-gating-system.md created)
|
||||
|
||||
### Phase 6: Remaining Work (NEW)
|
||||
- [ ] Admin tier change modal (upgrade/downgrade stores)
|
||||
- [ ] Admin subscription override UI (custom limits for enterprise)
|
||||
- [ ] Trial extension from admin panel
|
||||
- [ ] Email notifications for tier changes
|
||||
- [ ] Email notifications for approaching limits
|
||||
- [ ] Grace period handling for failed payments
|
||||
- [ ] Integration tests for full billing workflow
|
||||
- [ ] Stripe test mode checkout verification
|
||||
|
||||
---
|
||||
|
||||
## 6. Files Created/Modified
|
||||
|
||||
**Last Updated:** December 31, 2025
|
||||
|
||||
### New Files (Created)
|
||||
| File | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `app/templates/store/billing.html` | Store billing page | DONE |
|
||||
| `static/store/js/billing.js` | Billing page JS | DONE |
|
||||
| `app/api/v1/store/billing.py` | Store billing endpoints | DONE |
|
||||
| `models/database/feature.py` | Feature & StoreFeatureOverride models | DONE |
|
||||
| `app/services/feature_service.py` | Feature access control service | DONE |
|
||||
| `app/services/usage_service.py` | Usage tracking & limits service | DONE |
|
||||
| `app/core/feature_gate.py` | @require_feature decorator & dependency | DONE |
|
||||
| `app/api/v1/store/features.py` | Store features API | DONE |
|
||||
| `app/api/v1/store/usage.py` | Store usage API | DONE |
|
||||
| `app/api/v1/admin/features.py` | Admin features API | DONE |
|
||||
| `app/templates/admin/features.html` | Admin features management page | DONE |
|
||||
| `app/templates/shared/macros/feature_gate.html` | Jinja2 feature gate macros | DONE |
|
||||
| `static/shared/js/feature-store.js` | Alpine.js feature store | DONE |
|
||||
| `static/shared/js/upgrade-prompts.js` | Alpine.js upgrade prompts | DONE |
|
||||
| `alembic/versions/n2c3d4e5f6a7_add_features_table.py` | Features migration | DONE |
|
||||
| `docs/implementation/feature-gating-system.md` | Feature gating documentation | DONE |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes | Status |
|
||||
|------|---------|--------|
|
||||
| `models/database/subscription.py` | Add tier_id FK | DONE |
|
||||
| `models/database/__init__.py` | Export Feature models | DONE |
|
||||
| `app/templates/admin/store-detail.html` | Add subscription card | DONE |
|
||||
| `static/admin/js/store-detail.js` | Load subscription data | DONE |
|
||||
| `app/api/v1/admin/stores.py` | Include subscription in response | DONE |
|
||||
| `app/api/v1/admin/__init__.py` | Register features router | DONE |
|
||||
| `app/api/v1/store/__init__.py` | Register features/usage routers | DONE |
|
||||
| `app/services/subscription_service.py` | Tier change logic | DONE |
|
||||
| `app/templates/store/partials/sidebar.html` | Add Billing link | DONE |
|
||||
| `app/templates/store/base.html` | Load feature/upgrade stores | DONE |
|
||||
| `app/templates/store/dashboard.html` | Add tier badge & usage bars | DONE |
|
||||
| `app/handlers/stripe_webhook.py` | Create StoreAddOn on purchase | DONE |
|
||||
| `app/routes/admin_pages.py` | Add features page route | DONE |
|
||||
| `static/shared/js/api-client.js` | Add postFormData() & getBlob() | DONE |
|
||||
|
||||
### Architecture Fixes (48 files)
|
||||
| Rule | Files Fixed | Description |
|
||||
|------|-------------|-------------|
|
||||
| JS-003 | billing.js | Rename billingData→storeBilling |
|
||||
| JS-005 | 15 files | Add init guards |
|
||||
| JS-006 | 39 files | Add try/catch to async init |
|
||||
| JS-008 | 5 files | Use apiClient not fetch |
|
||||
| JS-009 | 30 files | Use Utils.showToast |
|
||||
| TPL-009 | validate_architecture.py | Check store templates too |
|
||||
|
||||
---
|
||||
|
||||
## 7. API Summary
|
||||
|
||||
### Admin APIs
|
||||
```
|
||||
GET /admin/stores/{id} # Includes subscription
|
||||
POST /admin/subscriptions/{store_id}/change-tier
|
||||
POST /admin/subscriptions/{store_id}/override-limits
|
||||
POST /admin/subscriptions/{store_id}/extend-trial
|
||||
POST /admin/subscriptions/{store_id}/cancel
|
||||
```
|
||||
|
||||
### Store APIs
|
||||
```
|
||||
GET /store/billing/subscription # Current subscription
|
||||
GET /store/billing/tiers # Available tiers
|
||||
POST /store/billing/preview-change # Preview tier change
|
||||
POST /store/billing/change-tier # Request tier change
|
||||
POST /store/billing/checkout # Stripe checkout session
|
||||
|
||||
GET /store/billing/addons # Available add-ons
|
||||
GET /store/billing/my-addons # Store's add-ons
|
||||
POST /store/billing/addons/purchase # Purchase add-on
|
||||
DELETE /store/billing/addons/{id} # Cancel add-on
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Questions to Resolve
|
||||
|
||||
1. **Trial without payment method?**
|
||||
- Allow full trial without card, or require card upfront?
|
||||
|
||||
2. **Downgrade handling:**
|
||||
- What happens if store has more products than new tier allows?
|
||||
- Block downgrade, or just prevent new products?
|
||||
|
||||
3. **Enterprise tier:**
|
||||
- Self-service or contact sales only?
|
||||
- Custom pricing in UI or hidden?
|
||||
|
||||
4. **Add-on provisioning:**
|
||||
- Domain: Use reseller API or manual process?
|
||||
- Email: Integrate with email provider or manual?
|
||||
|
||||
5. **Grace period:**
|
||||
- How long after payment failure before suspension?
|
||||
- What gets disabled first?
|
||||
135
app/modules/billing/docs/tier-management.md
Normal file
135
app/modules/billing/docs/tier-management.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Subscription Tier Management
|
||||
|
||||
This guide explains how to manage subscription tiers and assign features to them in the admin panel.
|
||||
|
||||
## Accessing Tier Management
|
||||
|
||||
Navigate to **Admin → Billing & Subscriptions → Subscription Tiers** or go directly to `/admin/subscription-tiers`.
|
||||
|
||||
## Dashboard Overview
|
||||
|
||||
The tier management page displays:
|
||||
|
||||
### Stats Cards
|
||||
- **Total Tiers**: Number of configured subscription tiers
|
||||
- **Active Tiers**: Tiers currently available for subscription
|
||||
- **Public Tiers**: Tiers visible to stores (excludes enterprise/custom)
|
||||
- **Est. MRR**: Estimated Monthly Recurring Revenue
|
||||
|
||||
### Tier Table
|
||||
|
||||
Each tier shows:
|
||||
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| # | Display order (affects pricing page order) |
|
||||
| Code | Unique identifier (e.g., `essential`, `professional`) |
|
||||
| Name | Display name shown to stores |
|
||||
| Monthly | Monthly price in EUR |
|
||||
| Annual | Annual price in EUR (or `-` if not set) |
|
||||
| Orders/Mo | Monthly order limit (or `Unlimited`) |
|
||||
| Products | Product limit (or `Unlimited`) |
|
||||
| Team | Team member limit (or `Unlimited`) |
|
||||
| Features | Number of features assigned |
|
||||
| Status | Active, Private, or Inactive |
|
||||
| Actions | Edit Features, Edit, Activate/Deactivate |
|
||||
|
||||
## Managing Tiers
|
||||
|
||||
### Creating a New Tier
|
||||
|
||||
1. Click **Create Tier** button
|
||||
2. Fill in the tier details:
|
||||
- **Code**: Unique lowercase identifier (cannot be changed after creation)
|
||||
- **Name**: Display name for the tier
|
||||
- **Monthly Price**: Price in cents (e.g., 4900 for €49.00)
|
||||
- **Annual Price**: Optional annual price in cents
|
||||
- **Order Limit**: Leave empty for unlimited
|
||||
- **Product Limit**: Leave empty for unlimited
|
||||
- **Team Members**: Leave empty for unlimited
|
||||
- **Display Order**: Controls sort order on pricing pages
|
||||
- **Active**: Whether tier is available
|
||||
- **Public**: Whether tier is visible to stores
|
||||
3. Click **Create**
|
||||
|
||||
### Editing a Tier
|
||||
|
||||
1. Click the **pencil icon** on the tier row
|
||||
2. Modify the tier properties
|
||||
3. Click **Update**
|
||||
|
||||
Note: The tier code cannot be changed after creation.
|
||||
|
||||
### Activating/Deactivating Tiers
|
||||
|
||||
- Click the **check-circle icon** to activate an inactive tier
|
||||
- Click the **x-circle icon** to deactivate an active tier
|
||||
|
||||
Deactivating a tier:
|
||||
- Does not affect existing subscriptions
|
||||
- Hides the tier from new subscription selection
|
||||
- Can be reactivated at any time
|
||||
|
||||
## Managing Features
|
||||
|
||||
### Assigning Features to a Tier
|
||||
|
||||
1. Click the **puzzle-piece icon** on the tier row
|
||||
2. A slide-over panel opens showing all available features
|
||||
3. Features are grouped by category:
|
||||
- Analytics
|
||||
- Product Management
|
||||
- Order Management
|
||||
- Marketing
|
||||
- Support
|
||||
- Integration
|
||||
- Branding
|
||||
- Team
|
||||
|
||||
4. Check/uncheck features to include in the tier
|
||||
5. Use **Select all** or **Deselect all** per category for bulk actions
|
||||
6. The footer shows the total number of selected features
|
||||
7. Click **Save Features** to apply changes
|
||||
|
||||
### Feature Categories
|
||||
|
||||
| Category | Example Features |
|
||||
|----------|------------------|
|
||||
| Analytics | Basic Analytics, Analytics Dashboard, Custom Reports |
|
||||
| Product Management | Bulk Edit, Variants, Bundles, Inventory Alerts |
|
||||
| Order Management | Order Automation, Advanced Fulfillment, Multi-Warehouse |
|
||||
| Marketing | Discount Codes, Abandoned Cart, Email Marketing, Loyalty |
|
||||
| Support | Email Support, Priority Support, Phone Support, Dedicated Manager |
|
||||
| Integration | Basic API, Advanced API, Webhooks, Custom Integrations |
|
||||
| Branding | Theme Customization, Custom Domain, White Label |
|
||||
| Team | Team Management, Role Permissions, Audit Logs |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Tier Pricing Strategy
|
||||
|
||||
1. **Essential**: Entry-level with basic features and limits
|
||||
2. **Professional**: Mid-tier with increased limits and key integrations
|
||||
3. **Business**: Full-featured for growing businesses
|
||||
4. **Enterprise**: Custom pricing with unlimited everything
|
||||
|
||||
### Feature Assignment Tips
|
||||
|
||||
- Start with fewer features in lower tiers
|
||||
- Ensure each upgrade tier adds meaningful value
|
||||
- Keep support features as upgrade incentives
|
||||
- API access typically belongs in Business+ tiers
|
||||
|
||||
### Stripe Integration
|
||||
|
||||
For each tier, you can optionally configure:
|
||||
- **Stripe Product ID**: Link to Stripe product
|
||||
- **Stripe Monthly Price ID**: Link to monthly price
|
||||
- **Stripe Annual Price ID**: Link to annual price
|
||||
|
||||
These are required for automated billing via Stripe Checkout.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Subscription & Billing System](subscription-system.md) - Complete billing documentation
|
||||
- [Feature Gating System](feature-gating.md) - Technical feature gating details
|
||||
Reference in New Issue
Block a user