chore: update API routers, validation, and docs for module system

- app/api/v1/admin/__init__.py: Updated router imports
- app/api/v1/vendor/__init__.py: Updated router imports
- app/exceptions/code_quality.py: Added module exception imports
- scripts/validate_architecture.py: Added module validation rules
- .architecture-rules/_main.yaml: Include module.yaml rules
- docs/proposals/module-migration-plan.md: Updated migration status

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 22:22:43 +01:00
parent bf871dc9f9
commit fbcf07914e
6 changed files with 465 additions and 14 deletions

View File

@@ -48,6 +48,7 @@ includes:
- quality.yaml - quality.yaml
- money.yaml - money.yaml
- migration.yaml - migration.yaml
- module.yaml
# ============================================================================ # ============================================================================
# VALIDATION SEVERITY LEVELS # VALIDATION SEVERITY LEVELS

View File

@@ -82,12 +82,12 @@ from . import (
# Import extracted module routers # Import extracted module routers
# NOTE: Import directly from admin.py files to avoid circular imports through __init__.py # NOTE: Import directly from admin.py files to avoid circular imports through __init__.py
from app.modules.billing.routes.admin import admin_router as billing_admin_router from app.modules.billing.routes.api.admin import admin_router as billing_admin_router
from app.modules.inventory.routes.admin import admin_router as inventory_admin_router from app.modules.inventory.routes.admin import admin_router as inventory_admin_router
from app.modules.orders.routes.admin import admin_router as orders_admin_router from app.modules.orders.routes.admin import admin_router as orders_admin_router
from app.modules.orders.routes.admin import admin_exceptions_router as orders_exceptions_router from app.modules.orders.routes.admin import admin_exceptions_router as orders_exceptions_router
from app.modules.marketplace.routes.admin import admin_router as marketplace_admin_router from app.modules.marketplace.routes.api.admin import admin_router as marketplace_admin_router
from app.modules.marketplace.routes.admin import admin_letzshop_router as letzshop_admin_router from app.modules.marketplace.routes.api.admin import admin_letzshop_router as letzshop_admin_router
# CMS module router # CMS module router
from app.modules.cms.routes.api.admin import router as cms_admin_router from app.modules.cms.routes.api.admin import router as cms_admin_router

View File

@@ -61,12 +61,12 @@ from . import (
# Import extracted module routers # Import extracted module routers
# NOTE: Import directly from vendor.py files to avoid circular imports through __init__.py # NOTE: Import directly from vendor.py files to avoid circular imports through __init__.py
from app.modules.billing.routes.vendor import vendor_router as billing_vendor_router from app.modules.billing.routes.api.vendor import vendor_router as billing_vendor_router
from app.modules.inventory.routes.vendor import vendor_router as inventory_vendor_router from app.modules.inventory.routes.vendor import vendor_router as inventory_vendor_router
from app.modules.orders.routes.vendor import vendor_router as orders_vendor_router from app.modules.orders.routes.vendor import vendor_router as orders_vendor_router
from app.modules.orders.routes.vendor import vendor_exceptions_router as orders_exceptions_router from app.modules.orders.routes.vendor import vendor_exceptions_router as orders_exceptions_router
from app.modules.marketplace.routes.vendor import vendor_router as marketplace_vendor_router from app.modules.marketplace.routes.api.vendor import vendor_router as marketplace_vendor_router
from app.modules.marketplace.routes.vendor import vendor_letzshop_router as letzshop_vendor_router from app.modules.marketplace.routes.api.vendor import vendor_letzshop_router as letzshop_vendor_router
# CMS module router # CMS module router
from app.modules.cms.routes.api.vendor import router as cms_vendor_router from app.modules.cms.routes.api.vendor import router as cms_vendor_router

View File

@@ -4,6 +4,9 @@ Code Quality Domain Exceptions
These exceptions are raised by the code quality service layer These exceptions are raised by the code quality service layer
and converted to HTTP responses by the global exception handler. and converted to HTTP responses by the global exception handler.
Note: These exceptions are also re-exported from app.modules.dev_tools.exceptions
for module-based access.
""" """
from app.exceptions.base import ( from app.exceptions.base import (

View File

@@ -2,7 +2,7 @@
**Status:** In Progress **Status:** In Progress
**Started:** 2026-01-25 **Started:** 2026-01-25
**Last Updated:** 2026-01-27 **Last Updated:** 2026-01-28
**Target:** v1.0.0 **Target:** v1.0.0
This is the unified migration plan for transforming Wizamart into a fully modular architecture. This is the unified migration plan for transforming Wizamart into a fully modular architecture.
@@ -31,13 +31,13 @@ Transform the platform from a monolithic structure to self-contained modules whe
| `payments` | Optional | 🟡 Partial | ✅ | ✅ | - | Done | | `payments` | Optional | 🟡 Partial | ✅ | ✅ | - | Done |
| `billing` | Optional | ✅ **Complete** | ✅ | ✅ | ✅ | Done | | `billing` | Optional | ✅ **Complete** | ✅ | ✅ | ✅ | Done |
| `marketplace` | Optional | ✅ **Complete** | ✅ | ✅ | ✅ | Done | | `marketplace` | Optional | ✅ **Complete** | ✅ | ✅ | ✅ | Done |
| `orders` | Optional | 🔴 Shell | | | - | Full | | `orders` | Optional | **Complete** | | | - | Done |
| `inventory` | Optional | 🔴 Shell | | | - | Full | | `inventory` | Optional | **Complete** | | | - | Done |
| `customers` | Core | 🔴 Shell | | | - | Full | | `customers` | Core | **Complete** | | | - | Done |
| `analytics` | Optional | 🔴 Shell | | | - | Full | | `analytics` | Optional | **Complete** | | - | - | Done |
| `messaging` | Optional | 🔴 Shell | | | - | Full | | `messaging` | Optional | **Complete** | | | - | Done |
| `monitoring` | Internal | 🔴 Shell | | | | Full | | `monitoring` | Internal | **Complete** | | | - | Done |
| `dev-tools` | Internal | 🔴 Shell | | | | Full | | `dev-tools` | Internal | **Complete** | | | | Done |
| `tenancy` | Core | 🔴 Shell | ❌ | ❌ | - | Full | | `tenancy` | Core | 🔴 Shell | ❌ | ❌ | - | Full |
| `core` | Core | 🔴 Shell | ❌ | ❌ | - | Minimal | | `core` | Core | 🔴 Shell | ❌ | ❌ | - | Minimal |
@@ -140,6 +140,69 @@ app/tasks/celery_tasks/ # → Move to respective modules
- Updated legacy task files to re-export from new location - Updated legacy task files to re-export from new location
- Removed marketplace/letzshop/export from LEGACY_TASK_MODULES - Removed marketplace/letzshop/export from LEGACY_TASK_MODULES
### ✅ Phase 7: Dev-Tools Module Migration
- Created `app/modules/dev_tools/models/` with:
- `architecture_scan.py` - ArchitectureScan, ArchitectureViolation, ArchitectureRule, etc.
- `test_run.py` - TestRun, TestResult, TestCollection
- Created `app/modules/dev_tools/services/` re-exporting code_quality_service, test_runner_service
- Created `app/modules/dev_tools/schemas/` (placeholder for future schemas)
- Created `app/modules/dev_tools/tasks/` with:
- `code_quality.py` - execute_code_quality_scan
- `test_runner.py` - execute_test_run
- Created `app/modules/dev_tools/exceptions.py` with test runner exceptions
- Created `app/modules/dev_tools/routes/api/` with admin router
- Updated `definition.py` with self-contained configuration
- Updated legacy model files to re-export from module location
- Updated legacy task files to re-export from module location
- Used lazy imports in `__init__.py` to avoid circular import issues
### ✅ Phase 8: Monitoring Module Migration
- Created `app/modules/monitoring/models/` re-exporting CapacitySnapshot, AdminNotification
- Created `app/modules/monitoring/services/` re-exporting BackgroundTasksService
- Created `app/modules/monitoring/schemas/` (placeholder)
- Created `app/modules/monitoring/exceptions.py` with monitoring-specific exceptions
- Updated `definition.py` with self-contained configuration
- Used lazy imports in `__init__.py` to avoid circular import issues
### ✅ Phase 9: Customers Module Migration
- Created `app/modules/customers/models/` re-exporting Customer, CustomerAddress, PasswordResetToken
- Created `app/modules/customers/services/` re-exporting customer services
- Created `app/modules/customers/schemas/` re-exporting customer schemas
- Created `app/modules/customers/exceptions.py` re-exporting customer exceptions
- Updated `definition.py` with self-contained configuration
- Used lazy imports in `__init__.py` to avoid circular import issues
### ✅ Phase 10: Orders Module Migration
- Created `app/modules/orders/models/` re-exporting Order, OrderItem, Invoice, etc.
- Created `app/modules/orders/services/` re-exporting order and invoice services
- Created `app/modules/orders/schemas/` re-exporting order and invoice schemas
- Created `app/modules/orders/exceptions.py` re-exporting order exceptions
- Updated `definition.py` with self-contained configuration
- Used lazy imports in `__init__.py` to avoid circular import issues
### ✅ Phase 11: Inventory Module Migration
- Created `app/modules/inventory/models/` re-exporting Inventory, InventoryTransaction
- Created `app/modules/inventory/services/` re-exporting inventory services
- Created `app/modules/inventory/schemas/` re-exporting inventory schemas
- Created `app/modules/inventory/exceptions.py` re-exporting inventory exceptions
- Updated `definition.py` with self-contained configuration
- Used lazy imports in `__init__.py` to avoid circular import issues
### ✅ Phase 12: Analytics & Messaging Module Migration
- **Analytics Module:**
- Created `app/modules/analytics/models/` (empty - uses data from other modules)
- Created `app/modules/analytics/services/` re-exporting StatsService, UsageService
- Created `app/modules/analytics/schemas/` re-exporting stats schemas
- Created `app/modules/analytics/exceptions.py` with reporting exceptions
- Updated `definition.py` with self-contained configuration
- **Messaging Module:**
- Created `app/modules/messaging/models/` re-exporting Conversation, Message, etc.
- Created `app/modules/messaging/services/` re-exporting messaging services
- Created `app/modules/messaging/schemas/` re-exporting messaging schemas
- Created `app/modules/messaging/exceptions.py` re-exporting message exceptions
- Updated `definition.py` with self-contained configuration
--- ---
## Module Migration Phases ## Module Migration Phases

View File

@@ -221,6 +221,9 @@ class ArchitectureValidator:
# Validate language/i18n rules # Validate language/i18n rules
self._validate_language_rules(target) self._validate_language_rules(target)
# Validate module structure
self._validate_modules(target)
return self.result return self.result
def validate_file(self, file_path: Path, quiet: bool = False) -> ValidationResult: def validate_file(self, file_path: Path, quiet: bool = False) -> ValidationResult:
@@ -3906,6 +3909,386 @@ class ArchitectureValidator:
suggestion="Specify the constraint name to drop", suggestion="Specify the constraint name to drop",
) )
def _validate_modules(self, target_path: Path):
"""Validate module structure rules (MOD-001 to MOD-012)"""
print("📦 Validating module structure...")
modules_path = target_path / "app" / "modules"
if not modules_path.exists():
return
# Get all module directories (excluding __pycache__ and special files)
module_dirs = [
d for d in modules_path.iterdir()
if d.is_dir() and d.name != "__pycache__" and not d.name.startswith(".")
]
for module_dir in module_dirs:
module_name = module_dir.name
definition_file = module_dir / "definition.py"
init_file = module_dir / "__init__.py"
# MOD-009: Check for definition.py (required for auto-discovery)
if not definition_file.exists():
# Only report if it looks like a module (has __init__.py or other .py files)
has_py_files = any(f.suffix == ".py" for f in module_dir.iterdir() if f.is_file())
if has_py_files or init_file.exists():
self._add_violation(
rule_id="MOD-009",
rule_name="Module must have definition.py for auto-discovery",
severity=Severity.ERROR,
file_path=module_dir / "__init__.py" if init_file.exists() else module_dir,
line_number=1,
message=f"Module '{module_name}' missing definition.py - won't be auto-discovered",
context="Module directory exists but no definition.py",
suggestion="Create definition.py with ModuleDefinition instance",
)
continue
# Check for __init__.py
if not init_file.exists():
self._add_violation(
rule_id="MOD-009",
rule_name="Module must have definition.py for auto-discovery",
severity=Severity.ERROR,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' missing __init__.py",
context="definition.py exists but no __init__.py",
suggestion="Create __init__.py to make module importable",
)
# Read definition to check if is_self_contained
definition_content = definition_file.read_text()
is_self_contained = "is_self_contained=True" in definition_content or "is_self_contained = True" in definition_content
if not is_self_contained:
continue
# MOD-001: Check required directories for self-contained modules
required_dirs = ["services", "models", "schemas", "routes"]
for req_dir in required_dirs:
dir_path = module_dir / req_dir
if not dir_path.exists():
self._add_violation(
rule_id="MOD-001",
rule_name="Self-contained modules must have required directories",
severity=Severity.ERROR,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' is self-contained but missing '{req_dir}/' directory",
context=f"is_self_contained=True",
suggestion=f"Create '{req_dir}/' directory with __init__.py",
)
elif req_dir != "routes":
# Check for __init__.py
init_file = dir_path / "__init__.py"
if not init_file.exists():
self._add_violation(
rule_id="MOD-001",
rule_name="Self-contained modules must have required directories",
severity=Severity.ERROR,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' missing '{req_dir}/__init__.py'",
context=f"is_self_contained=True",
suggestion=f"Create '{req_dir}/__init__.py' with exports",
)
# Check routes/api/ exists
routes_api_path = module_dir / "routes" / "api"
if not routes_api_path.exists():
self._add_violation(
rule_id="MOD-001",
rule_name="Self-contained modules must have required directories",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' is self-contained but missing 'routes/api/' directory",
context=f"is_self_contained=True",
suggestion="Create 'routes/api/' directory for API endpoints",
)
# MOD-002: Check services contain actual code, not re-exports
services_dir = module_dir / "services"
if services_dir.exists():
self._check_module_contains_actual_code(
services_dir, module_name, "services", "MOD-002",
["from app.services.", "from app/services/"]
)
# MOD-003: Check schemas contain actual code, not re-exports
schemas_dir = module_dir / "schemas"
if schemas_dir.exists():
self._check_module_contains_actual_code(
schemas_dir, module_name, "schemas", "MOD-003",
["from models.schema.", "from models/schema/"]
)
# MOD-004: Check routes import from module, not legacy locations
routes_dir = module_dir / "routes"
if routes_dir.exists():
for route_file in routes_dir.rglob("*.py"):
if route_file.name == "__init__.py":
continue
content = route_file.read_text()
lines = content.split("\n")
for i, line in enumerate(lines, 1):
if "from app.services." in line:
self._add_violation(
rule_id="MOD-004",
rule_name="Module routes must use module-internal implementations",
severity=Severity.WARNING,
file_path=route_file,
line_number=i,
message=f"Route imports from legacy 'app.services' instead of module services",
context=line.strip()[:80],
suggestion=f"Import from 'app.modules.{module_name}.services' or '..services'",
)
# MOD-005: Check for templates/static if module has UI (menu_items)
has_menu_items = "menu_items" in definition_content and "FrontendType." in definition_content
# Check if menu_items has actual entries (not just empty dict)
has_actual_menu = (
has_menu_items and
not re.search(r"menu_items\s*=\s*\{\s*\}", definition_content) and
('"' in definition_content[definition_content.find("menu_items"):definition_content.find("menu_items")+200] if "menu_items" in definition_content else False)
)
if has_actual_menu:
templates_dir = module_dir / "templates"
static_dir = module_dir / "static"
if not templates_dir.exists():
self._add_violation(
rule_id="MOD-005",
rule_name="Modules with UI must have templates and static directories",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' has menu_items but missing 'templates/' directory",
context="has_menu_items=True",
suggestion=f"Create 'templates/{module_name}/admin/' and/or 'templates/{module_name}/vendor/'",
)
if not static_dir.exists():
self._add_violation(
rule_id="MOD-005",
rule_name="Modules with UI must have templates and static directories",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' has menu_items but missing 'static/' directory",
context="has_menu_items=True",
suggestion="Create 'static/admin/js/' and/or 'static/vendor/js/'",
)
# MOD-006: Check for locales (info level)
locales_dir = module_dir / "locales"
if not locales_dir.exists():
self._add_violation(
rule_id="MOD-006",
rule_name="Module locales should exist for internationalization",
severity=Severity.INFO,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' is missing 'locales/' directory for translations",
context="is_self_contained=True",
suggestion="Create 'locales/' with en.json, de.json, fr.json, lu.json",
)
# MOD-008: Check for exceptions.py
exceptions_file = module_dir / "exceptions.py"
exceptions_pkg = module_dir / "exceptions" / "__init__.py"
if not exceptions_file.exists() and not exceptions_pkg.exists():
self._add_violation(
rule_id="MOD-008",
rule_name="Self-contained modules must have exceptions.py",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' is missing 'exceptions.py' for module-specific exceptions",
context="is_self_contained=True",
suggestion="Create 'exceptions.py' with module-specific exception classes",
)
# MOD-010: Check route files export 'router' variable
self._check_route_exports(module_dir, module_name)
# MOD-011: Check tasks directory has __init__.py
tasks_dir = module_dir / "tasks"
if tasks_dir.exists():
tasks_init = tasks_dir / "__init__.py"
if not tasks_init.exists():
self._add_violation(
rule_id="MOD-011",
rule_name="Module tasks must have __init__.py for Celery discovery",
severity=Severity.WARNING,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' has tasks/ but missing tasks/__init__.py",
context="tasks/ directory exists",
suggestion="Create 'tasks/__init__.py' for Celery auto-discovery",
)
# MOD-012: Check locales has all language files
if locales_dir.exists():
required_langs = ["en.json", "de.json", "fr.json", "lu.json"]
missing_langs = [lang for lang in required_langs if not (locales_dir / lang).exists()]
if missing_langs:
self._add_violation(
rule_id="MOD-012",
rule_name="Module locales should have all supported language files",
severity=Severity.INFO,
file_path=definition_file,
line_number=1,
message=f"Module '{module_name}' missing locale files: {', '.join(missing_langs)}",
context="locales/ directory exists",
suggestion=f"Add translation files: {', '.join(missing_langs)}",
)
# MOD-007: Validate definition paths match directory structure
self._validate_module_definition_paths(module_dir, module_name, definition_file, definition_content)
def _check_module_contains_actual_code(
self,
dir_path: Path,
module_name: str,
dir_type: str,
rule_id: str,
anti_patterns: list[str]
):
"""Check if module directory contains actual code vs re-exports"""
py_files = list(dir_path.glob("*.py"))
# Track if we found any file with actual code (not just re-exports)
has_actual_code = False
for py_file in py_files:
if py_file.name == "__init__.py":
continue
content = py_file.read_text()
lines = content.split("\n")
# Check if file has actual class/function definitions
has_definitions = any(
re.match(r"^(class|def|async def)\s+\w+", line)
for line in lines
)
# Check if file only has re-exports
has_reexport = any(
any(pattern in line for pattern in anti_patterns)
for line in lines
)
if has_definitions and not has_reexport:
has_actual_code = True
elif has_reexport and not has_definitions:
# File is a re-export only
for i, line in enumerate(lines, 1):
for pattern in anti_patterns:
if pattern in line:
self._add_violation(
rule_id=rule_id,
rule_name=f"Module {dir_type} must contain actual code, not re-exports",
severity=Severity.WARNING,
file_path=py_file,
line_number=i,
message=f"File re-exports from legacy location instead of containing actual code",
context=line.strip()[:80],
suggestion=f"Move actual code to this file and update legacy to re-export from here",
)
break
def _check_route_exports(self, module_dir: Path, module_name: str):
"""MOD-010: Check that route files export 'router' variable for auto-discovery."""
routes_dir = module_dir / "routes"
if not routes_dir.exists():
return
# Check api and pages subdirectories
for route_type in ["api", "pages"]:
type_dir = routes_dir / route_type
if not type_dir.exists():
continue
for route_file in ["admin.py", "vendor.py", "shop.py"]:
file_path = type_dir / route_file
if not file_path.exists():
continue
content = file_path.read_text()
# Check for 'router' variable definition
has_router = (
"router = APIRouter" in content or
"router: APIRouter" in content or
"\nrouter = " in content
)
if not has_router:
self._add_violation(
rule_id="MOD-010",
rule_name="Module routes must export router variable for auto-discovery",
severity=Severity.WARNING,
file_path=file_path,
line_number=1,
message=f"Route file missing 'router' variable for auto-discovery",
context=f"routes/{route_type}/{route_file}",
suggestion="Add: router = APIRouter() and use @router.get/post decorators",
)
def _validate_module_definition_paths(
self,
module_dir: Path,
module_name: str,
definition_file: Path,
definition_content: str
):
"""MOD-007: Validate that paths in definition match actual directories"""
path_checks = [
("services_path", "services/__init__.py"),
("models_path", "models/__init__.py"),
("schemas_path", "schemas/__init__.py"),
("templates_path", "templates"),
("locales_path", "locales"),
]
for path_attr, expected_path in path_checks:
# Check if path is defined in definition
if f'{path_attr}="' in definition_content or f"{path_attr}='" in definition_content:
expected_full_path = module_dir / expected_path
if not expected_full_path.exists():
self._add_violation(
rule_id="MOD-007",
rule_name="Module definition must match directory structure",
severity=Severity.ERROR,
file_path=definition_file,
line_number=1,
message=f"Module definition specifies '{path_attr}' but '{expected_path}' does not exist",
context=f"{path_attr}=app.modules.{module_name}.*",
suggestion=f"Create '{expected_path}' or remove '{path_attr}' from definition",
)
# Check exceptions_path specially (can be .py or package)
if 'exceptions_path="' in definition_content or "exceptions_path='" in definition_content:
exceptions_file = module_dir / "exceptions.py"
exceptions_pkg = module_dir / "exceptions" / "__init__.py"
if not exceptions_file.exists() and not exceptions_pkg.exists():
self._add_violation(
rule_id="MOD-007",
rule_name="Module definition must match directory structure",
severity=Severity.ERROR,
file_path=definition_file,
line_number=1,
message=f"Module definition specifies 'exceptions_path' but no exceptions module exists",
context=f"exceptions_path=app.modules.{module_name}.exceptions",
suggestion="Create 'exceptions.py' or 'exceptions/__init__.py'",
)
def _get_rule(self, rule_id: str) -> dict[str, Any]: def _get_rule(self, rule_id: str) -> dict[str, Any]:
"""Get rule configuration by ID""" """Get rule configuration by ID"""
# Look in different rule categories # Look in different rule categories
@@ -3919,6 +4302,7 @@ class ArchitectureValidator:
"frontend_component_rules", "frontend_component_rules",
"language_rules", "language_rules",
"migration_rules", "migration_rules",
"module_rules",
]: ]:
rules = self.config.get(category, []) rules = self.config.get(category, [])
for rule in rules: for rule in rules: