refactor: migrate remaining routes to modules and enforce auto-discovery

MIGRATION:
- Delete app/api/v1/vendor/analytics.py (duplicate - analytics module already auto-discovered)
- Move usage routes from app/api/v1/vendor/usage.py to billing module
- Move onboarding routes from app/api/v1/vendor/onboarding.py to marketplace module
- Move features routes to billing module (admin + vendor)
- Move inventory routes to inventory module (admin + vendor)
- Move marketplace/letzshop routes to marketplace module
- Move orders routes to orders module
- Delete legacy letzshop service files (moved to marketplace module)

DOCUMENTATION:
- Add docs/development/migration/module-autodiscovery-migration.md with full migration history
- Update docs/architecture/module-system.md with Entity Auto-Discovery Reference section
- Add detailed sections for each entity type: routes, services, models, schemas, tasks,
  exceptions, templates, static files, locales, configuration

ARCHITECTURE VALIDATION:
- Add MOD-016: Routes must be in modules, not app/api/v1/
- Add MOD-017: Services must be in modules, not app/services/
- Add MOD-018: Tasks must be in modules, not app/tasks/
- Add MOD-019: Schemas must be in modules, not models/schema/
- Update scripts/validate_architecture.py with _validate_legacy_locations method
- Update .architecture-rules/module.yaml with legacy location rules

These rules enforce that all entities must be in self-contained modules.
Legacy locations now trigger ERROR severity violations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 14:25:59 +01:00
parent e2cecff014
commit 401db56258
52 changed files with 1160 additions and 4968 deletions

View File

@@ -224,6 +224,9 @@ class ArchitectureValidator:
# Validate module structure
self._validate_modules(target)
# Validate legacy locations (must be in modules)
self._validate_legacy_locations(target)
return self.result
def validate_file(self, file_path: Path, quiet: bool = False) -> ValidationResult:
@@ -4348,6 +4351,193 @@ class ArchitectureValidator:
suggestion="Create 'exceptions.py' or 'exceptions/__init__.py'",
)
def _validate_legacy_locations(self, target_path: Path):
"""
Validate that code is not in legacy locations (MOD-016 to MOD-019).
All routes, services, tasks, and schemas should be in module directories,
not in the legacy centralized locations.
"""
print("🚫 Checking legacy locations...")
# MOD-016: Routes must be in modules, not app/api/v1/
self._check_legacy_routes(target_path)
# MOD-017: Services must be in modules, not app/services/
self._check_legacy_services(target_path)
# MOD-018: Tasks must be in modules, not app/tasks/
self._check_legacy_tasks(target_path)
# MOD-019: Schemas must be in modules, not models/schema/
self._check_legacy_schemas(target_path)
def _check_legacy_routes(self, target_path: Path):
"""MOD-016: Check for routes in legacy app/api/v1/ locations."""
# Check vendor routes
vendor_api_path = target_path / "app" / "api" / "v1" / "vendor"
if vendor_api_path.exists():
for py_file in vendor_api_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
# Allow auth.py for now (core authentication)
if py_file.name == "auth.py":
continue
# Check for noqa comment
content = py_file.read_text()
if "noqa: mod-016" in content.lower():
continue
self._add_violation(
rule_id="MOD-016",
rule_name="Routes must be in modules, not app/api/v1/",
severity=Severity.ERROR,
file_path=py_file,
line_number=1,
message=f"Route file '{py_file.name}' in legacy location - should be in module",
context="app/api/v1/vendor/",
suggestion="Move to app/modules/{module}/routes/api/vendor.py",
)
# Check admin routes
admin_api_path = target_path / "app" / "api" / "v1" / "admin"
if admin_api_path.exists():
for py_file in admin_api_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
# Allow auth.py for now (core authentication)
if py_file.name == "auth.py":
continue
# Check for noqa comment
content = py_file.read_text()
if "noqa: mod-016" in content.lower():
continue
self._add_violation(
rule_id="MOD-016",
rule_name="Routes must be in modules, not app/api/v1/",
severity=Severity.ERROR,
file_path=py_file,
line_number=1,
message=f"Route file '{py_file.name}' in legacy location - should be in module",
context="app/api/v1/admin/",
suggestion="Move to app/modules/{module}/routes/api/admin.py",
)
def _check_legacy_services(self, target_path: Path):
"""MOD-017: Check for services in legacy app/services/ location."""
services_path = target_path / "app" / "services"
if not services_path.exists():
return
for py_file in services_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
# Check for noqa comment
content = py_file.read_text()
if "noqa: mod-017" in content.lower():
continue
# Check if file is a pure re-export (only imports, no class/def)
lines = content.split("\n")
has_definitions = any(
re.match(r"^(class|def|async def)\s+\w+", line)
for line in lines
)
# If it's a re-export only file, it's a warning not error
if not has_definitions:
# Check if it imports from modules
imports_from_module = "from app.modules." in content
if imports_from_module:
# Re-export from module - this is acceptable during migration
continue
self._add_violation(
rule_id="MOD-017",
rule_name="Services must be in modules, not app/services/",
severity=Severity.ERROR,
file_path=py_file,
line_number=1,
message=f"Service file '{py_file.name}' in legacy location - should be in module",
context="app/services/",
suggestion="Move to app/modules/{module}/services/",
)
def _check_legacy_tasks(self, target_path: Path):
"""MOD-018: Check for tasks in legacy app/tasks/ location."""
tasks_path = target_path / "app" / "tasks"
if not tasks_path.exists():
return
for py_file in tasks_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
# Allow dispatcher.py (infrastructure)
if py_file.name == "dispatcher.py":
continue
# Check for noqa comment
content = py_file.read_text()
if "noqa: mod-018" in content.lower():
continue
self._add_violation(
rule_id="MOD-018",
rule_name="Tasks must be in modules, not app/tasks/",
severity=Severity.ERROR,
file_path=py_file,
line_number=1,
message=f"Task file '{py_file.name}' in legacy location - should be in module",
context="app/tasks/",
suggestion="Move to app/modules/{module}/tasks/",
)
def _check_legacy_schemas(self, target_path: Path):
"""MOD-019: Check for schemas in legacy models/schema/ location."""
schemas_path = target_path / "models" / "schema"
if not schemas_path.exists():
return
for py_file in schemas_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
# Allow auth.py (core authentication schemas)
if py_file.name == "auth.py":
continue
# Check for noqa comment
content = py_file.read_text()
if "noqa: mod-019" in content.lower():
continue
# Check if file is a pure re-export
lines = content.split("\n")
has_definitions = any(
re.match(r"^class\s+\w+", line)
for line in lines
)
# If it's a re-export only file, allow it during migration
if not has_definitions:
imports_from_module = "from app.modules." in content
if imports_from_module:
continue
self._add_violation(
rule_id="MOD-019",
rule_name="Schemas must be in modules, not models/schema/",
severity=Severity.ERROR,
file_path=py_file,
line_number=1,
message=f"Schema file '{py_file.name}' in legacy location - should be in module",
context="models/schema/",
suggestion="Move to app/modules/{module}/schemas/",
)
def _get_rule(self, rule_id: str) -> dict[str, Any]:
"""Get rule configuration by ID"""
# Look in different rule categories