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:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -47,6 +47,93 @@ if TYPE_CHECKING:
from app.modules.enums import FrontendType
# =============================================================================
# Menu Item Definitions
# =============================================================================
@dataclass
class MenuItemDefinition:
"""
Definition of a single menu item within a section.
Attributes:
id: Unique identifier (e.g., "catalog.products", "orders.list")
label_key: i18n key for the menu item label
icon: Lucide icon name (e.g., "box", "shopping-cart")
route: URL path (can include placeholders like {vendor_code})
order: Sort order within section (lower = higher priority)
is_mandatory: If True, cannot be hidden by user preferences
requires_permission: Permission code required to see this item
badge_source: Key for dynamic badge count (e.g., "pending_orders_count")
is_super_admin_only: Only visible to super admins (admin frontend only)
Example:
MenuItemDefinition(
id="catalog.products",
label_key="catalog.menu.products",
icon="box",
route="/admin/catalog/products",
order=10,
is_mandatory=True
)
"""
id: str
label_key: str
icon: str
route: str
order: int = 100
is_mandatory: bool = False
requires_permission: str | None = None
badge_source: str | None = None
is_super_admin_only: bool = False
@dataclass
class MenuSectionDefinition:
"""
Definition of a menu section containing related menu items.
Sections group related menu items together in the sidebar.
A section can be collapsed/expanded by the user.
Attributes:
id: Unique section identifier (e.g., "catalog", "orders")
label_key: i18n key for section header (None for headerless sections)
icon: Lucide icon name for section (optional)
order: Sort order among sections (lower = higher priority)
items: List of menu items in this section
is_super_admin_only: Only visible to super admins
is_collapsible: Whether section can be collapsed
Example:
MenuSectionDefinition(
id="catalog",
label_key="catalog.menu.section",
icon="package",
order=20,
items=[
MenuItemDefinition(
id="catalog.products",
label_key="catalog.menu.products",
icon="box",
route="/admin/catalog/products",
order=10
),
]
)
"""
id: str
label_key: str | None
icon: str | None = None
order: int = 100
items: list[MenuItemDefinition] = field(default_factory=list)
is_super_admin_only: bool = False
is_collapsible: bool = True
@dataclass
class ScheduledTask:
"""
@@ -190,6 +277,14 @@ class ModuleDefinition:
menu_items: dict[FrontendType, list[str]] = field(default_factory=dict)
permissions: list[str] = field(default_factory=list)
# =========================================================================
# Menu Definitions (Module-Driven Menus)
# =========================================================================
# NEW: Full menu definitions per frontend type. When set, these take
# precedence over menu_items for menu rendering. This enables modules
# to fully define their own menu structure with icons, routes, and labels.
menus: dict[FrontendType, list[MenuSectionDefinition]] = field(default_factory=dict)
# =========================================================================
# Classification
# =========================================================================
@@ -235,15 +330,15 @@ class ModuleDefinition:
scheduled_tasks: list[ScheduledTask] = field(default_factory=list)
# =========================================================================
# Menu Item Methods
# Menu Item Methods (Legacy - uses menu_items dict of IDs)
# =========================================================================
def get_menu_items(self, frontend_type: FrontendType) -> list[str]:
"""Get menu item IDs for a specific frontend type."""
"""Get menu item IDs for a specific frontend type (legacy)."""
return self.menu_items.get(frontend_type, [])
def get_all_menu_items(self) -> set[str]:
"""Get all menu item IDs across all frontend types."""
"""Get all menu item IDs across all frontend types (legacy)."""
all_items = set()
for items in self.menu_items.values():
all_items.update(items)
@@ -253,6 +348,50 @@ class ModuleDefinition:
"""Check if this module provides a specific menu item."""
return menu_item_id in self.get_all_menu_items()
# =========================================================================
# Menu Definition Methods (New - uses menus dict of full definitions)
# =========================================================================
def get_menu_sections(self, frontend_type: FrontendType) -> list[MenuSectionDefinition]:
"""
Get menu section definitions for a specific frontend type.
Args:
frontend_type: The frontend type to get menus for
Returns:
List of MenuSectionDefinition objects, sorted by order
"""
sections = self.menus.get(frontend_type, [])
return sorted(sections, key=lambda s: s.order)
def get_all_menu_definitions(self) -> dict[FrontendType, list[MenuSectionDefinition]]:
"""
Get all menu definitions for all frontend types.
Returns:
Dict mapping FrontendType to list of MenuSectionDefinition
"""
return self.menus
def has_menus_for_frontend(self, frontend_type: FrontendType) -> bool:
"""Check if this module has menu definitions for a frontend type."""
return frontend_type in self.menus and len(self.menus[frontend_type]) > 0
def get_mandatory_menu_item_ids(self, frontend_type: FrontendType) -> set[str]:
"""
Get IDs of all mandatory menu items for a frontend type.
Returns:
Set of menu item IDs that are marked as is_mandatory=True
"""
mandatory_ids = set()
for section in self.menus.get(frontend_type, []):
for item in section.items:
if item.is_mandatory:
mandatory_ids.add(item.id)
return mandatory_ids
# =========================================================================
# Feature Methods
# =========================================================================