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>
This commit is contained in:
2026-01-30 19:32:38 +01:00
parent b9f08b853f
commit 0f9b80c634
15 changed files with 336 additions and 268 deletions

View File

@@ -1,200 +1,22 @@
# models/database/feature.py
"""
Feature registry for tier-based access control.
DEPRECATED: This module re-exports from the canonical location.
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.
Feature is now part of the billing module.
Please update imports to use:
from app.modules.billing.models import Feature, FeatureCode, FeatureCategory, FeatureUILocation
"""
import enum
from datetime import UTC, datetime
from app.modules.billing.models import (
Feature,
FeatureCategory,
FeatureCode,
FeatureUILocation,
)
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
__all__ = [
"Feature",
"FeatureCategory",
"FeatureCode",
"FeatureUILocation",
]