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>
This commit is contained in:
@@ -1,24 +1,39 @@
|
||||
# Menu Management Architecture
|
||||
|
||||
The Wizamart platform provides a flexible menu system that supports per-platform and per-user customization. This document explains the menu architecture, configuration options, and how menus integrate with the module system.
|
||||
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
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MENU REGISTRY (Source of Truth) │
|
||||
│ app/config/menu_registry.py │
|
||||
│ MODULE DEFINITIONS (Source of Truth) │
|
||||
│ app/modules/*/definition.py │
|
||||
│ │
|
||||
│ Each module defines its menu items per FrontendType: │
|
||||
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ ADMIN_MENU_REGISTRY │ │ VENDOR_MENU_REGISTRY │ │
|
||||
│ │ (Admin panel menus) │ │ (Vendor dashboard menus) │ │
|
||||
│ │ catalog.definition.py │ │ orders.definition.py │ │
|
||||
│ │ menus={ADMIN: [...], │ │ menus={ADMIN: [...], │ │
|
||||
│ │ VENDOR: [...]} │ │ VENDOR: [...]} │ │
|
||||
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ VISIBILITY CONFIGURATION │
|
||||
│ models/database/admin_menu_config.py │
|
||||
│ 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 │ │
|
||||
@@ -32,95 +47,156 @@ The Wizamart platform provides a flexible menu system that supports per-platform
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ FILTERED MENU OUTPUT │
|
||||
│ │
|
||||
│ Registry - Hidden Items - Disabled Modules = Visible Menu Items │
|
||||
│ Module Menus - Disabled Modules - Hidden Items = Visible Menu │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Frontend Types
|
||||
|
||||
The system supports two distinct 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 |
|
||||
|
||||
```python
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
# Use in code
|
||||
FrontendType.ADMIN # "admin"
|
||||
FrontendType.VENDOR # "vendor"
|
||||
FrontendType.PLATFORM # "platform"
|
||||
FrontendType.ADMIN # "admin"
|
||||
FrontendType.VENDOR # "vendor"
|
||||
FrontendType.STOREFRONT # "storefront"
|
||||
```
|
||||
|
||||
## Menu Registry
|
||||
## Module-Driven Menus
|
||||
|
||||
The menu registry (`app/config/menu_registry.py`) is the **single source of truth** for menu structure. The database only stores visibility overrides.
|
||||
Each module defines its menu items in `definition.py` using dataclasses:
|
||||
|
||||
### Admin Menu Structure
|
||||
### Menu Item Definition
|
||||
|
||||
```python
|
||||
ADMIN_MENU_REGISTRY = {
|
||||
"frontend_type": FrontendType.ADMIN,
|
||||
"sections": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": None, # No header
|
||||
"items": [
|
||||
{"id": "dashboard", "label": "Dashboard", "icon": "home", "url": "/admin/dashboard"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "superAdmin",
|
||||
"label": "Super Admin",
|
||||
"super_admin_only": True, # Only visible to super admins
|
||||
"items": [
|
||||
{"id": "admin-users", "label": "Admin Users", "icon": "shield", "url": "/admin/admin-users"},
|
||||
],
|
||||
},
|
||||
# ... more sections
|
||||
],
|
||||
}
|
||||
# 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
|
||||
```
|
||||
|
||||
### Admin Menu Sections
|
||||
### Example Module Definition
|
||||
|
||||
| Section ID | Label | Description |
|
||||
|------------|-------|-------------|
|
||||
| `main` | (none) | Dashboard - always at top |
|
||||
| `superAdmin` | Super Admin | Super admin only tools |
|
||||
| `platformAdmin` | Platform Administration | Companies, vendors, messages |
|
||||
| `vendorOps` | Vendor Operations | Products, customers, inventory, orders |
|
||||
| `marketplace` | Marketplace | Letzshop integration |
|
||||
| `billing` | Billing & Subscriptions | Tiers, subscriptions, billing history |
|
||||
| `contentMgmt` | Content Management | Platforms, content pages, themes |
|
||||
| `devTools` | Developer Tools | Components, icons |
|
||||
| `platformHealth` | Platform Health | Capacity, testing, code quality |
|
||||
| `monitoring` | Platform Monitoring | Imports, tasks, logs, notifications |
|
||||
| `settingsSection` | Platform Settings | General settings, email templates, my menu |
|
||||
```python
|
||||
# app/modules/catalog/definition.py
|
||||
|
||||
### Vendor Menu Sections
|
||||
from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
| Section ID | Label | Description |
|
||||
|------------|-------|-------------|
|
||||
| `main` | (none) | Dashboard, analytics |
|
||||
| `products` | Products & Inventory | Products, inventory, marketplace import |
|
||||
| `sales` | Sales & Orders | Orders, Letzshop orders, invoices |
|
||||
| `customers` | Customers | Customers, messages, notifications |
|
||||
| `shop` | Shop & Content | Content pages, media library |
|
||||
| `account` | Account & Settings | Team, profile, billing, settings |
|
||||
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 Item Properties
|
||||
## Menu Discovery Service
|
||||
|
||||
Each menu item has these properties:
|
||||
The `MenuDiscoveryService` aggregates menus from all enabled modules:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | string | Unique identifier (used for config and access control) |
|
||||
| `label` | string | Display text |
|
||||
| `icon` | string | Heroicons icon name |
|
||||
| `url` | string | Route URL |
|
||||
| `super_admin_only` | boolean | (Optional) Only visible to super admins |
|
||||
```python
|
||||
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
|
||||
|
||||
@@ -133,23 +209,16 @@ The system uses an **opt-out model**:
|
||||
|
||||
### Mandatory Items
|
||||
|
||||
Certain menu items cannot be hidden:
|
||||
Certain menu items cannot be hidden. These are marked with `is_mandatory=True` in their definition:
|
||||
|
||||
```python
|
||||
MANDATORY_MENU_ITEMS = {
|
||||
FrontendType.ADMIN: frozenset({
|
||||
"dashboard", # Landing page
|
||||
"companies", # Platform admin core
|
||||
"vendors", # Platform admin core
|
||||
"admin-users", # Super admin core
|
||||
"settings", # Configuration access
|
||||
"my-menu", # Must be able to undo changes
|
||||
}),
|
||||
FrontendType.VENDOR: frozenset({
|
||||
"dashboard", # Landing page
|
||||
"settings", # Configuration access
|
||||
}),
|
||||
}
|
||||
MenuItemDefinition(
|
||||
id="dashboard",
|
||||
label_key="menu.dashboard",
|
||||
icon="home",
|
||||
route="/admin/dashboard",
|
||||
is_mandatory=True, # Cannot be hidden
|
||||
)
|
||||
```
|
||||
|
||||
### Scope Types
|
||||
@@ -210,6 +279,9 @@ CREATE TABLE admin_menu_configs (
|
||||
### Examples
|
||||
|
||||
```python
|
||||
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,
|
||||
@@ -237,97 +309,65 @@ AdminMenuConfig(
|
||||
|
||||
## Module Integration
|
||||
|
||||
Menu items are associated with modules. When a module is disabled for a platform, its menu items are automatically hidden.
|
||||
### Automatic Menu Discovery
|
||||
|
||||
### Module to Menu Item Mapping
|
||||
|
||||
Each module defines its menu items per frontend:
|
||||
When a module is enabled/disabled for a platform, the menu discovery service automatically includes/excludes its menu items:
|
||||
|
||||
```python
|
||||
# In module definition
|
||||
billing_module = ModuleDefinition(
|
||||
code="billing",
|
||||
menu_items={
|
||||
FrontendType.ADMIN: ["subscription-tiers", "subscriptions", "billing-history"],
|
||||
FrontendType.VENDOR: ["billing", "invoices"],
|
||||
},
|
||||
)
|
||||
```
|
||||
# Module enablement check happens automatically in MenuDiscoveryService
|
||||
enabled_modules = module_service.get_enabled_module_codes(db, platform_id)
|
||||
|
||||
### Menu Filtering Logic
|
||||
|
||||
```python
|
||||
from app.modules.service import module_service
|
||||
|
||||
# Get available menu items (module-aware)
|
||||
available_items = module_service.get_module_menu_items(
|
||||
db, platform_id, FrontendType.ADMIN
|
||||
)
|
||||
|
||||
# Check if specific menu item's module is enabled
|
||||
is_available = module_service.is_menu_item_module_enabled(
|
||||
db, platform_id, "subscription-tiers", FrontendType.ADMIN
|
||||
)
|
||||
# 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. **Registry**: All possible menu items
|
||||
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 = Registry Items
|
||||
Final Menu = Module Menu Items
|
||||
- Items from Disabled Modules
|
||||
- Items with is_visible=False in config
|
||||
- Items not matching user role (super_admin_only)
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
## Menu Service Integration
|
||||
|
||||
### Getting Menu Items
|
||||
The `MenuService` provides the interface used by templates:
|
||||
|
||||
```python
|
||||
from app.config.menu_registry import (
|
||||
get_all_menu_item_ids,
|
||||
get_menu_item,
|
||||
is_super_admin_only_item,
|
||||
ADMIN_MENU_REGISTRY,
|
||||
VENDOR_MENU_REGISTRY,
|
||||
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
|
||||
)
|
||||
|
||||
# Get all item IDs for a frontend
|
||||
admin_items = get_all_menu_item_ids(FrontendType.ADMIN)
|
||||
# Returns: {'dashboard', 'admin-users', 'companies', ...}
|
||||
|
||||
# Get specific item details
|
||||
item = get_menu_item(FrontendType.ADMIN, "subscription-tiers")
|
||||
# Returns: {
|
||||
# 'id': 'subscription-tiers',
|
||||
# 'label': 'Subscription Tiers',
|
||||
# 'icon': 'tag',
|
||||
# 'url': '/admin/subscription-tiers',
|
||||
# 'section_id': 'billing',
|
||||
# 'section_label': 'Billing & Subscriptions'
|
||||
# Returns legacy format for template compatibility:
|
||||
# {
|
||||
# "frontend_type": "admin",
|
||||
# "sections": [
|
||||
# {
|
||||
# "id": "main",
|
||||
# "label": None,
|
||||
# "items": [{"id": "dashboard", "label": "Dashboard", ...}]
|
||||
# },
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
|
||||
# Check if item requires super admin
|
||||
is_restricted = is_super_admin_only_item("admin-users") # True
|
||||
```
|
||||
|
||||
### Menu Enums
|
||||
|
||||
```python
|
||||
from app.config.menu_registry import AdminMenuItem, VendorMenuItem
|
||||
|
||||
# Use enums for type safety
|
||||
AdminMenuItem.DASHBOARD.value # "dashboard"
|
||||
AdminMenuItem.INVENTORY.value # "inventory"
|
||||
|
||||
VendorMenuItem.PRODUCTS.value # "products"
|
||||
VendorMenuItem.ANALYTICS.value # "analytics"
|
||||
```
|
||||
|
||||
## Access Control
|
||||
@@ -380,24 +420,74 @@ Located at `/admin/my-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`:
|
||||
|
||||
```python
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
2. **Add translation keys** in your module's locales:
|
||||
|
||||
```json
|
||||
// app/modules/mymodule/locales/en.json
|
||||
{
|
||||
"mymodule.menu.section": "My Module",
|
||||
"mymodule.menu.main": "Main Page"
|
||||
}
|
||||
```
|
||||
|
||||
3. **The menu is automatically discovered** - no registration needed.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Do
|
||||
|
||||
- Use menu item enums (`AdminMenuItem`, `VendorMenuItem`) for type safety
|
||||
- Check module enablement before showing module-specific menu items
|
||||
- Respect mandatory items - don't try to hide them
|
||||
- Use `require_menu_access` dependency to protect routes
|
||||
- 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 IDs as strings (use enums)
|
||||
- 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 without registering them in the registry
|
||||
- Create menu items outside of module definitions
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Module System](module-system.md) - Module architecture and menu item mapping
|
||||
- [Module System](module-system.md) - Module architecture and definitions
|
||||
- [Multi-Tenant System](multi-tenant.md) - Platform isolation
|
||||
- [Feature Gating](../implementation/feature-gating-system.md) - Tier-based access
|
||||
|
||||
Reference in New Issue
Block a user