- 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>
435 lines
13 KiB
Markdown
435 lines
13 KiB
Markdown
# 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/wizamart/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](../features/subscription-billing.md) - Core subscription system
|
|
- [Subscription Workflow Plan](./subscription-workflow-plan.md) - Implementation roadmap
|