Implement the foundation for multi-platform support allowing independent business offerings (OMS, Loyalty, etc.) with their own CMS pages. Database Models: - Add Platform model for business offerings (domain, branding, config) - Add VendorPlatform junction table for many-to-many relationship - Update SubscriptionTier with platform_id and CMS limits - Update ContentPage with platform_id, is_platform_page for three-tier hierarchy - Add CMS feature codes (cms_basic, cms_custom_pages, cms_templates, etc.) Three-Tier Content Resolution: 1. Vendor override (platform_id + vendor_id + slug) 2. Vendor default (platform_id + vendor_id=NULL + is_platform_page=False) 3. Platform marketing pages (is_platform_page=True) New Components: - PlatformContextMiddleware for detecting platform from domain/path - ContentPageService updated with full three-tier resolution - Platform folder structure (app/platforms/oms/, app/platforms/loyalty/) - Alembic migration with backfill for existing data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
6.9 KiB
Python
201 lines
6.9 KiB
Python
# 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"<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
|