# models/database/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"" 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