Files
orion/docs/architecture/menu-management.md
Samir Boulahtit d7a0ff8818 refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:02:56 +01:00

18 KiB

Menu Management Architecture

The Wizamart platform provides a module-driven menu system where each module defines its own menu items. The MenuDiscoveryService aggregates menus from all enabled modules, applying visibility configuration and permission filtering.

Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                   MODULE DEFINITIONS (Source of Truth)                   │
│  app/modules/*/definition.py                                            │
│                                                                          │
│  Each module defines its menu items per FrontendType:                   │
│  ┌─────────────────────────────┐  ┌─────────────────────────────┐      │
│  │   catalog.definition.py     │  │   orders.definition.py      │      │
│  │   menus={ADMIN: [...],     │  │   menus={ADMIN: [...],      │      │
│  │          VENDOR: [...]}     │  │          VENDOR: [...]}     │      │
│  └─────────────────────────────┘  └─────────────────────────────┘      │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                    MENU DISCOVERY SERVICE                                │
│  app/modules/core/services/menu_discovery_service.py                    │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  1. Collect menu items from all enabled modules                  │   │
│  │  2. Filter by user permissions (super_admin_only)               │   │
│  │  3. Apply visibility overrides (AdminMenuConfig)                │   │
│  │  4. Sort by section/item order                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                    VISIBILITY CONFIGURATION                              │
│  app/modules/core/models/admin_menu_config.py                           │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    AdminMenuConfig Table                         │   │
│  │  Stores visibility overrides (hidden items only)                 │   │
│  │  - Platform scope: applies to platform admins/vendors            │   │
│  │  - User scope: applies to specific super admin                   │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        FILTERED MENU OUTPUT                              │
│                                                                          │
│  Module Menus - Disabled Modules - Hidden Items = Visible Menu          │
└─────────────────────────────────────────────────────────────────────────┘

Frontend Types

The system supports four distinct frontend types:

Frontend Description Users
PLATFORM Public marketing pages Unauthenticated visitors
ADMIN Admin panel Super admins, platform admins
VENDOR Vendor dashboard Vendors on a platform
STOREFRONT Customer-facing shop Shop customers
from app.modules.enums import FrontendType

# Use in code
FrontendType.PLATFORM    # "platform"
FrontendType.ADMIN       # "admin"
FrontendType.VENDOR      # "vendor"
FrontendType.STOREFRONT  # "storefront"

Module-Driven Menus

Each module defines its menu items in definition.py using dataclasses:

Menu Item Definition

# app/modules/base.py

@dataclass
class MenuItemDefinition:
    """Single menu item definition."""
    id: str                      # Unique identifier (e.g., "catalog.products")
    label_key: str               # i18n key for label
    icon: str                    # Lucide icon name
    route: str                   # URL path
    order: int = 100             # Sort order within section
    is_mandatory: bool = False   # Cannot be hidden by user
    is_super_admin_only: bool = False  # Only visible to super admins

@dataclass
class MenuSectionDefinition:
    """Section containing menu items."""
    id: str                      # Section identifier
    label_key: str               # i18n key for section label
    icon: str                    # Section icon
    order: int = 100             # Sort order
    items: list[MenuItemDefinition] = field(default_factory=list)
    is_super_admin_only: bool = False

Example Module Definition

# app/modules/catalog/definition.py

from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition
from app.modules.enums import FrontendType

catalog_module = ModuleDefinition(
    code="catalog",
    name="Product Catalog",
    description="Product and category management",
    version="1.0.0",
    is_core=True,
    menus={
        FrontendType.ADMIN: [
            MenuSectionDefinition(
                id="catalog",
                label_key="menu.catalog",
                icon="package",
                order=30,
                items=[
                    MenuItemDefinition(
                        id="products",
                        label_key="menu.products",
                        icon="box",
                        route="/admin/products",
                        order=10,
                        is_mandatory=True
                    ),
                    MenuItemDefinition(
                        id="categories",
                        label_key="menu.categories",
                        icon="folder-tree",
                        route="/admin/categories",
                        order=20
                    ),
                ]
            )
        ],
        FrontendType.VENDOR: [
            MenuSectionDefinition(
                id="products",
                label_key="menu.my_products",
                icon="package",
                order=10,
                items=[
                    MenuItemDefinition(
                        id="products",
                        label_key="menu.products",
                        icon="box",
                        route="/vendor/{vendor_code}/products",
                        order=10,
                        is_mandatory=True
                    ),
                ]
            )
        ],
    }
)

Menu Discovery Service

The MenuDiscoveryService aggregates menus from all enabled modules:

from app.modules.core.services.menu_discovery_service import menu_discovery_service

# Get menu for a frontend type
sections = menu_discovery_service.get_menu_for_frontend(
    db=db,
    frontend_type=FrontendType.ADMIN,
    platform_id=1,
    user_id=current_user.id,
    is_super_admin=current_user.is_super_admin,
)

# Get all menu items for configuration UI
all_items = menu_discovery_service.get_all_menu_items(
    db=db,
    frontend_type=FrontendType.ADMIN,
    platform_id=1,
)

Discovery Flow

  1. Collect: Get menu definitions from all modules in MODULES registry
  2. Filter by Module: Only include menus from enabled modules for the platform
  3. Filter by Permissions: Remove super_admin_only items for non-super admins
  4. Apply Visibility: Check AdminMenuConfig for hidden items
  5. Sort: Order sections and items by their order field
  6. Return: List of MenuSectionDefinition with filtered items

Visibility Configuration

Opt-Out Model

The system uses an opt-out model:

  • All menu items are visible by default
  • Only hidden items are stored in the database
  • This keeps the database small and makes the default state explicit

Mandatory Items

Certain menu items cannot be hidden. These are marked with is_mandatory=True in their definition:

MenuItemDefinition(
    id="dashboard",
    label_key="menu.dashboard",
    icon="home",
    route="/admin/dashboard",
    is_mandatory=True,  # Cannot be hidden
)

Scope Types

Menu configuration supports two scopes:

Scope Field Description Use Case
Platform platform_id Applies to all users on platform Hide features not used by platform
User user_id Applies to specific super admin Personal preference customization

Important Rules:

  • Exactly one scope must be set (platform XOR user)
  • User scope is only allowed for admin frontend (super admins only)
  • Vendor frontend only supports platform scope

Resolution Order

Admin Frontend:

Platform admin → Check platform config → Fall back to default (all visible)
Super admin   → Check user config → Fall back to default (all visible)

Vendor Frontend:

Vendor → Check platform config → Fall back to default (all visible)

Database Model

AdminMenuConfig Table

CREATE TABLE admin_menu_configs (
    id SERIAL PRIMARY KEY,
    frontend_type VARCHAR(10) NOT NULL,  -- 'admin' or 'vendor'
    platform_id INTEGER REFERENCES platforms(id),
    user_id INTEGER REFERENCES users(id),
    menu_item_id VARCHAR(50) NOT NULL,
    is_visible BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,

    -- Constraints
    CONSTRAINT uq_frontend_platform_menu_config
        UNIQUE (frontend_type, platform_id, menu_item_id),
    CONSTRAINT uq_frontend_user_menu_config
        UNIQUE (frontend_type, user_id, menu_item_id),
    CONSTRAINT ck_admin_menu_config_scope
        CHECK ((platform_id IS NOT NULL AND user_id IS NULL) OR
               (platform_id IS NULL AND user_id IS NOT NULL)),
    CONSTRAINT ck_user_scope_admin_only
        CHECK ((user_id IS NULL) OR (frontend_type = 'admin'))
);

Examples

from app.modules.core.models import AdminMenuConfig
from app.modules.enums import FrontendType

# Platform "OMS" hides inventory from admin panel
AdminMenuConfig(
    frontend_type=FrontendType.ADMIN,
    platform_id=1,
    menu_item_id="inventory",
    is_visible=False
)

# Platform "OMS" hides letzshop from vendor dashboard
AdminMenuConfig(
    frontend_type=FrontendType.VENDOR,
    platform_id=1,
    menu_item_id="letzshop",
    is_visible=False
)

# Super admin "john" hides code-quality from their admin panel
AdminMenuConfig(
    frontend_type=FrontendType.ADMIN,
    user_id=5,
    menu_item_id="code-quality",
    is_visible=False
)

Module Integration

Automatic Menu Discovery

When a module is enabled/disabled for a platform, the menu discovery service automatically includes/excludes its menu items:

# Module enablement check happens automatically in MenuDiscoveryService
enabled_modules = module_service.get_enabled_module_codes(db, platform_id)

# Only modules in enabled_modules have their menus included
for module_code, module_def in MODULES.items():
    if module_code in enabled_modules:
        # Include this module's menus
        pass

Three-Layer Filtering

The final visible menu is computed by three layers:

  1. Module Definitions: All possible menu items from modules
  2. Module Enablement: Items from disabled modules are hidden
  3. Visibility Config: Explicitly hidden items are removed
Final Menu = Module Menu Items
           - Items from Disabled Modules
           - Items with is_visible=False in config
           - Items not matching user role (super_admin_only)

Menu Service Integration

The MenuService provides the interface used by templates:

from app.modules.core.services import menu_service

# Get menu for rendering in templates
menu_data = menu_service.get_menu_for_rendering(
    db=db,
    frontend_type=FrontendType.ADMIN,
    platform_id=platform_id,
    user_id=user_id,
    is_super_admin=is_super_admin,
    vendor_code=vendor_code,  # For vendor frontend
)

# Returns legacy format for template compatibility:
# {
#     "frontend_type": "admin",
#     "sections": [
#         {
#             "id": "main",
#             "label": None,
#             "items": [{"id": "dashboard", "label": "Dashboard", ...}]
#         },
#         ...
#     ]
# }

Access Control

Route Protection

from app.api.deps import require_menu_access

@router.get("/admin/inventory")
async def inventory_page(
    _access: bool = Depends(require_menu_access("inventory", FrontendType.ADMIN))
):
    # Only accessible if menu item is visible for user's context
    pass

Sidebar Rendering

The sidebar template filters items based on:

  1. Module enablement
  2. Visibility configuration
  3. User role (super admin check)
{% for section in menu_sections %}
  {% if not section.super_admin_only or current_user.is_super_admin %}
    {% for item in section.items %}
      {% if item.id in visible_menu_items %}
        <a href="{{ item.url }}">{{ item.label }}</a>
      {% endif %}
    {% endfor %}
  {% endif %}
{% endfor %}

UI for Menu Configuration

Platform Admin Menu Config

Located at /admin/platform-menu-config (accessible by super admins):

  • Configure which menu items are visible for platform admins
  • Configure which menu items are visible for vendors on this platform
  • Mandatory items cannot be unchecked

Personal Menu Config (Super Admins)

Located at /admin/my-menu:

  • Super admins can customize their own admin panel menu
  • Personal preferences that don't affect other users
  • Useful for hiding rarely-used features

Adding Menu Items to a Module

  1. Define menu sections and items in your module's definition.py:
# app/modules/mymodule/definition.py

from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition
from app.modules.enums import FrontendType

mymodule = ModuleDefinition(
    code="mymodule",
    name="My Module",
    description="Description of my module",
    version="1.0.0",
    menus={
        FrontendType.ADMIN: [
            MenuSectionDefinition(
                id="mymodule",
                label_key="mymodule.menu.section",
                icon="star",
                order=50,  # Position in sidebar
                items=[
                    MenuItemDefinition(
                        id="mymodule-main",
                        label_key="mymodule.menu.main",
                        icon="star",
                        route="/admin/mymodule",
                        order=10,
                    ),
                ]
            )
        ],
    }
)
  1. Add translation keys in your module's locales:
// app/modules/mymodule/locales/en.json
{
  "mymodule.menu.section": "My Module",
  "mymodule.menu.main": "Main Page"
}
  1. The menu is automatically discovered - no registration needed.

Best Practices

Do

  • Define menus in module definition.py using dataclasses
  • Use translation keys (label_key) for labels
  • Set appropriate order values for positioning
  • Mark essential items as is_mandatory=True
  • Use is_super_admin_only=True for admin-only features

Don't

  • Hardcode menu item labels (use i18n keys)
  • Store is_visible=True in database (default state, wastes space)
  • Allow hiding mandatory items via API
  • Create menu items outside of module definitions