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
- money.yaml
- migration.yaml
- module.yaml
# ============================================================================
# VALIDATION SEVERITY LEVELS

View File

@@ -82,12 +82,12 @@ from . import (
# Import extracted module routers
# 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.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.marketplace.routes.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_router as marketplace_admin_router
from app.modules.marketplace.routes.api.admin import admin_letzshop_router as letzshop_admin_router
# CMS module 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
# 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.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.marketplace.routes.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_router as marketplace_vendor_router
from app.modules.marketplace.routes.api.vendor import vendor_letzshop_router as letzshop_vendor_router
# CMS module 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
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 (

View File

@@ -2,7 +2,7 @@
**Status:** In Progress
**Started:** 2026-01-25
**Last Updated:** 2026-01-27
**Last Updated:** 2026-01-28
**Target:** v1.0.0
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 |
| `billing` | Optional | ✅ **Complete** | ✅ | ✅ | ✅ | Done |
| `marketplace` | Optional | ✅ **Complete** | ✅ | ✅ | ✅ | Done |
| `orders` | Optional | 🔴 Shell | | | - | Full |
| `inventory` | Optional | 🔴 Shell | | | - | Full |
| `customers` | Core | 🔴 Shell | | | - | Full |
| `analytics` | Optional | 🔴 Shell | | | - | Full |
| `messaging` | Optional | 🔴 Shell | | | - | Full |
| `monitoring` | Internal | 🔴 Shell | | | | Full |
| `dev-tools` | Internal | 🔴 Shell | | | | Full |
| `orders` | Optional | **Complete** | | | - | Done |
| `inventory` | Optional | **Complete** | | | - | Done |
| `customers` | Core | **Complete** | | | - | Done |
| `analytics` | Optional | **Complete** | | - | - | Done |
| `messaging` | Optional | **Complete** | | | - | Done |
| `monitoring` | Internal | **Complete** | | | - | Done |
| `dev-tools` | Internal | **Complete** | | | | Done |
| `tenancy` | Core | 🔴 Shell | ❌ | ❌ | - | Full |
| `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
- 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

View File

@@ -221,6 +221,9 @@ class ArchitectureValidator:
# Validate language/i18n rules
self._validate_language_rules(target)
# Validate module structure
self._validate_modules(target)
return self.result
def validate_file(self, file_path: Path, quiet: bool = False) -> ValidationResult:
@@ -3906,6 +3909,386 @@ class ArchitectureValidator:
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]:
"""Get rule configuration by ID"""
# Look in different rule categories
@@ -3919,6 +4302,7 @@ class ArchitectureValidator:
"frontend_component_rules",
"language_rules",
"migration_rules",
"module_rules",
]:
rules = self.config.get(category, [])
for rule in rules: