- 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>
13 KiB
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
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:
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:
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
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
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:
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):
{
"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:
// 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:
// 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
{% 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
{% from "shared/macros/feature_gate.html" import feature_locked %}
{{ feature_locked("advanced_analytics", "Advanced Analytics", "Get deeper insights") }}
Upgrade Banner
{% from "shared/macros/feature_gate.html" import upgrade_banner %}
{{ upgrade_banner("custom_domain") }}
Usage Limit Warning
{% from "shared/macros/feature_gate.html" import limit_warning %}
{{ limit_warning("orders") }} {# Shows warning when approaching limit #}
Usage Progress Bar
{% from "shared/macros/feature_gate.html" import usage_bar %}
{{ usage_bar("products", "Products") }}
Tier Badge
{% 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:
- Tier Badge: Shows current subscription tier in header
- Usage Bars: Visual progress bars for orders, products, team members
- Upgrade Prompts: Contextual upgrade recommendations when approaching limits
- 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
-
Stats Cards: Display total tiers, active tiers, public tiers, and estimated MRR
-
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)
-
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
-
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:
featurestable with 30 default featuresstore_feature_overridestable for per-store exceptions
Testing
Unit tests located in:
tests/unit/services/test_feature_service.pytests/unit/services/test_usage_service.py
Run tests:
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 rawfetch()) - JS-009: Notifications use
Utils.showToast()
Related Documentation
- Subscription Billing - Core subscription system
- Subscription Workflow Plan - Implementation roadmap