Files
orion/docs/implementation/feature-gating-system.md
Samir Boulahtit 2250054ba2 feat: consolidate media service, add merchant users page, fix metrics overlap
- Merge ImageService into MediaService with WebP variant generation,
  DB-backed storage stats, and module-driven media usage discovery
  via new MediaUsageProviderProtocol
- Add merchant users admin page with scoped user listing, stats
  endpoint, template, JS, and i18n strings (de/en/fr/lb)
- Fix merchant user metrics so Owners and Team Members are mutually
  exclusive (filter team_members on user_type="member" and exclude
  owner IDs) ensuring stat cards add up correctly
- Update billing and monitoring services to use media_service
- Update subscription-billing and feature-gating docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 21:17:11 +01:00

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:

  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:

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()