This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
554 lines
18 KiB
YAML
554 lines
18 KiB
YAML
# Architecture Rules - Module Structure Rules
|
|
# Rules for app/modules/*/ directories
|
|
|
|
module_rules:
|
|
|
|
- id: "MOD-001"
|
|
name: "Self-contained modules must have required directories"
|
|
severity: "error"
|
|
description: |
|
|
When a module declares is_self_contained=True in its definition,
|
|
it must have the following directory structure:
|
|
|
|
Required directories:
|
|
- services/ - Business logic services (actual code, not re-exports)
|
|
- models/ - Database models (actual code, not re-exports)
|
|
- schemas/ - Pydantic schemas (actual code, not re-exports)
|
|
- routes/ - API and page routes
|
|
- routes/api/ - API endpoints
|
|
- routes/pages/ - Page endpoints (if module has UI)
|
|
|
|
Optional directories (based on module needs):
|
|
- templates/ - Jinja templates (if module has UI)
|
|
- static/ - Static assets (JS/CSS)
|
|
- locales/ - Translation files
|
|
- tasks/ - Celery tasks
|
|
- exceptions.py - Module-specific exceptions
|
|
|
|
WHY THIS MATTERS:
|
|
- Consistency: All modules follow the same structure
|
|
- Discoverability: Developers know where to find code
|
|
- Encapsulation: Module owns all its components
|
|
- Testability: Clear boundaries for unit tests
|
|
pattern:
|
|
directory_pattern: "app/modules/*/"
|
|
required_when: "is_self_contained=True"
|
|
required_dirs:
|
|
- "services/"
|
|
- "models/"
|
|
- "schemas/"
|
|
- "routes/"
|
|
- "routes/api/"
|
|
|
|
- id: "MOD-002"
|
|
name: "Module services must contain actual code, not re-exports"
|
|
severity: "warning"
|
|
description: |
|
|
Self-contained module services should contain actual business logic,
|
|
not just re-exports from legacy locations.
|
|
|
|
If a module's services/ directory only contains re-exports (from ...),
|
|
the module is not truly self-contained.
|
|
|
|
WRONG (re-export only):
|
|
# app/modules/analytics/services/stats_service.py
|
|
from app.services.stats_service import stats_service, StatsService
|
|
__all__ = ["stats_service", "StatsService"]
|
|
|
|
RIGHT (actual code):
|
|
# app/modules/analytics/services/stats_service.py
|
|
class StatsService:
|
|
def get_stats(self, db: Session, vendor_id: int):
|
|
# Actual implementation here
|
|
...
|
|
pattern:
|
|
file_pattern: "app/modules/*/services/*.py"
|
|
anti_patterns:
|
|
- "^from app\\.services\\."
|
|
- "^from app\\/services\\/"
|
|
|
|
- id: "MOD-003"
|
|
name: "Module schemas must contain actual code, not re-exports"
|
|
severity: "warning"
|
|
description: |
|
|
Self-contained module schemas should contain actual Pydantic models,
|
|
not just re-exports from legacy models/schema/ location.
|
|
|
|
WRONG (re-export only):
|
|
# app/modules/analytics/schemas/stats.py
|
|
from models.schema.stats import StatsResponse, ...
|
|
__all__ = ["StatsResponse", ...]
|
|
|
|
RIGHT (actual code):
|
|
# app/modules/analytics/schemas/stats.py
|
|
class StatsResponse(BaseModel):
|
|
total: int
|
|
pending: int
|
|
...
|
|
pattern:
|
|
file_pattern: "app/modules/*/schemas/*.py"
|
|
anti_patterns:
|
|
- "^from models\\.schema\\."
|
|
- "^from models\\/schema\\/"
|
|
|
|
- id: "MOD-004"
|
|
name: "Module routes must use module-internal implementations"
|
|
severity: "warning"
|
|
description: |
|
|
Module routes should import services from within the module,
|
|
not from legacy app/services/ locations.
|
|
|
|
WRONG:
|
|
from app.services.stats_service import stats_service
|
|
|
|
RIGHT:
|
|
from app.modules.analytics.services import stats_service
|
|
# or
|
|
from ..services import stats_service
|
|
pattern:
|
|
file_pattern: "app/modules/*/routes/**/*.py"
|
|
anti_patterns:
|
|
- "from app\\.services\\."
|
|
|
|
- id: "MOD-005"
|
|
name: "Modules with UI must have templates and static directories"
|
|
severity: "warning"
|
|
description: |
|
|
Modules that define menu_items (have UI pages) should have:
|
|
- templates/{module_code}/admin/ or templates/{module_code}/vendor/
|
|
- static/admin/js/ or static/vendor/js/
|
|
|
|
This ensures:
|
|
- UI is self-contained within the module
|
|
- Templates are namespaced to avoid conflicts
|
|
- JavaScript can be module-specific
|
|
pattern:
|
|
directory_pattern: "app/modules/*/"
|
|
required_when: "has_menu_items=True"
|
|
required_dirs:
|
|
- "templates/"
|
|
- "static/"
|
|
|
|
- id: "MOD-006"
|
|
name: "Module locales should exist for internationalization"
|
|
severity: "info"
|
|
description: |
|
|
Self-contained modules should have a locales/ directory with
|
|
translation files for internationalization.
|
|
|
|
Structure:
|
|
app/modules/{module}/locales/
|
|
en.json
|
|
de.json
|
|
fr.json
|
|
lu.json
|
|
|
|
Translation keys are namespaced as {module}.key_name
|
|
pattern:
|
|
directory_pattern: "app/modules/*/"
|
|
suggested_dirs:
|
|
- "locales/"
|
|
|
|
- id: "MOD-008"
|
|
name: "Self-contained modules must have exceptions.py"
|
|
severity: "warning"
|
|
description: |
|
|
Self-contained modules should have an exceptions.py file defining
|
|
module-specific exceptions that inherit from WizamartException.
|
|
|
|
Structure:
|
|
app/modules/{module}/exceptions.py
|
|
|
|
Example:
|
|
# app/modules/analytics/exceptions.py
|
|
from app.exceptions import WizamartException
|
|
|
|
class AnalyticsException(WizamartException):
|
|
"""Base exception for analytics module."""
|
|
pass
|
|
|
|
class ReportGenerationError(AnalyticsException):
|
|
"""Error generating analytics report."""
|
|
pass
|
|
|
|
WHY THIS MATTERS:
|
|
- Encapsulation: Module owns its exception hierarchy
|
|
- Clarity: Clear which exceptions belong to which module
|
|
- Testability: Easy to mock module-specific exceptions
|
|
pattern:
|
|
directory_pattern: "app/modules/*/"
|
|
required_when: "is_self_contained=True"
|
|
required_files:
|
|
- "exceptions.py"
|
|
|
|
# =========================================================================
|
|
# Auto-Discovery Rules
|
|
# =========================================================================
|
|
|
|
- id: "MOD-009"
|
|
name: "Module must have definition.py for auto-discovery"
|
|
severity: "error"
|
|
description: |
|
|
Every module directory must have a definition.py file containing
|
|
a ModuleDefinition instance for auto-discovery.
|
|
|
|
The framework auto-discovers modules from app/modules/*/definition.py.
|
|
Without this file, the module won't be registered.
|
|
|
|
Required:
|
|
app/modules/<code>/definition.py
|
|
|
|
The definition.py must export a ModuleDefinition instance:
|
|
# app/modules/analytics/definition.py
|
|
from app.modules.base import ModuleDefinition
|
|
|
|
analytics_module = ModuleDefinition(
|
|
code="analytics",
|
|
name="Analytics & Reporting",
|
|
...
|
|
)
|
|
pattern:
|
|
directory_pattern: "app/modules/*/"
|
|
required_files:
|
|
- "definition.py"
|
|
- "__init__.py"
|
|
|
|
- id: "MOD-010"
|
|
name: "Module routes must export router variable for auto-discovery"
|
|
severity: "warning"
|
|
description: |
|
|
Route files (admin.py, vendor.py, shop.py) in routes/api/ and routes/pages/
|
|
must export a 'router' variable for auto-discovery.
|
|
|
|
The route discovery system looks for:
|
|
- routes/api/admin.py with 'router' variable
|
|
- routes/api/vendor.py with 'router' variable
|
|
- routes/pages/vendor.py with 'router' variable
|
|
|
|
Example:
|
|
# app/modules/analytics/routes/pages/vendor.py
|
|
from fastapi import APIRouter
|
|
|
|
router = APIRouter() # Must be named 'router'
|
|
|
|
@router.get("/analytics")
|
|
def analytics_page():
|
|
...
|
|
pattern:
|
|
file_pattern: "app/modules/*/routes/*/*.py"
|
|
required_exports:
|
|
- "router"
|
|
|
|
- id: "MOD-011"
|
|
name: "Module tasks must have __init__.py for Celery discovery"
|
|
severity: "warning"
|
|
description: |
|
|
If a module has a tasks/ directory, it must have __init__.py
|
|
for Celery task auto-discovery to work.
|
|
|
|
Celery uses autodiscover_tasks() which requires the tasks
|
|
package to be importable.
|
|
|
|
Required structure:
|
|
app/modules/<code>/tasks/
|
|
├── __init__.py <- Required for discovery
|
|
└── some_task.py
|
|
|
|
The __init__.py should import task functions:
|
|
# app/modules/billing/tasks/__init__.py
|
|
from app.modules.billing.tasks.subscription import reset_period_counters
|
|
pattern:
|
|
directory_pattern: "app/modules/*/tasks/"
|
|
required_files:
|
|
- "__init__.py"
|
|
|
|
- id: "MOD-012"
|
|
name: "Module locales should have all supported language files"
|
|
severity: "info"
|
|
description: |
|
|
Module locales/ directory should have translation files for
|
|
all supported languages to ensure consistent i18n.
|
|
|
|
Supported languages: en, de, fr, lu
|
|
|
|
Structure:
|
|
app/modules/<code>/locales/
|
|
├── en.json
|
|
├── de.json
|
|
├── fr.json
|
|
└── lu.json
|
|
|
|
Missing translations will fall back to English, but it's
|
|
better to have all languages covered.
|
|
pattern:
|
|
directory_pattern: "app/modules/*/locales/"
|
|
suggested_files:
|
|
- "en.json"
|
|
- "de.json"
|
|
- "fr.json"
|
|
- "lu.json"
|
|
|
|
- id: "MOD-007"
|
|
name: "Module definition must match directory structure"
|
|
severity: "error"
|
|
description: |
|
|
If a module definition specifies paths like services_path, models_path,
|
|
etc., those directories must exist and contain __init__.py files.
|
|
|
|
Example: If definition.py has:
|
|
services_path="app.modules.analytics.services"
|
|
|
|
Then these must exist:
|
|
app/modules/analytics/services/
|
|
app/modules/analytics/services/__init__.py
|
|
pattern:
|
|
file_pattern: "app/modules/*/definition.py"
|
|
validates:
|
|
- "services_path -> services/__init__.py"
|
|
- "models_path -> models/__init__.py"
|
|
- "schemas_path -> schemas/__init__.py"
|
|
- "exceptions_path -> exceptions.py or exceptions/__init__.py"
|
|
- "templates_path -> templates/"
|
|
- "locales_path -> locales/"
|
|
|
|
# =========================================================================
|
|
# Self-Contained Config & Migrations Rules
|
|
# =========================================================================
|
|
|
|
- id: "MOD-013"
|
|
name: "Module config.py for environment-based configuration"
|
|
severity: "info"
|
|
description: |
|
|
Self-contained modules can have a config.py file for environment-based
|
|
configuration using Pydantic Settings.
|
|
|
|
Structure:
|
|
app/modules/<code>/config.py
|
|
|
|
Pattern:
|
|
# app/modules/marketplace/config.py
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings
|
|
|
|
class MarketplaceConfig(BaseSettings):
|
|
'''Configuration for marketplace module.'''
|
|
|
|
# Settings prefixed with MARKETPLACE_ in environment
|
|
api_timeout: int = Field(default=30, description="API timeout")
|
|
batch_size: int = Field(default=100, description="Import batch size")
|
|
|
|
model_config = {"env_prefix": "MARKETPLACE_"}
|
|
|
|
# Export for auto-discovery
|
|
config_class = MarketplaceConfig
|
|
config = MarketplaceConfig()
|
|
|
|
The config is auto-discovered by app/modules/config.py and can be
|
|
accessed via get_module_config("marketplace").
|
|
|
|
WHY THIS MATTERS:
|
|
- Encapsulation: Module owns its configuration
|
|
- Environment: Settings loaded from environment variables
|
|
- Validation: Pydantic validates configuration on startup
|
|
- Defaults: Sensible defaults in code, overridable via env
|
|
pattern:
|
|
directory_pattern: "app/modules/*/"
|
|
suggested_files:
|
|
- "config.py"
|
|
|
|
- id: "MOD-014"
|
|
name: "Module migrations must follow naming convention"
|
|
severity: "warning"
|
|
description: |
|
|
If a module has migrations, they must:
|
|
1. Be located in migrations/versions/ directory
|
|
2. Follow the naming convention: {module_code}_{sequence}_{description}.py
|
|
|
|
Structure:
|
|
app/modules/<code>/migrations/
|
|
├── __init__.py
|
|
└── versions/
|
|
├── __init__.py
|
|
├── <code>_001_initial.py
|
|
├── <code>_002_add_feature.py
|
|
└── ...
|
|
|
|
Example for cms module:
|
|
app/modules/cms/migrations/versions/
|
|
├── cms_001_create_content_pages.py
|
|
├── cms_002_add_sections.py
|
|
└── cms_003_add_media_library.py
|
|
|
|
Migration file must include:
|
|
revision = "cms_001"
|
|
down_revision = None # or previous revision
|
|
branch_labels = ("cms",) # Module-specific branch
|
|
|
|
WHY THIS MATTERS:
|
|
- Isolation: Module migrations don't conflict with core
|
|
- Ordering: Sequence numbers ensure correct order
|
|
- Traceability: Clear which module owns each migration
|
|
- Rollback: Can rollback module migrations independently
|
|
pattern:
|
|
directory_pattern: "app/modules/*/migrations/versions/"
|
|
file_pattern: "{module_code}_*.py"
|
|
|
|
- id: "MOD-015"
|
|
name: "Module migrations directory must have __init__.py files"
|
|
severity: "warning"
|
|
description: |
|
|
If a module has a migrations/ directory, both the migrations/
|
|
and migrations/versions/ directories must have __init__.py files
|
|
for proper Python package discovery.
|
|
|
|
Required structure:
|
|
app/modules/<code>/migrations/
|
|
├── __init__.py <- Required
|
|
└── versions/
|
|
└── __init__.py <- Required
|
|
|
|
The __init__.py files can be empty or contain docstrings:
|
|
# migrations/__init__.py
|
|
'''Module migrations.'''
|
|
|
|
WHY THIS MATTERS:
|
|
- Alembic needs to import migration scripts as Python modules
|
|
- Without __init__.py, the directories are not Python packages
|
|
pattern:
|
|
directory_pattern: "app/modules/*/migrations/"
|
|
required_files:
|
|
- "__init__.py"
|
|
- "versions/__init__.py"
|
|
|
|
# =========================================================================
|
|
# Legacy Location Rules (Auto-Discovery Enforcement)
|
|
# =========================================================================
|
|
|
|
- id: "MOD-016"
|
|
name: "Routes must be in modules, not app/api/v1/"
|
|
severity: "error"
|
|
description: |
|
|
All API routes must be defined in module directories, not in legacy
|
|
app/api/v1/vendor/ or app/api/v1/admin/ locations.
|
|
|
|
WRONG (legacy location):
|
|
app/api/v1/vendor/orders.py
|
|
app/api/v1/admin/orders.py
|
|
|
|
RIGHT (module location):
|
|
app/modules/orders/routes/api/vendor.py
|
|
app/modules/orders/routes/api/admin.py
|
|
|
|
Routes in modules are auto-discovered and registered. Legacy routes
|
|
require manual registration and don't follow module patterns.
|
|
|
|
EXCEPTIONS (allowed in legacy):
|
|
- __init__.py (router aggregation)
|
|
- auth.py (core authentication - will move to tenancy)
|
|
- Files with # noqa: mod-016 comment
|
|
|
|
WHY THIS MATTERS:
|
|
- Auto-discovery: Module routes are automatically registered
|
|
- Encapsulation: Routes belong with their domain logic
|
|
- Consistency: All modules follow the same pattern
|
|
- Maintainability: Easier to understand module boundaries
|
|
pattern:
|
|
prohibited_locations:
|
|
- "app/api/v1/vendor/*.py"
|
|
- "app/api/v1/admin/*.py"
|
|
exceptions:
|
|
- "__init__.py"
|
|
- "auth.py"
|
|
|
|
- id: "MOD-017"
|
|
name: "Services must be in modules, not app/services/"
|
|
severity: "error"
|
|
description: |
|
|
All business logic services must be defined in module directories,
|
|
not in the legacy app/services/ location.
|
|
|
|
WRONG (legacy location):
|
|
app/services/order_service.py
|
|
|
|
RIGHT (module location):
|
|
app/modules/orders/services/order_service.py
|
|
|
|
EXCEPTIONS (allowed in legacy):
|
|
- __init__.py (re-exports for backwards compatibility)
|
|
- Files that are pure re-exports from modules
|
|
- Files with # noqa: mod-017 comment
|
|
|
|
WHY THIS MATTERS:
|
|
- Encapsulation: Services belong with their domain
|
|
- Clear boundaries: Know which module owns which service
|
|
- Testability: Can test modules in isolation
|
|
- Refactoring: Easier to move/rename modules
|
|
pattern:
|
|
prohibited_locations:
|
|
- "app/services/*.py"
|
|
exceptions:
|
|
- "__init__.py"
|
|
|
|
- id: "MOD-018"
|
|
name: "Tasks must be in modules, not app/tasks/"
|
|
severity: "error"
|
|
description: |
|
|
All Celery background tasks must be defined in module directories,
|
|
not in the legacy app/tasks/ location.
|
|
|
|
WRONG (legacy location):
|
|
app/tasks/subscription_tasks.py
|
|
|
|
RIGHT (module location):
|
|
app/modules/billing/tasks/subscription.py
|
|
|
|
The module tasks/ directory must have __init__.py for Celery
|
|
autodiscovery to work.
|
|
|
|
EXCEPTIONS (allowed in legacy):
|
|
- __init__.py (Celery app configuration)
|
|
- dispatcher.py (task routing infrastructure)
|
|
- Files with # noqa: mod-018 comment
|
|
|
|
WHY THIS MATTERS:
|
|
- Auto-discovery: Celery finds tasks from module directories
|
|
- Encapsulation: Tasks belong with their domain logic
|
|
- Consistency: All async operations in one place per module
|
|
pattern:
|
|
prohibited_locations:
|
|
- "app/tasks/*.py"
|
|
exceptions:
|
|
- "__init__.py"
|
|
- "dispatcher.py"
|
|
|
|
- id: "MOD-019"
|
|
name: "Schemas must be in modules, not models/schema/"
|
|
severity: "error"
|
|
description: |
|
|
All Pydantic schemas must be defined in module directories,
|
|
not in the legacy models/schema/ location.
|
|
|
|
WRONG (legacy location):
|
|
models/schema/order.py
|
|
|
|
RIGHT (module location):
|
|
app/modules/orders/schemas/order.py
|
|
|
|
EXCEPTIONS (allowed in legacy):
|
|
- __init__.py (re-exports for backwards compatibility)
|
|
- base.py (base schema classes - infrastructure)
|
|
- auth.py (core authentication schemas - cross-cutting)
|
|
- Files with # noqa: mod-019 comment
|
|
|
|
WHY THIS MATTERS:
|
|
- Encapsulation: Schemas belong with their domain
|
|
- Co-location: Request/response schemas near route handlers
|
|
- Clear ownership: Know which module owns which schema
|
|
pattern:
|
|
prohibited_locations:
|
|
- "models/schema/*.py"
|
|
exceptions:
|
|
- "__init__.py"
|
|
- "base.py"
|
|
- "auth.py"
|