diff --git a/app/modules/registry.py b/app/modules/registry.py index 6e0b3f70..0d9ecb35 100644 --- a/app/modules/registry.py +++ b/app/modules/registry.py @@ -1,16 +1,19 @@ # app/modules/registry.py """ -Module registry defining all available platform modules. +Module registry - AUTO-DISCOVERED. + +All modules are automatically discovered from app/modules/*/definition.py. +No manual imports needed - just create a module directory with definition.py. The module system uses a three-tier classification: -1. CORE MODULES (4) - Always enabled, cannot be disabled +1. CORE MODULES - Always enabled, cannot be disabled (is_core=True) - core: Dashboard, settings, profile - tenancy: Platform, company, vendor, admin user management - cms: Content pages, media library, themes - customers: Customer database, profiles, segmentation -2. OPTIONAL MODULES (7) - Can be enabled/disabled per platform +2. OPTIONAL MODULES - Can be enabled/disabled per platform (default) - payments: Payment gateway integrations (Stripe, PayPal, etc.) - billing: Platform subscriptions, vendor invoices (requires: payments) - inventory: Stock management, locations @@ -19,142 +22,56 @@ The module system uses a three-tier classification: - analytics: Reports, dashboards - messaging: Messages, notifications -3. INTERNAL MODULES (2) - Admin-only tools, not customer-facing +3. INTERNAL MODULES - Admin-only tools, not customer-facing (is_internal=True) - dev-tools: Component library, icons - monitoring: Logs, background tasks, Flower link, Grafana dashboards -Module Structure: -- Inline modules: Defined directly in this file (core, tenancy) -- Extracted modules: Imported from app/modules/{module}/ (billing, etc.) - -As modules are extracted to their own directories, they are imported -here and their inline definitions are replaced. +To add a new module: +1. Create app/modules// directory +2. Add definition.py with ModuleDefinition instance +3. Set is_core=True or is_internal=True for non-optional modules +4. That's it! Module will be auto-discovered. """ +import logging +from functools import lru_cache + from app.modules.base import ModuleDefinition +from app.modules.discovery import discover_modules, discover_modules_by_tier from models.database.admin_menu_config import FrontendType -# Import extracted modules -from app.modules.billing.definition import billing_module -from app.modules.payments.definition import payments_module -from app.modules.inventory.definition import inventory_module -from app.modules.marketplace.definition import marketplace_module -from app.modules.orders.definition import orders_module -from app.modules.customers.definition import customers_module -from app.modules.cms.definition import cms_module -from app.modules.analytics.definition import analytics_module -from app.modules.messaging.definition import messaging_module -from app.modules.dev_tools.definition import dev_tools_module -from app.modules.monitoring.definition import monitoring_module +logger = logging.getLogger(__name__) # ============================================================================= -# Core Modules (Always Enabled, Cannot Be Disabled) +# Auto-Discovered Module Registry # ============================================================================= -CORE_MODULES: dict[str, ModuleDefinition] = { - "core": ModuleDefinition( - code="core", - name="Core Platform", - description="Dashboard, settings, and profile management. Required for basic operation.", - is_core=True, - features=[ - "dashboard", - "settings", - "profile", - ], - menu_items={ - FrontendType.ADMIN: [ - "dashboard", - "settings", - "email-templates", - "my-menu", - ], - FrontendType.VENDOR: [ - "dashboard", - "profile", - "settings", - "email-templates", - ], - }, - ), - "tenancy": ModuleDefinition( - code="tenancy", - name="Tenancy Management", - description="Platform, company, vendor, and admin user management. Required for multi-tenant operation.", - is_core=True, - features=[ - "platform_management", - "company_management", - "vendor_management", - "admin_user_management", - ], - menu_items={ - FrontendType.ADMIN: [ - "platforms", - "companies", - "vendors", - "admin-users", - ], - FrontendType.VENDOR: [ - "team", - ], - }, - ), - # CMS module - imported from app/modules/cms/ - "cms": cms_module, - # Customers module - imported from app/modules/customers/ - "customers": customers_module, -} +@lru_cache(maxsize=1) +def _get_all_modules() -> dict[str, ModuleDefinition]: + """Get all modules (cached).""" + return discover_modules() -# ============================================================================= -# Optional Modules (Can Be Enabled/Disabled Per Platform) -# ============================================================================= - -OPTIONAL_MODULES: dict[str, ModuleDefinition] = { - # Payments module - imported from app/modules/payments/ - # Gateway integrations (Stripe, PayPal, etc.) - "payments": payments_module, - # Billing module - imported from app/modules/billing/ - # Platform subscriptions, vendor invoices (requires: payments) - "billing": billing_module, - # Inventory module - imported from app/modules/inventory/ - "inventory": inventory_module, - # Orders module - imported from app/modules/orders/ - # Order management, customer checkout (requires: payments) - "orders": orders_module, - # Marketplace module - imported from app/modules/marketplace/ - # Letzshop integration (requires: inventory) - "marketplace": marketplace_module, - # Analytics module - imported from app/modules/analytics/ - "analytics": analytics_module, - # Messaging module - imported from app/modules/messaging/ - "messaging": messaging_module, -} +@lru_cache(maxsize=1) +def _get_modules_by_tier() -> dict[str, dict[str, ModuleDefinition]]: + """Get modules organized by tier (cached).""" + return discover_modules_by_tier() -# ============================================================================= -# Internal Modules (Admin-Only, Not Customer-Facing) -# ============================================================================= - -INTERNAL_MODULES: dict[str, ModuleDefinition] = { - # Dev-Tools module - imported from app/modules/dev_tools/ - "dev-tools": dev_tools_module, - # Monitoring module - imported from app/modules/monitoring/ - "monitoring": monitoring_module, -} - - -# ============================================================================= -# Combined Module Registry -# ============================================================================= - -MODULES: dict[str, ModuleDefinition] = { - **CORE_MODULES, - **OPTIONAL_MODULES, - **INTERNAL_MODULES, -} +# Expose as module-level variables for backward compatibility +# These are computed lazily on first access +def __getattr__(name: str): + """Lazy module-level attribute access for backward compatibility.""" + if name == "MODULES": + return _get_all_modules() + elif name == "CORE_MODULES": + return _get_modules_by_tier()["core"] + elif name == "OPTIONAL_MODULES": + return _get_modules_by_tier()["optional"] + elif name == "INTERNAL_MODULES": + return _get_modules_by_tier()["internal"] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # ============================================================================= @@ -164,52 +81,52 @@ MODULES: dict[str, ModuleDefinition] = { def get_module(code: str) -> ModuleDefinition | None: """Get a module definition by code.""" - return MODULES.get(code) + return _get_all_modules().get(code) def get_core_modules() -> list[ModuleDefinition]: """Get all core modules (cannot be disabled).""" - return list(CORE_MODULES.values()) + return list(_get_modules_by_tier()["core"].values()) def get_core_module_codes() -> set[str]: """Get codes of all core modules.""" - return set(CORE_MODULES.keys()) + return set(_get_modules_by_tier()["core"].keys()) def get_optional_modules() -> list[ModuleDefinition]: """Get all optional modules (can be enabled/disabled).""" - return list(OPTIONAL_MODULES.values()) + return list(_get_modules_by_tier()["optional"].values()) def get_optional_module_codes() -> set[str]: """Get codes of all optional modules.""" - return set(OPTIONAL_MODULES.keys()) + return set(_get_modules_by_tier()["optional"].keys()) def get_internal_modules() -> list[ModuleDefinition]: """Get all internal modules (admin-only tools).""" - return list(INTERNAL_MODULES.values()) + return list(_get_modules_by_tier()["internal"].values()) def get_internal_module_codes() -> set[str]: """Get codes of all internal modules.""" - return set(INTERNAL_MODULES.keys()) + return set(_get_modules_by_tier()["internal"].keys()) def get_all_module_codes() -> set[str]: """Get all module codes.""" - return set(MODULES.keys()) + return set(_get_all_modules().keys()) def is_core_module(code: str) -> bool: """Check if a module is a core module.""" - return code in CORE_MODULES + return code in _get_modules_by_tier()["core"] def is_internal_module(code: str) -> bool: """Check if a module is an internal module.""" - return code in INTERNAL_MODULES + return code in _get_modules_by_tier()["internal"] def get_menu_item_module(menu_item_id: str, frontend_type: FrontendType) -> str | None: @@ -223,7 +140,7 @@ def get_menu_item_module(menu_item_id: str, frontend_type: FrontendType) -> str Returns: Module code if found, None otherwise """ - for module in MODULES.values(): + for module in _get_all_modules().values(): if menu_item_id in module.get_menu_items(frontend_type): return module.code return None @@ -239,7 +156,7 @@ def get_feature_module(feature_code: str) -> str | None: Returns: Module code if found, None otherwise """ - for module in MODULES.values(): + for module in _get_all_modules().values(): if module.has_feature(feature_code): return module.code return None @@ -254,15 +171,16 @@ def validate_module_dependencies() -> list[str]: """ errors = [] all_codes = get_all_module_codes() + core_codes = get_core_module_codes() - for module in MODULES.values(): + for module in _get_all_modules().values(): for required in module.requires: if required not in all_codes: errors.append( f"Module '{module.code}' requires unknown module '{required}'" ) # Core modules should not depend on optional modules - if module.is_core and required not in get_core_module_codes(): + if module.is_core and required not in core_codes: errors.append( f"Core module '{module.code}' depends on optional module '{required}'" ) @@ -277,10 +195,11 @@ def get_modules_by_tier() -> dict[str, list[ModuleDefinition]]: Returns: Dict with keys 'core', 'optional', 'internal' mapping to module lists """ + by_tier = _get_modules_by_tier() return { - "core": list(CORE_MODULES.values()), - "optional": list(OPTIONAL_MODULES.values()), - "internal": list(INTERNAL_MODULES.values()), + "core": list(by_tier["core"].values()), + "optional": list(by_tier["optional"].values()), + "internal": list(by_tier["internal"].values()), } @@ -294,26 +213,41 @@ def get_module_tier(code: str) -> str | None: Returns: 'core', 'optional', 'internal', or None if not found """ - if code in CORE_MODULES: + by_tier = _get_modules_by_tier() + if code in by_tier["core"]: return "core" - elif code in OPTIONAL_MODULES: + elif code in by_tier["optional"]: return "optional" - elif code in INTERNAL_MODULES: + elif code in by_tier["internal"]: return "internal" return None -# Validate dependencies on import (development check) -_validation_errors = validate_module_dependencies() -if _validation_errors: - import warnings +def clear_registry_cache(): + """Clear the module registry cache. Useful for testing.""" + _get_all_modules.cache_clear() + _get_modules_by_tier.cache_clear() - for error in _validation_errors: - warnings.warn(f"Module registry validation: {error}", stacklevel=2) + +# ============================================================================= +# Validation on Import (Development Check) +# ============================================================================= + +def _run_validation(): + """Run validation checks on module registry.""" + _validation_errors = validate_module_dependencies() + if _validation_errors: + import warnings + for error in _validation_errors: + warnings.warn(f"Module registry validation: {error}", stacklevel=2) + + +# Run validation on import (can be disabled in production) +_run_validation() __all__ = [ - # Module dictionaries + # Module dictionaries (lazy-loaded) "MODULES", "CORE_MODULES", "OPTIONAL_MODULES", @@ -337,4 +271,6 @@ __all__ = [ "get_feature_module", # Validation "validate_module_dependencies", + # Cache management + "clear_registry_cache", ] diff --git a/app/routes/vendor_pages.py b/app/routes/vendor_pages.py index 908f7331..c8b021ab 100644 --- a/app/routes/vendor_pages.py +++ b/app/routes/vendor_pages.py @@ -673,25 +673,10 @@ async def vendor_notifications_page( # ============================================================================ # ANALYTICS # ============================================================================ - - -@router.get( - "/{vendor_code}/analytics", response_class=HTMLResponse, include_in_schema=False -) -async def vendor_analytics_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_vendor_from_cookie_or_header), - db: Session = Depends(get_db), -): - """ - Render analytics and reports page. - JavaScript loads analytics data via API. - """ - return templates.TemplateResponse( - "vendor/analytics.html", - get_vendor_context(request, db, current_user, vendor_code), - ) +# NOTE: Analytics routes moved to self-contained module: app.modules.analytics.routes.pages.vendor +# Routes are registered directly in main.py from the Analytics module +# This includes: +# - /{vendor_code}/analytics (dashboard) # ============================================================================ diff --git a/main.py b/main.py index c0c177b4..fec16cb2 100644 --- a/main.py +++ b/main.py @@ -61,12 +61,11 @@ from app.core.lifespan import lifespan from app.exceptions import ServiceUnavailableException from app.exceptions.handler import setup_exception_handlers -# Import page routers +# Import page routers (legacy routes - will be migrated to modules) from app.routes import admin_pages, platform_pages, shop_pages, vendor_pages -# Import CMS module page routers (self-contained module) -from app.modules.cms.routes.pages import admin_router as cms_admin_pages -from app.modules.cms.routes.pages import vendor_router as cms_vendor_pages +# Module route auto-discovery +from app.modules.routes import discover_module_routes, get_vendor_page_routes from app.utils.i18n import get_jinja2_globals from middleware.context import ContextMiddleware from middleware.language import LanguageMiddleware @@ -330,6 +329,7 @@ app.include_router( ) # Vendor management pages (dashboard, products, orders, etc.) +# NOTE: Legacy routes - modules with their own routes will override these logger.info("Registering vendor page routes: /vendor/{code}/*") app.include_router( vendor_pages.router, @@ -338,12 +338,32 @@ app.include_router( include_in_schema=False, ) -# CMS module vendor pages (self-contained module) -# NOTE: Includes catch-all /{vendor_code}/{slug} - must be registered AFTER vendor_pages -logger.info("Registering CMS vendor page routes: /vendor/{code}/content-pages/*") -app.include_router( - cms_vendor_pages, prefix="/vendor", tags=["cms-vendor-pages"], include_in_schema=False -) +# ============================================================================= +# AUTO-DISCOVERED MODULE ROUTES +# ============================================================================= +# Self-contained modules register their routes automatically. +# Routes are discovered from app/modules/*/routes/pages/ and routes/api/ +# NOTE: CMS has catch-all route, so it's registered last via priority sorting + +logger.info("Auto-discovering module page routes...") +vendor_page_routes = get_vendor_page_routes() + +# Sort routes: CMS last (has catch-all), others alphabetically +def route_priority(route): + if route.module_code == "cms": + return (1, route.module_code) # CMS last + return (0, route.module_code) + +vendor_page_routes.sort(key=route_priority) + +for route_info in vendor_page_routes: + logger.info(f" Registering {route_info.module_code} vendor pages: {route_info.prefix}") + app.include_router( + route_info.router, + prefix=route_info.prefix, + tags=route_info.tags, + include_in_schema=route_info.include_in_schema, + ) # Customer shop pages - Register at TWO prefixes: # 1. /shop/* (for subdomain/custom domain modes)