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

@@ -35,6 +35,44 @@ from app.modules.cms.schemas.homepage_sections import (
HomepageSectionsResponse,
)
# Media schemas
from app.modules.cms.schemas.media import (
FailedFileInfo,
MediaDetailResponse,
MediaItemResponse,
MediaListResponse,
MediaMetadataUpdate,
MediaUploadResponse,
MediaUsageResponse,
MessageResponse,
MultipleUploadResponse,
OptimizationResultResponse,
ProductUsageInfo,
UploadedFileInfo,
)
# Image schemas
from app.modules.cms.schemas.image import (
ImageDeleteResponse,
ImageStorageStats,
ImageUploadResponse,
ImageUrls,
)
# Theme schemas
from app.modules.cms.schemas.vendor_theme import (
ThemeDeleteResponse,
ThemePresetListResponse,
ThemePresetPreview,
ThemePresetResponse,
VendorThemeBranding,
VendorThemeColors,
VendorThemeFonts,
VendorThemeLayout,
VendorThemeResponse,
VendorThemeUpdate,
)
__all__ = [
# Content Page - Admin
"ContentPageCreate",
@@ -60,4 +98,33 @@ __all__ = [
"HomepageSections",
"SectionUpdateRequest",
"HomepageSectionsResponse",
# Media
"FailedFileInfo",
"MediaDetailResponse",
"MediaItemResponse",
"MediaListResponse",
"MediaMetadataUpdate",
"MediaUploadResponse",
"MediaUsageResponse",
"MessageResponse",
"MultipleUploadResponse",
"OptimizationResultResponse",
"ProductUsageInfo",
"UploadedFileInfo",
# Image
"ImageDeleteResponse",
"ImageStorageStats",
"ImageUploadResponse",
"ImageUrls",
# Theme
"ThemeDeleteResponse",
"ThemePresetListResponse",
"ThemePresetPreview",
"ThemePresetResponse",
"VendorThemeBranding",
"VendorThemeColors",
"VendorThemeFonts",
"VendorThemeLayout",
"VendorThemeResponse",
"VendorThemeUpdate",
]

View File

@@ -0,0 +1,46 @@
# app/modules/cms/schemas/image.py
"""
Pydantic schemas for image operations.
"""
from pydantic import BaseModel
class ImageUrls(BaseModel):
"""URLs for image variants."""
original: str
medium: str | None = None # 800px variant
thumb: str | None = None # 200px variant
# Allow arbitrary keys for flexibility
class Config:
extra = "allow"
class ImageUploadResponse(BaseModel):
"""Response from image upload."""
success: bool
image: dict | None = None
error: str | None = None
class ImageDeleteResponse(BaseModel):
"""Response from image deletion."""
success: bool
message: str
class ImageStorageStats(BaseModel):
"""Image storage statistics."""
total_files: int
total_size_bytes: int
total_size_mb: float
total_size_gb: float
directory_count: int
max_files_per_dir: int
avg_files_per_dir: float
products_estimated: int

View File

@@ -0,0 +1,198 @@
# app/modules/cms/schemas/media.py
"""
Media/file management Pydantic schemas for API validation and responses.
This module provides schemas for:
- Media library listing
- File upload responses
- Media metadata operations
- Media usage tracking
"""
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
# ============================================================================
# SHARED RESPONSE SCHEMAS
# ============================================================================
class MessageResponse(BaseModel):
"""Generic message response for simple operations."""
message: str
# ============================================================================
# MEDIA ITEM SCHEMAS
# ============================================================================
class MediaItemResponse(BaseModel):
"""Single media item response."""
id: int
filename: str
original_filename: str | None = None
file_url: str
url: str | None = None # Alias for file_url for JS compatibility
thumbnail_url: str | None = None
media_type: str # image, video, document
mime_type: str | None = None
file_size: int | None = None # bytes
width: int | None = None # for images/videos
height: int | None = None # for images/videos
alt_text: str | None = None
description: str | None = None
folder: str | None = None
extra_metadata: dict[str, Any] | None = None
created_at: datetime
updated_at: datetime | None = None
model_config = {"from_attributes": True}
def model_post_init(self, __context: Any) -> None:
"""Set url from file_url if not provided."""
if self.url is None:
object.__setattr__(self, "url", self.file_url)
class MediaListResponse(BaseModel):
"""Paginated list of media items."""
media: list[MediaItemResponse] = []
total: int = 0
skip: int = 0
limit: int = 100
message: str | None = None
# ============================================================================
# UPLOAD RESPONSE SCHEMAS
# ============================================================================
class MediaUploadResponse(BaseModel):
"""Response for single file upload."""
success: bool = True
message: str | None = None
media: MediaItemResponse | None = None
# Legacy fields for backwards compatibility
id: int | None = None
file_url: str | None = None
thumbnail_url: str | None = None
filename: str | None = None
file_size: int | None = None
media_type: str | None = None
class UploadedFileInfo(BaseModel):
"""Information about a successfully uploaded file."""
id: int
filename: str
file_url: str
thumbnail_url: str | None = None
class FailedFileInfo(BaseModel):
"""Information about a failed file upload."""
filename: str
error: str
class MultipleUploadResponse(BaseModel):
"""Response for multiple file upload."""
uploaded_files: list[UploadedFileInfo] = []
failed_files: list[FailedFileInfo] = []
total_uploaded: int = 0
total_failed: int = 0
message: str | None = None
# ============================================================================
# MEDIA DETAIL SCHEMAS
# ============================================================================
class MediaDetailResponse(BaseModel):
"""Detailed media item response with usage info."""
id: int | None = None
filename: str | None = None
original_filename: str | None = None
file_url: str | None = None
thumbnail_url: str | None = None
media_type: str | None = None
mime_type: str | None = None
file_size: int | None = None
width: int | None = None
height: int | None = None
alt_text: str | None = None
description: str | None = None
folder: str | None = None
extra_metadata: dict[str, Any] | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
message: str | None = None
model_config = {"from_attributes": True}
# ============================================================================
# MEDIA UPDATE SCHEMAS
# ============================================================================
class MediaMetadataUpdate(BaseModel):
"""Request model for updating media metadata."""
filename: str | None = Field(None, max_length=255)
alt_text: str | None = Field(None, max_length=500)
description: str | None = None
folder: str | None = Field(None, max_length=100)
metadata: dict[str, Any] | None = None # Named 'metadata' in API, stored as 'extra_metadata'
# ============================================================================
# MEDIA USAGE SCHEMAS
# ============================================================================
class ProductUsageInfo(BaseModel):
"""Information about product using this media."""
product_id: int
product_name: str
usage_type: str # main_image, gallery, variant, etc.
class MediaUsageResponse(BaseModel):
"""Response showing where media is being used."""
media_id: int | None = None
products: list[ProductUsageInfo] = []
other_usage: list[dict[str, Any]] = []
total_usage_count: int = 0
message: str | None = None
# ============================================================================
# MEDIA OPTIMIZATION SCHEMAS
# ============================================================================
class OptimizationResultResponse(BaseModel):
"""Response for media optimization operation."""
media_id: int | None = None
original_size: int | None = None
optimized_size: int | None = None
savings_percent: float | None = None
optimized_url: str | None = None
message: str | None = None

View File

@@ -0,0 +1,108 @@
# app/modules/cms/schemas/vendor_theme.py
"""
Pydantic schemas for vendor theme operations.
"""
from pydantic import BaseModel, Field
class VendorThemeColors(BaseModel):
"""Color scheme for vendor theme."""
primary: str | None = Field(None, description="Primary brand color")
secondary: str | None = Field(None, description="Secondary color")
accent: str | None = Field(None, description="Accent/CTA color")
background: str | None = Field(None, description="Background color")
text: str | None = Field(None, description="Text color")
border: str | None = Field(None, description="Border color")
class VendorThemeFonts(BaseModel):
"""Typography settings for vendor theme."""
heading: str | None = Field(None, description="Font for headings")
body: str | None = Field(None, description="Font for body text")
class VendorThemeBranding(BaseModel):
"""Branding assets for vendor theme."""
logo: str | None = Field(None, description="Logo URL")
logo_dark: str | None = Field(None, description="Dark mode logo URL")
favicon: str | None = Field(None, description="Favicon URL")
banner: str | None = Field(None, description="Banner image URL")
class VendorThemeLayout(BaseModel):
"""Layout settings for vendor theme."""
style: str | None = Field(
None, description="Product layout style (grid, list, masonry)"
)
header: str | None = Field(
None, description="Header style (fixed, static, transparent)"
)
product_card: str | None = Field(
None, description="Product card style (modern, classic, minimal)"
)
class VendorThemeUpdate(BaseModel):
"""Schema for updating vendor theme (partial updates allowed)."""
theme_name: str | None = Field(None, description="Theme preset name")
colors: dict[str, str] | None = Field(None, description="Color scheme")
fonts: dict[str, str] | None = Field(None, description="Font settings")
branding: dict[str, str | None] | None = Field(None, description="Branding assets")
layout: dict[str, str] | None = Field(None, description="Layout settings")
custom_css: str | None = Field(None, description="Custom CSS rules")
social_links: dict[str, str] | None = Field(None, description="Social media links")
class VendorThemeResponse(BaseModel):
"""Schema for vendor theme response."""
theme_name: str = Field(..., description="Theme name")
colors: dict[str, str] = Field(..., description="Color scheme")
fonts: dict[str, str] = Field(..., description="Font settings")
branding: dict[str, str | None] = Field(..., description="Branding assets")
layout: dict[str, str] = Field(..., description="Layout settings")
social_links: dict[str, str] | None = Field(
default_factory=dict, description="Social links"
)
custom_css: str | None = Field(None, description="Custom CSS")
css_variables: dict[str, str] | None = Field(
None, description="CSS custom properties"
)
class ThemePresetPreview(BaseModel):
"""Preview information for a theme preset."""
name: str = Field(..., description="Preset name")
description: str = Field(..., description="Preset description")
primary_color: str = Field(..., description="Primary color")
secondary_color: str = Field(..., description="Secondary color")
accent_color: str = Field(..., description="Accent color")
heading_font: str = Field(..., description="Heading font")
body_font: str = Field(..., description="Body font")
layout_style: str = Field(..., description="Layout style")
class ThemePresetResponse(BaseModel):
"""Response after applying a preset."""
message: str = Field(..., description="Success message")
theme: VendorThemeResponse = Field(..., description="Applied theme")
class ThemePresetListResponse(BaseModel):
"""List of available theme presets."""
presets: list[ThemePresetPreview] = Field(..., description="Available presets")
class ThemeDeleteResponse(BaseModel):
"""Response after deleting a theme."""
message: str = Field(..., description="Success message")