refactor(arch): eliminate all cross-module model imports in service layer
Some checks failed
Some checks failed
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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user