refactor(arch): eliminate all cross-module model imports in service layer
Some checks failed
CI / ruff (push) Successful in 9s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / pytest (push) Has been cancelled

Enforce MOD-025/MOD-026 rules: zero top-level cross-module model imports
remain in any service file. All 66 files migrated using deferred import
patterns (method-body, _get_model() helpers, instance-cached self._Model)
and new cross-module service methods in tenancy. Documentation updated
with Pattern 6 (deferred imports), migration plan marked complete, and
violations status reflects 84→0 service-layer violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 06:13:15 +01:00
parent e3a52f6536
commit 86e85a98b8
66 changed files with 2242 additions and 1295 deletions

View File

@@ -89,16 +89,16 @@ class CatalogFeatureProvider:
platform_id: int,
) -> list[FeatureUsage]:
from app.modules.catalog.models.product import Product
from app.modules.tenancy.models import Store, StorePlatform
from app.modules.tenancy.services.platform_service import platform_service
from app.modules.tenancy.services.store_service import store_service
merchant_stores = store_service.get_stores_by_merchant_id(db, merchant_id)
platform_store_ids = platform_service.get_store_ids_for_platform(db, platform_id)
store_ids = [s.id for s in merchant_stores if s.id in platform_store_ids]
count = (
db.query(func.count(Product.id))
.join(Store, Product.store_id == Store.id)
.join(StorePlatform, Store.id == StorePlatform.store_id)
.filter(
Store.merchant_id == merchant_id,
StorePlatform.platform_id == platform_id,
)
.filter(Product.store_id.in_(store_ids))
.scalar()
or 0
)

View File

@@ -152,18 +152,11 @@ class CatalogMetricsProvider:
Aggregates catalog data across all stores.
"""
from app.modules.catalog.models import Product
from app.modules.tenancy.models import StorePlatform
from app.modules.tenancy.services.platform_service import platform_service
try:
# Get all store IDs for this platform using StorePlatform junction table
store_ids = (
db.query(StorePlatform.store_id)
.filter(
StorePlatform.platform_id == platform_id,
StorePlatform.is_active == True,
)
.subquery()
)
# Get all store IDs for this platform via platform service
store_ids = platform_service.get_store_ids_for_platform(db, platform_id)
# Total products
total_products = (

View File

@@ -17,7 +17,6 @@ from sqlalchemy.orm import Session
from app.modules.catalog.exceptions import ProductMediaException
from app.modules.catalog.models import Product, ProductMedia
from app.modules.cms.models import MediaFile
logger = logging.getLogger(__name__)
@@ -64,6 +63,8 @@ class ProductMediaService:
)
# Verify media belongs to store
from app.modules.cms.models import MediaFile
media = (
db.query(MediaFile)
.filter(MediaFile.id == media_id, MediaFile.store_id == store_id)
@@ -162,6 +163,8 @@ class ProductMediaService:
# Update usage count on media
if deleted_count > 0:
from app.modules.cms.models import MediaFile
media = db.query(MediaFile).filter(MediaFile.id == media_id).first()
if media:
media.usage_count = max(0, (media.usage_count or 0) - deleted_count)

View File

@@ -11,6 +11,7 @@ This module provides:
import logging
from datetime import UTC, datetime
from sqlalchemy import func
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
@@ -22,7 +23,6 @@ from app.modules.catalog.exceptions import (
)
from app.modules.catalog.models import Product
from app.modules.catalog.schemas import ProductCreate, ProductUpdate
from app.modules.marketplace.models import MarketplaceProduct # IMPORT-002
logger = logging.getLogger(__name__)
@@ -83,6 +83,8 @@ class ProductService:
"""
try:
# Verify marketplace product exists
from app.modules.marketplace.models import MarketplaceProduct
marketplace_product = (
db.query(MarketplaceProduct)
.filter(MarketplaceProduct.id == product_data.marketplace_product_id)
@@ -333,5 +335,74 @@ class ProductService:
raise ProductValidationException("Failed to search products")
# ========================================================================
# Cross-module public API methods
# ========================================================================
def get_product_by_id(self, db: Session, product_id: int) -> Product | None:
"""
Get product by ID without store scope.
Args:
db: Database session
product_id: Product ID
Returns:
Product object or None
"""
return db.query(Product).filter(Product.id == product_id).first()
def get_products_with_gtin(
self, db: Session, store_id: int
) -> list[Product]:
"""Get all products with a GTIN for a store."""
return (
db.query(Product)
.filter(
Product.store_id == store_id,
Product.gtin.isnot(None),
)
.all()
)
def get_store_product_count(
self,
db: Session,
store_id: int,
active_only: bool = False,
featured_only: bool = False,
) -> int:
"""
Count products for a store with optional filters.
Args:
db: Database session
store_id: Store ID
active_only: Only count active products
featured_only: Only count featured products
Returns:
Product count
"""
query = db.query(func.count(Product.id)).filter(Product.store_id == store_id)
if active_only:
query = query.filter(Product.is_active == True) # noqa: E712
if featured_only:
query = query.filter(Product.is_featured == True) # noqa: E712
return query.scalar() or 0
def get_total_product_count(self, db: Session) -> int:
"""
Get total product count across all stores.
Args:
db: Database session
Returns:
Total product count
"""
return db.query(func.count(Product.id)).scalar() or 0
# Create service instance
product_service = ProductService()

View File

@@ -16,7 +16,6 @@ from sqlalchemy.orm import Session, joinedload
from app.modules.catalog.exceptions import ProductNotFoundException
from app.modules.catalog.models import Product
from app.modules.tenancy.models import Store
logger = logging.getLogger(__name__)
@@ -43,7 +42,6 @@ class StoreProductService:
"""
query = (
db.query(Product)
.join(Store, Product.store_id == Store.id)
.options(
joinedload(Product.store),
joinedload(Product.marketplace_product),
@@ -122,16 +120,21 @@ class StoreProductService:
# Count by store (only when not filtered by store_id)
by_store = {}
if not store_id:
store_counts = (
# Get product counts grouped by store_id
store_id_counts = (
db.query(
Store.name,
Product.store_id,
func.count(Product.id),
)
.join(Store, Product.store_id == Store.id)
.group_by(Store.name)
.group_by(Product.store_id)
.all()
)
by_store = {name or "unknown": count for name, count in store_counts}
# Resolve store names via service
from app.modules.tenancy.services.store_service import store_service
for sid, count in store_id_counts:
store = store_service.get_store_by_id_optional(db, sid)
name = store.name if store else "unknown"
by_store[name] = count
return {
"total": total,
@@ -145,15 +148,20 @@ class StoreProductService:
def get_catalog_stores(self, db: Session) -> list[dict]:
"""Get list of stores with products in their catalogs."""
stores = (
db.query(Store.id, Store.name, Store.store_code)
.join(Product, Store.id == Product.store_id)
from app.modules.tenancy.services.store_service import store_service
# Get distinct store IDs that have products
store_ids = (
db.query(Product.store_id)
.distinct()
.all()
)
return [
{"id": v.id, "name": v.name, "store_code": v.store_code} for v in stores
]
result = []
for (sid,) in store_ids:
store = store_service.get_store_by_id_optional(db, sid)
if store:
result.append({"id": store.id, "name": store.name, "store_code": store.store_code})
return result
def get_product_detail(self, db: Session, product_id: int) -> dict:
"""Get detailed store product information including override info."""