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

@@ -499,6 +499,381 @@ Currently, all migrations reside in central `alembic/versions/`. The module-spec
- **New modules**: Should create migrations in their own `migrations/versions/`
- **Future reorganization**: Existing migrations will be moved to modules pre-production
## Entity Auto-Discovery Reference
This section details the auto-discovery requirements for each entity type. **All entities must be in modules** - legacy locations are deprecated and will trigger architecture validation errors.
### Routes
Routes define API and page endpoints. They are auto-discovered from module directories.
| Type | Location | Discovery | Router Name |
|------|----------|-----------|-------------|
| Vendor API | `routes/api/vendor.py` | `app/modules/routes.py` | `vendor_router` |
| Admin API | `routes/api/admin.py` | `app/modules/routes.py` | `admin_router` |
| Shop API | `routes/api/shop.py` | `app/modules/routes.py` | `shop_router` |
| Vendor Pages | `routes/pages/vendor.py` | `app/modules/routes.py` | `vendor_router` |
| Admin Pages | `routes/pages/admin.py` | `app/modules/routes.py` | `admin_router` |
**Structure:**
```
app/modules/{module}/routes/
├── __init__.py
├── api/
│ ├── __init__.py
│ ├── vendor.py # Must export vendor_router
│ ├── admin.py # Must export admin_router
│ └── vendor_{feature}.py # Sub-routers aggregated in vendor.py
└── pages/
├── __init__.py
└── vendor.py # Must export vendor_router
```
**Example - Aggregating Sub-Routers:**
```python
# app/modules/billing/routes/api/vendor.py
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
vendor_router = APIRouter(
prefix="/billing",
dependencies=[Depends(require_module_access("billing"))],
)
# Aggregate sub-routers
from .vendor_checkout import vendor_checkout_router
from .vendor_usage import vendor_usage_router
vendor_router.include_router(vendor_checkout_router)
vendor_router.include_router(vendor_usage_router)
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `app/api/v1/vendor/*.py` - Move to module `routes/api/vendor.py`
- `app/api/v1/admin/*.py` - Move to module `routes/api/admin.py`
---
### Services
Services contain business logic. They are not auto-discovered but should be in modules for organization.
| Location | Import Pattern |
|----------|----------------|
| `services/*.py` | `from app.modules.{module}.services import service_name` |
| `services/__init__.py` | Re-exports all public services |
**Structure:**
```
app/modules/{module}/services/
├── __init__.py # Re-exports: from .order_service import order_service
├── order_service.py # OrderService class + order_service singleton
└── fulfillment_service.py # Related services
```
**Example:**
```python
# app/modules/orders/services/order_service.py
from sqlalchemy.orm import Session
from app.modules.orders.models import Order
class OrderService:
def get_order(self, db: Session, order_id: int) -> Order:
return db.query(Order).filter(Order.id == order_id).first()
order_service = OrderService()
# app/modules/orders/services/__init__.py
from .order_service import order_service, OrderService
__all__ = ["order_service", "OrderService"]
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `app/services/*.py` - Move to module `services/`
- `app/services/{module}/` - Move to `app/modules/{module}/services/`
---
### Models
Database models (SQLAlchemy). Currently in `models/database/`, migrating to modules.
| Location | Base Class | Discovery |
|----------|------------|-----------|
| `models/*.py` | `Base` from `models.base` | Alembic autogenerate |
**Structure:**
```
app/modules/{module}/models/
├── __init__.py # Re-exports: from .order import Order, OrderItem
├── order.py # Order model
└── order_item.py # Related models
```
**Example:**
```python
# app/modules/orders/models/order.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.base import Base, TimestampMixin
class Order(Base, TimestampMixin):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
status = Column(String(50), default="pending")
items = relationship("OrderItem", back_populates="order")
```
**Legacy Locations (being migrated):**
- `models/database/*.py` - Core models remain here, domain models move to modules
---
### Schemas
Pydantic schemas for request/response validation.
| Location | Base Class | Usage |
|----------|------------|-------|
| `schemas/*.py` | `BaseModel` from Pydantic | API routes, validation |
**Structure:**
```
app/modules/{module}/schemas/
├── __init__.py # Re-exports all schemas
├── order.py # Order request/response schemas
└── order_item.py # Related schemas
```
**Example:**
```python
# app/modules/orders/schemas/order.py
from pydantic import BaseModel
from datetime import datetime
class OrderResponse(BaseModel):
id: int
vendor_id: int
status: str
created_at: datetime
class Config:
from_attributes = True
class OrderCreateRequest(BaseModel):
customer_id: int
items: list[OrderItemRequest]
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `models/schema/*.py` - Move to module `schemas/`
---
### Tasks (Celery)
Background tasks are auto-discovered by Celery from module `tasks/` directories.
| Location | Discovery | Registration |
|----------|-----------|--------------|
| `tasks/*.py` | `app/modules/tasks.py` | Celery autodiscover |
**Structure:**
```
app/modules/{module}/tasks/
├── __init__.py # REQUIRED - imports task functions
├── import_tasks.py # Task definitions
└── export_tasks.py # Related tasks
```
**Example:**
```python
# app/modules/marketplace/tasks/import_tasks.py
from celery import shared_task
from app.core.database import SessionLocal
@shared_task(bind=True)
def process_import(self, job_id: int, vendor_id: int):
db = SessionLocal()
try:
# Process import
pass
finally:
db.close()
# app/modules/marketplace/tasks/__init__.py
from .import_tasks import process_import
from .export_tasks import export_products
__all__ = ["process_import", "export_products"]
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `app/tasks/*.py` - Move to module `tasks/`
---
### Exceptions
Module-specific exceptions inherit from `WizamartException`.
| Location | Base Class | Usage |
|----------|------------|-------|
| `exceptions.py` | `WizamartException` | Domain errors |
**Structure:**
```
app/modules/{module}/
└── exceptions.py # All module exceptions
```
**Example:**
```python
# app/modules/orders/exceptions.py
from app.exceptions import WizamartException
class OrderException(WizamartException):
"""Base exception for orders module."""
pass
class OrderNotFoundError(OrderException):
"""Order not found."""
def __init__(self, order_id: int):
super().__init__(f"Order {order_id} not found")
self.order_id = order_id
class OrderAlreadyFulfilledError(OrderException):
"""Order has already been fulfilled."""
pass
```
---
### Templates
Jinja2 templates are auto-discovered from module `templates/` directories.
| Location | URL Pattern | Discovery |
|----------|-------------|-----------|
| `templates/{module}/vendor/*.html` | `/vendor/{vendor}/...` | Jinja2 loader |
| `templates/{module}/admin/*.html` | `/admin/...` | Jinja2 loader |
**Structure:**
```
app/modules/{module}/templates/
└── {module}/
├── vendor/
│ ├── index.html
│ └── detail.html
└── admin/
└── list.html
```
**Template Reference:**
```python
# In route
return templates.TemplateResponse(
request=request,
name="{module}/vendor/index.html",
context={"items": items}
)
```
---
### Static Files
JavaScript, CSS, and images are auto-mounted from module `static/` directories.
| Location | URL | Discovery |
|----------|-----|-----------|
| `static/vendor/js/*.js` | `/static/modules/{module}/vendor/js/*.js` | `main.py` |
| `static/admin/js/*.js` | `/static/modules/{module}/admin/js/*.js` | `main.py` |
**Structure:**
```
app/modules/{module}/static/
├── vendor/js/
│ └── {module}.js
├── admin/js/
│ └── {module}.js
└── shared/js/
└── common.js
```
**Template Reference:**
```html
<script src="{{ url_for('{module}_static', path='vendor/js/{module}.js') }}"></script>
```
---
### Locales (i18n)
Translation files are auto-discovered from module `locales/` directories.
| Location | Format | Discovery |
|----------|--------|-----------|
| `locales/*.json` | JSON key-value | `app/utils/i18n.py` |
**Structure:**
```
app/modules/{module}/locales/
├── en.json
├── de.json
├── fr.json
└── lb.json
```
**Example:**
```json
{
"orders.title": "Orders",
"orders.status.pending": "Pending",
"orders.status.fulfilled": "Fulfilled"
}
```
**Usage:**
```python
from app.utils.i18n import t
message = t("orders.title", locale="en") # "Orders"
```
---
### Configuration
Module-specific environment configuration.
| Location | Base Class | Discovery |
|----------|------------|-----------|
| `config.py` | `BaseSettings` | `app/modules/config.py` |
**Example:**
```python
# app/modules/marketplace/config.py
from pydantic_settings import BaseSettings
class MarketplaceConfig(BaseSettings):
api_timeout: int = 30
batch_size: int = 100
model_config = {"env_prefix": "MARKETPLACE_"}
config = MarketplaceConfig()
```
**Environment Variables:**
```bash
MARKETPLACE_API_TIMEOUT=60
MARKETPLACE_BATCH_SIZE=500
```
## Architecture Validation Rules
The architecture validator (`scripts/validate_architecture.py`) enforces module structure:
@@ -520,6 +895,10 @@ The architecture validator (`scripts/validate_architecture.py`) enforces module
| MOD-013 | INFO | config.py should export `config` or `config_class` |
| MOD-014 | WARNING | Migrations must follow naming convention |
| MOD-015 | WARNING | Migrations directory must have `__init__.py` files |
| MOD-016 | ERROR | Routes must be in modules, not `app/api/v1/` |
| MOD-017 | ERROR | Services must be in modules, not `app/services/` |
| MOD-018 | ERROR | Tasks must be in modules, not `app/tasks/` |
| MOD-019 | ERROR | Schemas must be in modules, not `models/schema/` |
Run validation:
```bash