All route files (admin.py, store.py) 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. ModuleDefinition fields remain admin_router/store_router. 64 files changed across all modules. Architecture rules, docs, and migration plan updated. Added noqa:API001 support to validator for pre-existing raw dict endpoints now visible with standardized router name. All 1114 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22 KiB
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 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
-
Move
UserContextclass frommodels/schema/auth.pytoapp/modules/tenancy/schemas/auth.py- Keep all properties and methods intact
- Also move related schemas used alongside it:
UserLogin,LogoutResponse,StoreUserResponse
-
Add re-export in legacy location (temporary backwards compat):
# models/schema/auth.py from app.modules.tenancy.schemas.auth import UserContext # noqa: F401 -
Update all 74 import sites from:
from models.schema.auth import UserContextto:
from app.modules.tenancy.schemas.auth import UserContext -
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.pyroutes/api/admin_module_config.pyroutes/api/admin_merchant_domains.pyroutes/api/admin_modules.pyroutes/api/admin_platforms.pyroutes/api/admin_store_domains.pyroutes/api/admin_stores.pyroutes/api/admin_users.pyroutes/api/merchant.pyroutes/api/store_auth.py(also importsLogoutResponse,StoreUserResponse,UserLogin)routes/api/store_profile.pyroutes/api/store_team.pyroutes/pages/merchant.pyservices/admin_platform_service.pytests/integration/test_merchant_routes.py
app/modules/core/ (12 files)
routes/api/admin_dashboard.pyroutes/api/admin_menu_config.pyroutes/api/admin_settings.pyroutes/api/merchant_menu.pyroutes/api/store_dashboard.pyroutes/api/store_menu.pyroutes/api/store_settings.pyroutes/pages/merchant.pytests/integration/test_merchant_dashboard_routes.pytests/integration/test_merchant_menu_routes.pytests/integration/test_store_dashboard_routes.py
app/modules/billing/ (9 files)
routes/api/admin.pyroutes/api/admin_features.pyroutes/api/store.pyroutes/api/store_addons.pyroutes/api/store_checkout.pyroutes/api/store_features.pyroutes/api/store_usage.pyroutes/pages/merchant.pytests/integration/test_merchant_routes.py
app/modules/marketplace/ (8 files)
routes/api/admin_letzshop.pyroutes/api/admin_marketplace.pyroutes/api/admin_products.pyroutes/api/store_letzshop.pyroutes/api/store_marketplace.pyroutes/api/store_onboarding.pytests/integration/test_store_page_routes.pytests/unit/test_store_page_routes.py
app/modules/messaging/ (7 files)
routes/api/admin_email_templates.pyroutes/api/admin_messages.pyroutes/api/admin_notifications.pyroutes/api/store_email_settings.pyroutes/api/store_email_templates.pyroutes/api/store_messages.pyroutes/api/store_notifications.py
app/modules/orders/ (6 files)
routes/api/admin.pyroutes/api/admin_exceptions.pyroutes/api/store.pyroutes/api/store_customer_orders.pyroutes/api/store_exceptions.pyroutes/api/store_invoices.py
app/modules/monitoring/ (6 files)
routes/api/admin_audit.pyroutes/api/admin_code_quality.pyroutes/api/admin_logs.pyroutes/api/admin_platform_health.pyroutes/api/admin_tasks.pyroutes/api/admin_tests.py
app/modules/cms/ (4 files)
routes/api/admin_images.pyroutes/api/admin_media.pyroutes/api/admin_store_themes.pyroutes/api/store_media.py
app/modules/catalog/ (2 files)
routes/api/admin.pyroutes/api/store.py
app/modules/customers/ (2 files)
routes/api/admin.pyroutes/api/store.py
app/modules/inventory/ (2 files)
routes/api/admin.pyroutes/api/store.py
app/modules/loyalty/ (2 files)
routes/pages/merchant.pytests/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.mddocs/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:
# 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
# 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
# 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:
# 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
# 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:
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:
# 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
Cat 5: Move UserContext to— Pending (separate task)tenancy.schemas.auth- Add service methods to tenancy — DONE (2026-02-27)
Phase 2: High-Impact Migrations — DONE (2026-02-27)
- Cat 1 - billing→tenancy: 13 violations — DONE
- Cat 1 - loyalty→tenancy: 10 violations — DONE
- Cat 1 - marketplace→tenancy/catalog/orders: 10 violations — DONE
- Cat 1 - core→tenancy: 3 violations — DONE
- Cat 1 - analytics→tenancy/catalog: 4 violations — DONE
Phase 3: Remaining Migrations — DONE (2026-02-27)
- Cat 2: Model creation violations — DONE (deferred imports in method bodies)
- Cat 3: All aggregation queries — DONE (service calls + pre-query ID filtering)
- Cat 4: All join queries — DONE (joinedload + service calls)
- Cat 1: Remaining modules — DONE (all modules migrated)
Phase 4: Provider Enrichment (Incremental)
- P5: Add widget providers to orders, billing, catalog (highest value)
- P5: Add metrics providers to loyalty, payments
- P5: Add remaining widget providers as modules are touched
Phase 5: Cleanup — DONE (2026-02-27)
Cat 5: Move UserContext to— DONE (commitstenancy.schemas.auth(74 files)4aa6f76,e3a52f6)P6: Route variable naming standardization— DONE (all route files exportrouter)
Testing Strategy
For each migration:
- Add/verify service method has a unit test
- Run existing integration tests to confirm no regressions
- Run
python scripts/validate/validate_architecture.pyto verify violation count decreases
Related Documentation
- Cross-Module Import Rules — The rules being enforced
- Architecture Violations Status — Violation tracking
- Module System Architecture — Module structure reference