# Cross-Module Import Migration Plan **Created:** 2026-02-26 **Updated:** 2026-02-27 **Status:** Complete (Service Layer) — Cat 1-4 fully migrated **Rules:** MOD-025, MOD-026 This document tracks the migration of all cross-module model imports to proper service-based access patterns. ## Overview | Category | Description | Files | Priority | Status | |----------|-------------|-------|----------|--------| | Cat 5 | UserContext legacy import path | 74 | URGENT | Pending | | Cat 1 | Direct queries on another module's models | ~47 | URGENT | **DONE** | | Cat 2 | Creating instances across module boundaries | ~15 | URGENT | **DONE** | | Cat 3 | Aggregation/count queries across boundaries | ~11 | URGENT | **DONE** | | Cat 4 | Join queries involving another module's models | ~4 | URGENT | **DONE** | | P5 | Provider pattern gaps (widgets, metrics) | ~8 modules | Incremental | Pending | | P6 | Route variable naming standardization | ~70 files | Low | **DONE** | ## Completed Service-Layer Migration (2026-02-27) **Result:** Zero top-level cross-module model imports remain in any service file. All 1114 tests pass. ### Patterns Used | Pattern | When Used | Files | |---------|-----------|-------| | **Service method call** | Owning module has/got a service method | Most files — replaced `db.query(Model)` with `some_service.get_by_id()` | | **`from __future__ import annotations` + `TYPE_CHECKING`** | Type hints only, no runtime usage | `invoice_service.py`, `marketplace_import_job_service.py`, `stripe_service.py`, etc. | | **Method-body deferred import** | 1-2 methods need the model | `product_service.py`, `product_media_service.py`, `platform_settings_service.py` | | **`_get_model()` helper** | 3+ methods use same infrastructure model | `log_service.py`, `admin_audit_service.py`, `admin_settings_service.py`, `admin_notification_service.py`, `platform_service.py` | | **Instance-cached `self._Model`** | Model used in nearly every method | `letzshop/order_service.py` (Order/OrderItem) | | **`joinedload()` replacement** | Replaced `.join(Model)` with eager loading via ORM relationship | `inventory_service.py`, `admin_audit_service.py` | | **Pre-query ID filtering** | Get IDs from service, then `Model.id.in_(ids)` | All `*_metrics.py`, `*_features.py` files (StorePlatform → `platform_service.get_store_ids_for_platform()`) | See [Cross-Module Import Rules](cross-module-import-rules.md#6-method-body-deferred-imports) for detailed pattern documentation. ### New Service Methods Added | Module | Method | Purpose | |--------|--------|---------| | `tenancy/platform_service` | `get_default_platform(db)` | Returns first platform | | `tenancy/platform_service` | `get_primary_platform_id_for_store(db, store_id)` | Primary platform ID for a store | | `tenancy/store_service` | `list_all_stores(db, active_only)` | All stores (with optional active filter) | | `tenancy/store_service` | `is_letzshop_slug_claimed(db, slug)` | Check if Letzshop slug is claimed | | `tenancy/store_service` | `is_store_code_taken(db, code)` | Check store code uniqueness | | `tenancy/store_service` | `is_subdomain_taken(db, subdomain)` | Check subdomain uniqueness | | `tenancy/admin_service` | `get_user_by_email(db, email)` | Lookup user by email | | `tenancy/admin_service` | `get_user_by_username(db, username)` | Lookup user by username | | `billing/subscription_service` | `get_all_active_subscriptions(db)` | All active/trial subscriptions | | `catalog/product_service` | `get_products_with_gtin(db, store_id)` | Products that have GTINs | | `inventory/inventory_service` | `delete_inventory_by_gtin(db, gtin)` | Delete inventory by GTIN | | `inventory/inventory_service` | `get_inventory_by_gtin(db, gtin)` | Get inventory records by GTIN | | `marketplace/import_job_service` | `get_import_job_stats(db)` | Import job statistics with processing/today counts | ### Files Migrated (by module) **catalog/** (5 files): `catalog_metrics.py`, `catalog_features.py`, `product_service.py`, `product_media_service.py`, `store_product_service.py` **orders/** (4 files): `order_metrics.py`, `order_features.py`, `order_item_exception_service.py`, `order_inventory_service.py` **inventory/** (3 files): `inventory_metrics.py`, `inventory_service.py`, `inventory_import_service.py` **marketplace/** (8 files): `marketplace_widgets.py`, `marketplace_product_service.py`, `marketplace_import_job_service.py`, `onboarding_service.py`, `platform_signup_service.py`, `letzshop_export_service.py`, `letzshop/order_service.py`, `letzshop/store_sync_service.py` **monitoring/** (5 files): `admin_audit_service.py`, `audit_provider.py`, `background_tasks_service.py`, `capacity_forecast_service.py`, `log_service.py`, `platform_health_service.py` **messaging/** (3 files): `email_service.py`, `store_email_settings_service.py`, `admin_notification_service.py` **cms/** (3 files): `cms_metrics.py`, `store_theme_service.py`, `content_page_service.py` **core/** (2 files): `admin_settings_service.py`, `platform_settings_service.py` **customers/** (2 files): `customer_metrics.py`, `admin_customer_service.py` **tenancy/** (1 file): `platform_service.py` **cart/** (1 file): `cart_service.py` --- ## Cat 5: Move UserContext to Tenancy Module (74 files) **Priority:** URGENT — mechanical, low risk, high impact **Approach:** Move definition, update all imports in one batch ### What `UserContext` is defined in `models/schema/auth.py` (legacy location). Per MOD-019, schemas belong in their module. UserContext is a tenancy concern (user identity, platform/store context). ### Steps 1. **Move `UserContext` class** from `models/schema/auth.py` to `app/modules/tenancy/schemas/auth.py` - Keep all properties and methods intact - Also move related schemas used alongside it: `UserLogin`, `LogoutResponse`, `StoreUserResponse` 2. **Add re-export in legacy location** (temporary backwards compat): ```python # models/schema/auth.py from app.modules.tenancy.schemas.auth import UserContext # noqa: F401 ``` 3. **Update all 74 import sites** from: ```python from models.schema.auth import UserContext ``` to: ```python from app.modules.tenancy.schemas.auth import UserContext ``` 4. **Remove legacy re-export** once all imports are updated ### Files to Update (by module) **app/api/** (1 file) - `app/api/deps.py` **app/modules/tenancy/** (15 files) - `routes/api/admin_merchants.py` - `routes/api/admin_module_config.py` - `routes/api/admin_merchant_domains.py` - `routes/api/admin_modules.py` - `routes/api/admin_platforms.py` - `routes/api/admin_store_domains.py` - `routes/api/admin_stores.py` - `routes/api/admin_users.py` - `routes/api/merchant.py` - `routes/api/store_auth.py` (also imports `LogoutResponse`, `StoreUserResponse`, `UserLogin`) - `routes/api/store_profile.py` - `routes/api/store_team.py` - `routes/pages/merchant.py` - `services/admin_platform_service.py` - `tests/integration/test_merchant_routes.py` **app/modules/core/** (12 files) - `routes/api/admin_dashboard.py` - `routes/api/admin_menu_config.py` - `routes/api/admin_settings.py` - `routes/api/merchant_menu.py` - `routes/api/store_dashboard.py` - `routes/api/store_menu.py` - `routes/api/store_settings.py` - `routes/pages/merchant.py` - `tests/integration/test_merchant_dashboard_routes.py` - `tests/integration/test_merchant_menu_routes.py` - `tests/integration/test_store_dashboard_routes.py` **app/modules/billing/** (9 files) - `routes/api/admin.py` - `routes/api/admin_features.py` - `routes/api/store.py` - `routes/api/store_addons.py` - `routes/api/store_checkout.py` - `routes/api/store_features.py` - `routes/api/store_usage.py` - `routes/pages/merchant.py` - `tests/integration/test_merchant_routes.py` **app/modules/marketplace/** (8 files) - `routes/api/admin_letzshop.py` - `routes/api/admin_marketplace.py` - `routes/api/admin_products.py` - `routes/api/store_letzshop.py` - `routes/api/store_marketplace.py` - `routes/api/store_onboarding.py` - `tests/integration/test_store_page_routes.py` - `tests/unit/test_store_page_routes.py` **app/modules/messaging/** (7 files) - `routes/api/admin_email_templates.py` - `routes/api/admin_messages.py` - `routes/api/admin_notifications.py` - `routes/api/store_email_settings.py` - `routes/api/store_email_templates.py` - `routes/api/store_messages.py` - `routes/api/store_notifications.py` **app/modules/orders/** (6 files) - `routes/api/admin.py` - `routes/api/admin_exceptions.py` - `routes/api/store.py` - `routes/api/store_customer_orders.py` - `routes/api/store_exceptions.py` - `routes/api/store_invoices.py` **app/modules/monitoring/** (6 files) - `routes/api/admin_audit.py` - `routes/api/admin_code_quality.py` - `routes/api/admin_logs.py` - `routes/api/admin_platform_health.py` - `routes/api/admin_tasks.py` - `routes/api/admin_tests.py` **app/modules/cms/** (4 files) - `routes/api/admin_images.py` - `routes/api/admin_media.py` - `routes/api/admin_store_themes.py` - `routes/api/store_media.py` **app/modules/catalog/** (2 files) - `routes/api/admin.py` - `routes/api/store.py` **app/modules/customers/** (2 files) - `routes/api/admin.py` - `routes/api/store.py` **app/modules/inventory/** (2 files) - `routes/api/admin.py` - `routes/api/store.py` **app/modules/loyalty/** (2 files) - `routes/pages/merchant.py` - `tests/conftest.py` **app/modules/payments/** (1 file) - `routes/api/store.py` **tests/** (1 file) - `tests/unit/api/test_deps.py` **docs/** (2 files — update code examples) - `docs/architecture/user-context-pattern.md` - `docs/proposals/decouple-modules.md` --- ## Cat 1: Direct Queries → Service Methods (~47 violations) **Priority:** URGENT — requires creating new service methods first **Approach:** Per-module, add service methods then migrate consumers ### Required New Service Methods #### Tenancy Module (most consumed) The tenancy module needs these public service methods for cross-module consumers: ```python # app/modules/tenancy/services/merchant_service.py (new or extend existing) class MerchantService: def get_merchant_by_id(self, db, merchant_id) -> Merchant | None def get_merchant_by_owner_id(self, db, owner_user_id) -> Merchant | None def get_merchants_for_platform(self, db, platform_id, **filters) -> list[Merchant] def get_merchant_count(self, db, platform_id=None) -> int def search_merchants(self, db, query, platform_id=None) -> list[Merchant] # app/modules/tenancy/services/store_service.py (extend existing) class StoreService: def get_store_by_id(self, db, store_id) -> Store | None def get_stores_for_merchant(self, db, merchant_id) -> list[Store] def get_stores_for_platform(self, db, platform_id, **filters) -> list[Store] def get_store_count(self, db, merchant_id=None, platform_id=None) -> int def get_active_store_count(self, db, platform_id) -> int # app/modules/tenancy/services/platform_service.py (extend existing) class PlatformService: def get_platform_by_id(self, db, platform_id) -> Platform | None def list_platforms(self, db) -> list[Platform] # app/modules/tenancy/services/user_service.py (extend existing) class UserService: def get_user_by_id(self, db, user_id) -> User | None def get_user_by_email(self, db, email) -> User | None def get_store_users(self, db, store_id) -> list[StoreUser] ``` #### Catalog Module ```python # app/modules/catalog/services/product_service.py (extend existing) class ProductService: def get_product_by_id(self, db, product_id) -> Product | None def get_products_by_ids(self, db, product_ids) -> list[Product] def get_product_count(self, db, store_id=None) -> int ``` #### Orders Module ```python # app/modules/orders/services/order_service.py (extend existing) class OrderService: def get_order_by_id(self, db, order_id) -> Order | None def get_order_count(self, db, store_id=None, **filters) -> int def get_orders_for_store(self, db, store_id, **filters) -> list[Order] ``` ### Migration Per Consuming Module #### billing → tenancy (13 direct queries) | File | What it queries | Replace with | |------|----------------|--------------| | `services/admin_subscription_service.py:279` | `db.query(Platform)` | `platform_service.get_platform_by_id()` | | `services/admin_subscription_service.py:285` | `db.query(Store)` | `store_service.get_store_by_id()` | | `services/admin_subscription_service.py:362` | `db.query(Store).filter()` | `store_service.get_stores_for_merchant()` | | `services/billing_service.py:158` | `db.query(Store)` | `store_service.get_store_by_id()` | | `services/billing_service.py:497` | `db.query(Merchant)` | `merchant_service.get_merchant_by_id()` | | `services/feature_service.py:118,145` | `db.query(StorePlatform)` | `store_service.get_store_platform()` | | `services/store_platform_sync_service.py:14` | `db.query(StorePlatform)` | `store_service` methods | | `services/stripe_service.py:26,297,316` | `db.query(Merchant/Store)` | `merchant_service/store_service` | | `services/subscription_service.py:56,74,178,191` | `db.query(Platform/Store)` | service methods | | `services/usage_service.py:22` | `db.query(Store)` | `store_service` | #### loyalty → tenancy (10 direct queries) | File | What it queries | Replace with | |------|----------------|--------------| | `services/card_service.py` | `db.query(Store/User)` | `store_service/user_service` | | `services/program_service.py` | `db.query(Merchant)` | `merchant_service` | | `services/admin_loyalty_service.py` | `db.query(Merchant)` | `merchant_service` | #### marketplace → tenancy (5 direct queries) | File | What it queries | Replace with | |------|----------------|--------------| | `services/letzshop/order_service.py:76` | `db.query(Store)` | `store_service.get_store_by_id()` | | `services/letzshop/order_service.py:100,110` | `db.query(Order)` | `order_service.get_order_count()` | | `services/marketplace_metrics.py:203` | `db.query(StorePlatform)` | `store_service.get_store_count()` | #### core → tenancy (3 direct queries) | File | What it queries | Replace with | |------|----------------|--------------| | `services/auth_service.py:156` | `db.query(Merchant)` | `merchant_service.get_merchant_by_owner_id()` | | `services/auth_service.py:25` | `db.query(Store)` | `store_service.get_store_by_id()` | | `services/menu_service.py:351` | `db.query(Store).join()` | `store_service.get_stores_for_merchant()` | #### analytics → tenancy/catalog (4 direct queries) | File | What it queries | Replace with | |------|----------------|--------------| | `services/stats_service.py:69` | `db.query(Product)` | `product_service.get_product_count()` | | `services/stats_service.py:75` | `db.query(Product)` | `product_service` methods | | `services/stats_service.py:88-92` | `db.query(MarketplaceProduct)` | marketplace service | | `services/stats_service.py:229` | `db.query(Product)` aggregation | `product_service` | #### Other modules (various) | Module | File | Replace with | |--------|------|--------------| | `inventory` | `services/inventory_transaction_service.py:236` | `order_service.get_order_by_id()` | | `cms` | `services/cms_features.py:160` | `store_service.get_store_count()` | | `customers` | `services/admin_customer_service.py:16` | `store_service.get_store_by_id()` | --- ## Cat 2: Model Creation Across Boundaries (~15 violations) **Priority:** URGENT **Approach:** Add create/factory methods to owning services Most of these are in **test fixtures** (acceptable exception) but a few are in production code: ### Production Code Violations | File | Creates | Replace with | |------|---------|--------------| | `contracts/audit.py:117` | `AdminAuditLog()` | Use audit provider `log_action()` | | `billing/services/store_platform_sync_service.py` | `StorePlatform()` | `store_service.create_store_platform()` | | `marketplace/services/letzshop/order_service.py` | `Order/OrderItem()` | `order_service.create_order()` | ### Test Fixtures (Acceptable — Document as Exception) Test files creating models from other modules for integration test setup is acceptable but should be documented: ```python # ACCEPTABLE in tests — document with comment: # Test fixture: creates tenancy models for integration test setup merchant = Merchant(name="Test", ...) ``` --- ## Cat 3: Aggregation/Count Queries (~11 violations) **Priority:** URGENT **Approach:** Add count/stats methods to owning services | File | Query | Replace with | |------|-------|--------------| | `marketplace/services/letzshop/order_service.py:100` | `func.count(Order.id)` | `order_service.get_order_count()` | | `marketplace/services/letzshop/order_service.py:110` | `func.count(Order.id)` | `order_service.get_order_count()` | | `catalog/services/catalog_features.py:92` | `StorePlatform` count | `store_service.get_active_store_count()` | | `cms/services/cms_features.py:160` | `StorePlatform` count | `store_service.get_active_store_count()` | | `marketplace/services/marketplace_metrics.py:203` | `StorePlatform` count | `store_service.get_active_store_count()` | | `customers/services/customer_features.py` | Store count | `store_service.get_store_count()` | | `inventory/services/inventory_features.py` | Store count | `store_service.get_store_count()` | | `orders/services/order_features.py` | Store count | `store_service.get_store_count()` | --- ## Cat 4: Join Queries (~4 violations) **Priority:** URGENT **Approach:** Decompose into service calls or add service methods | File | Join | Resolution | |------|------|------------| | `catalog/services/store_product_service.py:19` | `.join(Store)` | Use `store_service.get_store_by_id()` + own query | | `core/services/menu_service.py:351` | `.join(Store)` | `store_service.get_stores_for_merchant()` | | `customers/services/admin_customer_service.py:16` | `.join(Store)` | `store_service.get_store_by_id()` + own query | | `messaging/services/messaging_service.py:653` | `.join(StoreUser)` | `user_service.get_store_users()` + filter | --- ## P5: Provider Pattern Gaps (Incremental) **Priority:** MEDIUM — implement as each module is touched ### Widget Providers to Add Currently only 2 modules (marketplace, tenancy) provide dashboard widgets. These modules have valuable dashboard data: | Module | Widget Ideas | Implementation | |--------|-------------|----------------| | **orders** | Recent orders list, order status breakdown | `services/order_widgets.py` | | **billing** | Subscription status, revenue trend | `services/billing_widgets.py` | | **catalog** | Product summary, category breakdown | `services/catalog_widgets.py` | | **inventory** | Stock summary, low-stock alerts | `services/inventory_widgets.py` | | **loyalty** | Program stats, member engagement | `services/loyalty_widgets.py` | | **customers** | Customer growth, segments | `services/customer_widgets.py` | ### Metrics Providers to Add | Module | Metric Ideas | Implementation | |--------|-------------|----------------| | **loyalty** | Active programs, total enrollments, points issued | `services/loyalty_metrics.py` | | **payments** | Transaction volume, success rate, gateway stats | `services/payment_metrics.py` | | **analytics** | Report count, active dashboards | `services/analytics_metrics.py` | ### Implementation Template ```python # app/modules/{module}/services/{module}_widgets.py from app.modules.contracts.widgets import ( DashboardWidgetProviderProtocol, ListWidget, BreakdownWidget, ) class {Module}WidgetProvider: @property def widget_category(self) -> str: return "{module}" def get_store_widgets(self, db, store_id, context=None): # Return list of ListWidget/BreakdownWidget ... def get_platform_widgets(self, db, platform_id, context=None): ... {module}_widget_provider = {Module}WidgetProvider() ``` Register in `definition.py`: ```python def _get_widget_provider(): from app.modules.{module}.services.{module}_widgets import {module}_widget_provider return {module}_widget_provider {module}_module = ModuleDefinition( ... widget_provider=_get_widget_provider, ) ``` --- ## P6: Route Variable Naming — DONE (2026-02-27) **Priority:** LOW — cosmetic, no functional impact **Files Changed:** ~70 (25 route files + 40 definition/init files + architecture rules + docs) All route files now export `router` instead of `admin_router`/`store_router`. Consumer code (definition.py, `__init__.py`) imports as `router as admin_router` where distinction is needed. The `ModuleDefinition` dataclass fields remain `admin_router`/`store_router`. **Pattern:** ```python # Route file (admin.py, store.py) — uses `router` router = APIRouter(prefix="/billing", ...) # definition.py — imports as `router`, assigns to distinct field def _get_admin_router(): from app.modules.billing.routes.api.admin import router return router billing_module.admin_router = _get_admin_router() ``` --- ## Execution Order ### Phase 1: Foundation (Do First) — DONE 1. ~~**Cat 5**: Move UserContext to `tenancy.schemas.auth`~~ — Pending (separate task) 2. **Add service methods to tenancy** — **DONE** (2026-02-27) ### Phase 2: High-Impact Migrations — DONE (2026-02-27) 3. **Cat 1 - billing→tenancy**: 13 violations — **DONE** 4. **Cat 1 - loyalty→tenancy**: 10 violations — **DONE** 5. **Cat 1 - marketplace→tenancy/catalog/orders**: 10 violations — **DONE** 6. **Cat 1 - core→tenancy**: 3 violations — **DONE** 7. **Cat 1 - analytics→tenancy/catalog**: 4 violations — **DONE** ### Phase 3: Remaining Migrations — DONE (2026-02-27) 8. **Cat 2**: Model creation violations — **DONE** (deferred imports in method bodies) 9. **Cat 3**: All aggregation queries — **DONE** (service calls + pre-query ID filtering) 10. **Cat 4**: All join queries — **DONE** (joinedload + service calls) 11. **Cat 1**: Remaining modules — **DONE** (all modules migrated) ### Phase 4: Provider Enrichment (Incremental) 12. **P5**: Add widget providers to orders, billing, catalog (highest value) 13. **P5**: Add metrics providers to loyalty, payments 14. **P5**: Add remaining widget providers as modules are touched ### Phase 5: Cleanup — DONE (2026-02-27) 15. ~~**Cat 5**: Move UserContext to `tenancy.schemas.auth` (74 files)~~ — **DONE** (commits 4aa6f76, e3a52f6) 16. ~~**P6**: Route variable naming standardization~~ — **DONE** (all route files export `router`) --- ## Testing Strategy For each migration: 1. Add/verify service method has a unit test 2. Run existing integration tests to confirm no regressions 3. Run `python scripts/validate/validate_architecture.py` to verify violation count decreases ## Related Documentation - [Cross-Module Import Rules](cross-module-import-rules.md) — The rules being enforced - [Architecture Violations Status](architecture-violations-status.md) — Violation tracking - [Module System Architecture](module-system.md) — Module structure reference