refactor: complete module-driven architecture migration

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>
This commit is contained in:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -28,7 +28,7 @@ from app.modules.tenancy.exceptions import (
UserNotActiveException,
)
from middleware.auth import AuthManager
from models.database.user import User
from app.modules.tenancy.models import User
@pytest.mark.unit

View File

@@ -8,7 +8,7 @@ Tests the admin-platform junction table model and its relationships.
import pytest
from sqlalchemy.exc import IntegrityError
from models.database.admin_platform import AdminPlatform
from app.modules.tenancy.models import AdminPlatform
@pytest.mark.unit
@@ -68,7 +68,7 @@ class TestAdminPlatformModel:
self, db, auth_manager, test_platform, test_super_admin
):
"""Test that deleting user cascades to admin platform assignments."""
from models.database.user import User
from app.modules.tenancy.models import User
# Create a temporary admin
temp_admin = User(

View File

@@ -3,7 +3,7 @@
import pytest
from models.database.vendor import Role, Vendor, VendorUser
from app.modules.tenancy.models import Role, Vendor, VendorUser
@pytest.mark.unit

View File

@@ -4,7 +4,7 @@
import pytest
from sqlalchemy.exc import IntegrityError
from models.database.user import User
from app.modules.tenancy.models import User
@pytest.mark.unit

View File

@@ -4,7 +4,7 @@
import pytest
from sqlalchemy.exc import IntegrityError
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
@pytest.mark.unit

View File

@@ -4,7 +4,7 @@
import pytest
from pydantic import ValidationError
from models.schema.vendor import (
from app.modules.tenancy.schemas.vendor import (
VendorCreate,
VendorDetailResponse,
VendorListResponse,

View File

@@ -16,7 +16,7 @@ from app.modules.messaging.services.admin_notification_service import (
Severity,
)
from app.modules.messaging.models import AdminNotification
from models.database.admin import PlatformAlert
from app.modules.tenancy.models import PlatformAlert
@pytest.fixture

View File

@@ -123,7 +123,7 @@ class TestAdminPlatformServiceAssign:
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test reactivating an inactive assignment."""
from models.database.admin_platform import AdminPlatform
from app.modules.tenancy.models import AdminPlatform
service = AdminPlatformService()
@@ -157,7 +157,7 @@ class TestAdminPlatformServiceRemove:
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test successfully removing an admin from a platform."""
from models.database.admin_platform import AdminPlatform
from app.modules.tenancy.models import AdminPlatform
service = AdminPlatformService()
@@ -208,7 +208,7 @@ class TestAdminPlatformServiceQueries:
self, db, test_platform_admin, test_platform, another_platform, test_super_admin
):
"""Test getting platforms for an admin."""
from models.database.admin_platform import AdminPlatform
from app.modules.tenancy.models import AdminPlatform
service = AdminPlatformService()
@@ -242,8 +242,8 @@ class TestAdminPlatformServiceQueries:
self, db, test_platform_admin, test_platform, test_super_admin, auth_manager
):
"""Test getting admins for a platform."""
from models.database.admin_platform import AdminPlatform
from models.database.user import User
from app.modules.tenancy.models import AdminPlatform
from app.modules.tenancy.models import User
service = AdminPlatformService()
@@ -281,7 +281,7 @@ class TestAdminPlatformServiceQueries:
self, db, test_platform_admin, test_platform, another_platform, test_super_admin
):
"""Test getting admin assignments with platform details."""
from models.database.admin_platform import AdminPlatform
from app.modules.tenancy.models import AdminPlatform
service = AdminPlatformService()
@@ -330,7 +330,7 @@ class TestAdminPlatformServiceSuperAdmin:
self, db, test_super_admin, auth_manager
):
"""Test demoting super admin to platform admin."""
from models.database.user import User
from app.modules.tenancy.models import User
service = AdminPlatformService()

View File

@@ -12,7 +12,7 @@ from app.modules.tenancy.exceptions import (
)
from app.modules.tenancy.services.admin_service import AdminService
from app.modules.analytics.services.stats_service import stats_service
from models.schema.vendor import VendorCreate
from app.modules.tenancy.schemas.vendor import VendorCreate
@pytest.mark.unit
@@ -56,7 +56,7 @@ class TestAdminService:
def test_toggle_user_status_activate(self, db, test_user, test_admin):
"""Test activating a user"""
from models.database.user import User
from app.modules.tenancy.models import User
# Re-query user to get fresh instance
user_to_deactivate = db.query(User).filter(User.id == test_user.id).first()
@@ -122,7 +122,7 @@ class TestAdminService:
def test_verify_vendor_mark_verified(self, db, test_vendor):
"""Test marking vendor as verified"""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
# Re-query vendor to get fresh instance
vendor_to_unverify = (
@@ -373,7 +373,7 @@ class TestAdminServiceVendorCreation:
assert vendor.vendor_code == vendor_data.vendor_code.upper()
# Verify platform assignment
from models.database.vendor_platform import VendorPlatform
from app.modules.tenancy.models import VendorPlatform
assignment = (
db.query(VendorPlatform)
@@ -408,7 +408,7 @@ class TestAdminServiceVendorCreation:
assert vendor is not None
# Verify both platform assignments
from models.database.vendor_platform import VendorPlatform
from app.modules.tenancy.models import VendorPlatform
assignments = (
db.query(VendorPlatform)
@@ -442,7 +442,7 @@ class TestAdminServiceVendorCreation:
assert vendor is not None
# Verify no platform assignments created
from models.database.vendor_platform import VendorPlatform
from app.modules.tenancy.models import VendorPlatform
assignments = (
db.query(VendorPlatform)

View File

@@ -77,7 +77,7 @@ class TestAuthService:
def test_login_user_inactive_user(self, db, test_user):
"""Test login fails for inactive user."""
from models.database.user import User
from app.modules.tenancy.models import User
# Re-query user and deactivate
user = db.query(User).filter(User.id == test_user.id).first()

View File

@@ -330,7 +330,7 @@ class TestContentPageServiceVendorMethods:
self, db, vendor_about_page, other_company
):
"""Test updating vendor page with wrong vendor raises exception."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
# Create another vendor
unique_id = str(uuid.uuid4())[:8]
@@ -374,7 +374,7 @@ class TestContentPageServiceVendorMethods:
self, db, vendor_about_page, other_company
):
"""Test deleting vendor page with wrong vendor raises exception."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
# Create another vendor
unique_id = str(uuid.uuid4())[:8]

View File

@@ -13,7 +13,7 @@ from app.modules.messaging.services.email_service import (
SMTPProvider,
get_provider,
)
from models.database.email import EmailCategory, EmailLog, EmailStatus, EmailTemplate
from app.modules.messaging.models import EmailCategory, EmailLog, EmailStatus, EmailTemplate
@pytest.mark.unit

View File

@@ -486,7 +486,7 @@ class TestInventoryService:
def test_update_inventory_wrong_vendor(self, db, test_inventory, other_company):
"""Test updating inventory from wrong vendor raises InventoryNotFoundException."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
unique_id = str(uuid.uuid4())[:8]
other_vendor = Vendor(
@@ -527,7 +527,7 @@ class TestInventoryService:
def test_delete_inventory_wrong_vendor(self, db, test_inventory, other_company):
"""Test deleting inventory from wrong vendor raises InventoryNotFoundException."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
unique_id = str(uuid.uuid4())[:8]
other_vendor = Vendor(
@@ -735,7 +735,7 @@ class TestInventoryService:
self, db, test_product, other_company
):
"""Test _get_vendor_product raises for wrong vendor."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
unique_id = str(uuid.uuid4())[:8]
other_vendor = Vendor(

View File

@@ -158,7 +158,7 @@ class TestMarketplaceImportJobService:
self, db, test_marketplace_import_job, other_user, other_company
):
"""Test getting import job for wrong vendor."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
# Create another vendor
unique_id = str(uuid.uuid4())[:8]
@@ -241,7 +241,7 @@ class TestMarketplaceImportJobService:
def test_get_import_jobs_empty(self, db, test_user, other_user, other_company):
"""Test getting import jobs when none exist."""
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
# Create a vendor with no jobs
unique_id = str(uuid.uuid4())[:8]

View File

@@ -19,7 +19,7 @@ from app.modules.registry import (
validate_module_dependencies,
)
from app.modules.service import ModuleService
from models.database.admin_menu_config import FrontendType
from app.modules.enums import FrontendType
@pytest.mark.unit

View File

@@ -17,7 +17,7 @@ import pytest
from app.exceptions import ValidationException
from app.modules.tenancy.services.team_service import TeamService, team_service
from models.database.vendor import Role, VendorUser
from app.modules.tenancy.models import Role, VendorUser
@pytest.mark.unit

View File

@@ -6,7 +6,7 @@ import pytest
from app.modules.analytics.services.usage_service import UsageService, usage_service
from app.modules.catalog.models import Product
from app.modules.billing.models import SubscriptionTier, VendorSubscription
from models.database.vendor import VendorUser
from app.modules.tenancy.models import VendorUser
@pytest.mark.unit
@@ -285,7 +285,7 @@ def test_vendor_with_products(db, test_vendor_with_subscription, marketplace_pro
@pytest.fixture
def test_vendor_with_team(db, test_vendor_with_subscription, test_user, other_user):
"""Create vendor with team members (owner + team member = 2)."""
from models.database.vendor import VendorUserType
from app.modules.tenancy.models import VendorUserType
# Add owner
owner = VendorUser(

View File

@@ -11,7 +11,8 @@ from app.exceptions import (
ValidationException,
)
from app.modules.cms.services.vendor_email_settings_service import VendorEmailSettingsService
from models.database import VendorEmailSettings, TierCode
from app.modules.messaging.models import VendorEmailSettings
from app.modules.billing.models import TierCode
# =============================================================================

View File

@@ -15,10 +15,10 @@ from app.modules.tenancy.exceptions import (
from app.modules.marketplace.exceptions import MarketplaceProductNotFoundException
from app.modules.catalog.exceptions import ProductAlreadyExistsException
from app.modules.tenancy.services.vendor_service import VendorService
from models.database.company import Company
from models.database.vendor import Vendor
from app.modules.tenancy.models import Company
from app.modules.tenancy.models import Vendor
from app.modules.catalog.schemas import ProductCreate
from models.schema.vendor import VendorCreate
from app.modules.tenancy.schemas.vendor import VendorCreate
@pytest.fixture
@@ -582,7 +582,7 @@ class TestVendorServicePermissions:
def test_can_update_vendor_non_owner(self, db, other_company, test_vendor):
"""Test non-owner cannot update vendor."""
from models.database.user import User
from app.modules.tenancy.models import User
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
@@ -598,7 +598,7 @@ class TestVendorServicePermissions:
def test_is_vendor_owner_false(self, db, other_company, test_vendor):
"""Test _is_vendor_owner returns False for non-owner."""
from models.database.user import User
from app.modules.tenancy.models import User
vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
@@ -643,7 +643,7 @@ class TestVendorServiceUpdate:
from pydantic import BaseModel
from app.modules.tenancy.exceptions import InsufficientPermissionsException
from models.database.user import User
from app.modules.tenancy.models import User
class VendorUpdate(BaseModel):
name: str | None = None
@@ -695,7 +695,7 @@ class TestVendorServiceUpdate:
):
"""Test marketplace settings update fails for unauthorized user."""
from app.modules.tenancy.exceptions import InsufficientPermissionsException
from models.database.user import User
from app.modules.tenancy.models import User
other_user = db.query(User).filter(User.id == other_company.owner_user_id).first()
marketplace_config = {"letzshop_csv_url_fr": "https://example.com/fr.csv"}