Files
orion/app/modules/billing/models/feature.py
Samir Boulahtit 0f9b80c634 refactor: migrate Feature to billing module and split ProductMedia to catalog
- Move Feature model from models/database/ to app/modules/billing/models/
  (tightly coupled to SubscriptionTier for tier-based access control)
- Move ProductMedia from models/database/media.py to app/modules/catalog/models/
  (product-specific media associations belong with catalog)
- Keep MediaFile as CORE in models/database/media.py (cross-cutting file storage)
- Convert legacy feature.py to re-export for backwards compatibility
- Update all imports to use canonical module locations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:32:38 +01:00

201 lines
6.9 KiB
Python

# app/modules/billing/models/feature.py
"""
Feature registry for tier-based access control.
Provides a database-driven feature registry that allows:
- Dynamic feature-to-tier assignment (no code changes needed)
- UI metadata for frontend rendering
- Feature categorization for organization
- Upgrade prompts with tier info
Features are assigned to tiers via the SubscriptionTier.features JSON array.
This model provides the metadata and acts as a registry of all available features.
"""
import enum
from datetime import UTC, datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class FeatureCategory(str, enum.Enum):
"""Feature categories for organization."""
ORDERS = "orders"
INVENTORY = "inventory"
ANALYTICS = "analytics"
INVOICING = "invoicing"
INTEGRATIONS = "integrations"
TEAM = "team"
BRANDING = "branding"
CUSTOMERS = "customers"
CMS = "cms"
class FeatureUILocation(str, enum.Enum):
"""Where the feature appears in the UI."""
SIDEBAR = "sidebar" # Main navigation item
DASHBOARD = "dashboard" # Dashboard widget/section
SETTINGS = "settings" # Settings page option
API = "api" # API-only feature (no UI)
INLINE = "inline" # Inline feature within a page
class Feature(Base, TimestampMixin):
"""
Feature registry for tier-based access control.
Each feature represents a capability that can be enabled/disabled per tier.
The actual tier assignment is stored in SubscriptionTier.features as a JSON
array of feature codes. This table provides metadata for:
- UI rendering (icons, labels, locations)
- Upgrade prompts (which tier unlocks this?)
- Admin management (description, categorization)
Example features:
- analytics_dashboard: Full analytics with charts
- api_access: REST API access for integrations
- team_roles: Role-based permissions for team members
- automation_rules: Automatic order processing rules
"""
__tablename__ = "features"
id = Column(Integer, primary_key=True, index=True)
# Unique identifier used in code and tier.features JSON
code = Column(String(50), unique=True, nullable=False, index=True)
# Display info
name = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
# Categorization
category = Column(String(50), nullable=False, index=True)
# UI metadata - tells frontend how to render
ui_location = Column(String(50), nullable=True) # sidebar, dashboard, settings, api
ui_icon = Column(String(50), nullable=True) # Icon name (e.g., "chart-bar")
ui_route = Column(String(100), nullable=True) # Route pattern (e.g., "/vendor/{code}/analytics")
ui_badge_text = Column(String(20), nullable=True) # Badge to show (e.g., "Pro", "New")
# Minimum tier that includes this feature (for upgrade prompts)
# This is denormalized for performance - the actual assignment is in SubscriptionTier.features
minimum_tier_id = Column(
Integer, ForeignKey("subscription_tiers.id"), nullable=True, index=True
)
minimum_tier = relationship("SubscriptionTier", foreign_keys=[minimum_tier_id])
# Status
is_active = Column(Boolean, default=True, nullable=False) # Feature available at all
is_visible = Column(Boolean, default=True, nullable=False) # Show in UI even if locked
display_order = Column(Integer, default=0, nullable=False) # Sort order within category
# Indexes
__table_args__ = (
Index("idx_feature_category_order", "category", "display_order"),
Index("idx_feature_active_visible", "is_active", "is_visible"),
)
def __repr__(self) -> str:
return f"<Feature(code='{self.code}', name='{self.name}', category='{self.category}')>"
def to_dict(self) -> dict:
"""Convert to dictionary for API responses."""
return {
"id": self.id,
"code": self.code,
"name": self.name,
"description": self.description,
"category": self.category,
"ui_location": self.ui_location,
"ui_icon": self.ui_icon,
"ui_route": self.ui_route,
"ui_badge_text": self.ui_badge_text,
"minimum_tier_code": self.minimum_tier.code if self.minimum_tier else None,
"minimum_tier_name": self.minimum_tier.name if self.minimum_tier else None,
"is_active": self.is_active,
"is_visible": self.is_visible,
"display_order": self.display_order,
}
# ============================================================================
# Feature Code Constants
# ============================================================================
# These constants are used throughout the codebase for type safety.
# The actual feature definitions and tier assignments are in the database.
class FeatureCode:
"""
Feature code constants for use in @require_feature decorator and checks.
Usage:
@require_feature(FeatureCode.ANALYTICS_DASHBOARD)
def get_analytics(...):
...
if feature_service.has_feature(db, vendor_id, FeatureCode.API_ACCESS):
...
"""
# Orders
ORDER_MANAGEMENT = "order_management"
ORDER_BULK_ACTIONS = "order_bulk_actions"
ORDER_EXPORT = "order_export"
AUTOMATION_RULES = "automation_rules"
# Inventory
INVENTORY_BASIC = "inventory_basic"
INVENTORY_LOCATIONS = "inventory_locations"
INVENTORY_PURCHASE_ORDERS = "inventory_purchase_orders"
LOW_STOCK_ALERTS = "low_stock_alerts"
# Analytics
BASIC_REPORTS = "basic_reports"
ANALYTICS_DASHBOARD = "analytics_dashboard"
CUSTOM_REPORTS = "custom_reports"
EXPORT_REPORTS = "export_reports"
# Invoicing
INVOICE_LU = "invoice_lu"
INVOICE_EU_VAT = "invoice_eu_vat"
INVOICE_BULK = "invoice_bulk"
ACCOUNTING_EXPORT = "accounting_export"
# Integrations
LETZSHOP_SYNC = "letzshop_sync"
API_ACCESS = "api_access"
WEBHOOKS = "webhooks"
CUSTOM_INTEGRATIONS = "custom_integrations"
# Team
SINGLE_USER = "single_user"
TEAM_BASIC = "team_basic"
TEAM_ROLES = "team_roles"
AUDIT_LOG = "audit_log"
# Branding
BASIC_SHOP = "basic_shop"
CUSTOM_DOMAIN = "custom_domain"
WHITE_LABEL = "white_label"
# Customers
CUSTOMER_VIEW = "customer_view"
CUSTOMER_EXPORT = "customer_export"
CUSTOMER_MESSAGING = "customer_messaging"
# CMS
CMS_BASIC = "cms_basic" # Basic CMS functionality (override defaults)
CMS_CUSTOM_PAGES = "cms_custom_pages" # Create custom pages beyond defaults
CMS_UNLIMITED_PAGES = "cms_unlimited_pages" # No page limit
CMS_TEMPLATES = "cms_templates" # Access to page templates
CMS_SEO = "cms_seo" # Advanced SEO features
CMS_SCHEDULING = "cms_scheduling" # Schedule page publish/unpublish