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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user