# 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
