"""add features table and seed data Revision ID: n2c3d4e5f6a7 Revises: ba2c0ce78396 Create Date: 2025-12-31 10:00:00.000000 """ import json from collections.abc import Sequence import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "n2c3d4e5f6a7" down_revision: str | None = "ba2c0ce78396" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None # ============================================================================ # Feature Definitions # ============================================================================ # category, code, name, description, ui_location, ui_icon, ui_route, display_order FEATURES = [ # Orders (category: orders) ("orders", "order_management", "Order Management", "View and manage orders", "sidebar", "clipboard-list", "/vendor/{code}/orders", 1), ("orders", "order_bulk_actions", "Bulk Order Actions", "Process multiple orders at once", "inline", None, None, 2), ("orders", "order_export", "Order Export", "Export orders to CSV/Excel", "inline", "download", None, 3), ("orders", "automation_rules", "Automation Rules", "Automatic order processing rules", "sidebar", "cog", "/vendor/{code}/automation", 4), # Inventory (category: inventory) ("inventory", "inventory_basic", "Basic Inventory", "Track product stock levels", "sidebar", "cube", "/vendor/{code}/inventory", 1), ("inventory", "inventory_locations", "Warehouse Locations", "Manage multiple warehouse locations", "inline", "map-pin", None, 2), ("inventory", "inventory_purchase_orders", "Purchase Orders", "Create and manage purchase orders", "sidebar", "shopping-cart", "/vendor/{code}/purchase-orders", 3), ("inventory", "low_stock_alerts", "Low Stock Alerts", "Get notified when stock is low", "inline", "bell", None, 4), # Analytics (category: analytics) ("analytics", "basic_reports", "Basic Reports", "Essential sales and order reports", "sidebar", "chart-pie", "/vendor/{code}/reports", 1), ("analytics", "analytics_dashboard", "Analytics Dashboard", "Advanced analytics with charts and trends", "sidebar", "chart-bar", "/vendor/{code}/analytics", 2), ("analytics", "custom_reports", "Custom Reports", "Build custom report configurations", "inline", "document-report", None, 3), ("analytics", "export_reports", "Export Reports", "Export reports to various formats", "inline", "download", None, 4), # Invoicing (category: invoicing) ("invoicing", "invoice_lu", "Luxembourg Invoicing", "Generate compliant Luxembourg invoices", "sidebar", "document-text", "/vendor/{code}/invoices", 1), ("invoicing", "invoice_eu_vat", "EU VAT Support", "Handle EU VAT for cross-border sales", "inline", "globe", None, 2), ("invoicing", "invoice_bulk", "Bulk Invoicing", "Generate invoices in bulk", "inline", "document-duplicate", None, 3), ("invoicing", "accounting_export", "Accounting Export", "Export to accounting software formats", "inline", "calculator", None, 4), # Integrations (category: integrations) ("integrations", "letzshop_sync", "Letzshop Sync", "Sync orders and products with Letzshop", "settings", "refresh", None, 1), ("integrations", "api_access", "API Access", "REST API access for custom integrations", "settings", "code", "/vendor/{code}/settings/api", 2), ("integrations", "webhooks", "Webhooks", "Receive real-time event notifications", "settings", "lightning-bolt", "/vendor/{code}/settings/webhooks", 3), ("integrations", "custom_integrations", "Custom Integrations", "Connect with any third-party service", "settings", "puzzle", None, 4), # Team (category: team) ("team", "single_user", "Single User", "One user account", "api", None, None, 1), ("team", "team_basic", "Team Access", "Invite team members", "sidebar", "users", "/vendor/{code}/team", 2), ("team", "team_roles", "Team Roles", "Role-based permissions for team members", "inline", "shield-check", None, 3), ("team", "audit_log", "Audit Log", "Track all user actions", "sidebar", "clipboard-check", "/vendor/{code}/audit-log", 4), # Branding (category: branding) ("branding", "basic_shop", "Basic Shop", "Your shop on the platform", "api", None, None, 1), ("branding", "custom_domain", "Custom Domain", "Use your own domain name", "settings", "globe-alt", None, 2), ("branding", "white_label", "White Label", "Remove platform branding entirely", "settings", "color-swatch", None, 3), # Customers (category: customers) ("customers", "customer_view", "Customer View", "View customer information", "sidebar", "user-group", "/vendor/{code}/customers", 1), ("customers", "customer_export", "Customer Export", "Export customer data", "inline", "download", None, 2), ("customers", "customer_messaging", "Customer Messaging", "Send messages to customers", "inline", "chat", None, 3), ] # ============================================================================ # Tier Feature Assignments # ============================================================================ # tier_code -> list of feature codes TIER_FEATURES = { "essential": [ "order_management", "inventory_basic", "basic_reports", "invoice_lu", "letzshop_sync", "single_user", "basic_shop", "customer_view", ], "professional": [ # All Essential features "order_management", "order_bulk_actions", "order_export", "inventory_basic", "inventory_locations", "inventory_purchase_orders", "low_stock_alerts", "basic_reports", "invoice_lu", "invoice_eu_vat", "letzshop_sync", "team_basic", "basic_shop", "customer_view", "customer_export", ], "business": [ # All Professional features "order_management", "order_bulk_actions", "order_export", "automation_rules", "inventory_basic", "inventory_locations", "inventory_purchase_orders", "low_stock_alerts", "basic_reports", "analytics_dashboard", "custom_reports", "export_reports", "invoice_lu", "invoice_eu_vat", "invoice_bulk", "accounting_export", "letzshop_sync", "api_access", "webhooks", "team_basic", "team_roles", "audit_log", "basic_shop", "custom_domain", "customer_view", "customer_export", "customer_messaging", ], "enterprise": [ # All features "order_management", "order_bulk_actions", "order_export", "automation_rules", "inventory_basic", "inventory_locations", "inventory_purchase_orders", "low_stock_alerts", "basic_reports", "analytics_dashboard", "custom_reports", "export_reports", "invoice_lu", "invoice_eu_vat", "invoice_bulk", "accounting_export", "letzshop_sync", "api_access", "webhooks", "custom_integrations", "team_basic", "team_roles", "audit_log", "basic_shop", "custom_domain", "white_label", "customer_view", "customer_export", "customer_messaging", ], } # Minimum tier for each feature (for upgrade prompts) # Maps feature_code -> tier_code MINIMUM_TIER = { # Essential "order_management": "essential", "inventory_basic": "essential", "basic_reports": "essential", "invoice_lu": "essential", "letzshop_sync": "essential", "single_user": "essential", "basic_shop": "essential", "customer_view": "essential", # Professional "order_bulk_actions": "professional", "order_export": "professional", "inventory_locations": "professional", "inventory_purchase_orders": "professional", "low_stock_alerts": "professional", "invoice_eu_vat": "professional", "team_basic": "professional", "customer_export": "professional", # Business "automation_rules": "business", "analytics_dashboard": "business", "custom_reports": "business", "export_reports": "business", "invoice_bulk": "business", "accounting_export": "business", "api_access": "business", "webhooks": "business", "team_roles": "business", "audit_log": "business", "custom_domain": "business", "customer_messaging": "business", # Enterprise "custom_integrations": "enterprise", "white_label": "enterprise", } def upgrade() -> None: # Create features table op.create_table( "features", sa.Column("id", sa.Integer(), nullable=False), sa.Column("code", sa.String(50), nullable=False), sa.Column("name", sa.String(100), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column("category", sa.String(50), nullable=False), sa.Column("ui_location", sa.String(50), nullable=True), sa.Column("ui_icon", sa.String(50), nullable=True), sa.Column("ui_route", sa.String(100), nullable=True), sa.Column("ui_badge_text", sa.String(20), nullable=True), sa.Column("minimum_tier_id", sa.Integer(), nullable=True), sa.Column("is_active", sa.Boolean(), nullable=False, default=True), sa.Column("is_visible", sa.Boolean(), nullable=False, default=True), sa.Column("display_order", sa.Integer(), nullable=False, default=0), sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["minimum_tier_id"], ["subscription_tiers.id"]), sa.PrimaryKeyConstraint("id"), ) op.create_index("ix_features_code", "features", ["code"], unique=True) op.create_index("ix_features_category", "features", ["category"], unique=False) op.create_index("idx_feature_category_order", "features", ["category", "display_order"]) op.create_index("idx_feature_active_visible", "features", ["is_active", "is_visible"]) # Get connection for data operations conn = op.get_bind() # Get tier IDs tier_ids = {} result = conn.execute(sa.text("SELECT id, code FROM subscription_tiers")) for row in result: tier_ids[row[1]] = row[0] # Insert features sa.func.now() for category, code, name, description, ui_location, ui_icon, ui_route, display_order in FEATURES: minimum_tier_code = MINIMUM_TIER.get(code) minimum_tier_id = tier_ids.get(minimum_tier_code) if minimum_tier_code else None conn.execute( sa.text(""" INSERT INTO features (code, name, description, category, ui_location, ui_icon, ui_route, minimum_tier_id, is_active, is_visible, display_order, created_at, updated_at) VALUES (:code, :name, :description, :category, :ui_location, :ui_icon, :ui_route, :minimum_tier_id, true, true, :display_order, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """), { "code": code, "name": name, "description": description, "category": category, "ui_location": ui_location, "ui_icon": ui_icon, "ui_route": ui_route, "minimum_tier_id": minimum_tier_id, "display_order": display_order, }, ) # Update subscription_tiers with feature arrays for tier_code, features in TIER_FEATURES.items(): features_json = json.dumps(features) conn.execute( sa.text("UPDATE subscription_tiers SET features = :features WHERE code = :code"), {"features": features_json, "code": tier_code}, ) def downgrade() -> None: # Clear features from subscription_tiers conn = op.get_bind() conn.execute(sa.text("UPDATE subscription_tiers SET features = '[]'")) # Drop features table op.drop_index("idx_feature_active_visible", table_name="features") op.drop_index("idx_feature_category_order", table_name="features") op.drop_index("ix_features_category", table_name="features") op.drop_index("ix_features_code", table_name="features") op.drop_table("features")