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:
@@ -24,7 +24,6 @@ from dataclasses import dataclass, field
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.inventory.models.inventory import Inventory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -131,15 +130,10 @@ class InventoryImportService:
|
||||
db.flush()
|
||||
|
||||
# Build EAN to Product mapping for this store
|
||||
products = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.store_id == store_id,
|
||||
Product.gtin.isnot(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
ean_to_product: dict[str, Product] = {p.gtin: p for p in products if p.gtin}
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
|
||||
products = product_service.get_products_with_gtin(db, store_id)
|
||||
ean_to_product = {p.gtin: p for p in products if p.gtin}
|
||||
|
||||
# Track unmatched GTINs
|
||||
unmatched: dict[str, int] = {} # EAN -> total quantity
|
||||
|
||||
@@ -182,18 +182,11 @@ class InventoryMetricsProvider:
|
||||
Aggregates stock data across all stores.
|
||||
"""
|
||||
from app.modules.inventory.models import Inventory
|
||||
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 inventory
|
||||
total_quantity = (
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.catalog.exceptions import ProductNotFoundException
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.inventory.exceptions import (
|
||||
InsufficientInventoryException,
|
||||
InvalidInventoryOperationException,
|
||||
@@ -32,7 +31,6 @@ from app.modules.inventory.schemas.inventory import (
|
||||
ProductInventorySummary,
|
||||
)
|
||||
from app.modules.tenancy.exceptions import StoreNotFoundException
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -615,7 +613,11 @@ class InventoryService:
|
||||
Returns:
|
||||
AdminInventoryListResponse
|
||||
"""
|
||||
query = db.query(Inventory).join(Product).join(Store)
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
query = db.query(Inventory).options(
|
||||
joinedload(Inventory.product), joinedload(Inventory.store)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if store_id is not None:
|
||||
@@ -628,13 +630,15 @@ class InventoryService:
|
||||
query = query.filter(Inventory.quantity <= low_stock)
|
||||
|
||||
if search:
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.marketplace.models import ( # IMPORT-002
|
||||
MarketplaceProduct,
|
||||
MarketplaceProductTranslation,
|
||||
)
|
||||
|
||||
query = (
|
||||
query.join(MarketplaceProduct)
|
||||
query.join(Product, Inventory.product_id == Product.id)
|
||||
.join(MarketplaceProduct)
|
||||
.outerjoin(MarketplaceProductTranslation)
|
||||
.filter(
|
||||
(MarketplaceProductTranslation.title.ilike(f"%{search}%"))
|
||||
@@ -736,10 +740,11 @@ class InventoryService:
|
||||
limit: int = 50,
|
||||
) -> list[AdminLowStockItem]:
|
||||
"""Get items with low stock levels (admin only)."""
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
query = (
|
||||
db.query(Inventory)
|
||||
.join(Product)
|
||||
.join(Store)
|
||||
.options(joinedload(Inventory.product), joinedload(Inventory.store))
|
||||
.filter(Inventory.quantity <= threshold)
|
||||
)
|
||||
|
||||
@@ -780,18 +785,22 @@ class InventoryService:
|
||||
) -> AdminStoresWithInventoryResponse:
|
||||
"""Get list of stores that have inventory entries (admin only)."""
|
||||
# SVC-005 - Admin function, intentionally cross-store
|
||||
# Use subquery to avoid DISTINCT on JSON columns (PostgreSQL can't compare JSON)
|
||||
store_ids_subquery = (
|
||||
db.query(Inventory.store_id)
|
||||
.distinct()
|
||||
.subquery()
|
||||
)
|
||||
stores = (
|
||||
db.query(Store)
|
||||
.filter(Store.id.in_(db.query(store_ids_subquery.c.store_id)))
|
||||
.order_by(Store.name)
|
||||
.all()
|
||||
)
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
|
||||
# Get distinct store IDs from inventory
|
||||
store_ids = [
|
||||
r[0]
|
||||
for r in db.query(Inventory.store_id).distinct().all()
|
||||
]
|
||||
|
||||
stores = []
|
||||
for sid in sorted(store_ids):
|
||||
s = store_service.get_store_by_id_optional(db, sid)
|
||||
if s:
|
||||
stores.append(s)
|
||||
|
||||
# Sort by name
|
||||
stores.sort(key=lambda s: s.name or "")
|
||||
|
||||
return AdminStoresWithInventoryResponse(
|
||||
stores=[
|
||||
@@ -826,7 +835,9 @@ class InventoryService:
|
||||
) -> AdminInventoryListResponse:
|
||||
"""Get inventory for a specific store (admin only)."""
|
||||
# Verify store exists
|
||||
store = db.query(Store).filter(Store.id == store_id).first()
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
|
||||
store = store_service.get_store_by_id_optional(db, store_id)
|
||||
if not store:
|
||||
raise StoreNotFoundException(f"Store {store_id} not found")
|
||||
|
||||
@@ -890,16 +901,20 @@ class InventoryService:
|
||||
self, db: Session, product_id: int
|
||||
) -> ProductInventorySummary:
|
||||
"""Get inventory summary for a product (admin only - no store check)."""
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
|
||||
product = product_service.get_product_by_id(db, product_id)
|
||||
if not product:
|
||||
raise ProductNotFoundException(f"Product {product_id} not found")
|
||||
|
||||
# Use existing method with the product's store_id
|
||||
return self.get_product_inventory(db, product.store_id, product_id)
|
||||
|
||||
def verify_store_exists(self, db: Session, store_id: int) -> Store:
|
||||
def verify_store_exists(self, db: Session, store_id: int):
|
||||
"""Verify store exists and return it."""
|
||||
store = db.query(Store).filter(Store.id == store_id).first()
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
|
||||
store = store_service.get_store_by_id_optional(db, store_id)
|
||||
if not store:
|
||||
raise StoreNotFoundException(f"Store {store_id} not found")
|
||||
return store
|
||||
@@ -915,23 +930,17 @@ class InventoryService:
|
||||
# Private helper methods
|
||||
# =========================================================================
|
||||
|
||||
def _get_store_product(
|
||||
self, db: Session, store_id: int, product_id: int
|
||||
) -> Product:
|
||||
def _get_store_product(self, db: Session, store_id: int, product_id: int):
|
||||
"""Get product and verify it belongs to store."""
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(Product.id == product_id, Product.store_id == store_id)
|
||||
.first()
|
||||
)
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
|
||||
if not product:
|
||||
try:
|
||||
return product_service.get_product(db, store_id, product_id)
|
||||
except ProductNotFoundException:
|
||||
raise ProductNotFoundException(
|
||||
f"Product {product_id} not found in your catalog"
|
||||
)
|
||||
|
||||
return product
|
||||
|
||||
def _get_inventory_entry(
|
||||
self, db: Session, product_id: int, location: str
|
||||
) -> Inventory | None:
|
||||
@@ -970,5 +979,91 @@ class InventoryService:
|
||||
raise InvalidQuantityException("Quantity must be positive")
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# Cross-module public API methods
|
||||
# ========================================================================
|
||||
|
||||
def get_store_inventory_stats(self, db: Session, store_id: int) -> dict:
|
||||
"""
|
||||
Get inventory statistics for a store.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
store_id: Store ID
|
||||
|
||||
Returns:
|
||||
Dict with total, reserved, available, locations
|
||||
"""
|
||||
total = (
|
||||
db.query(func.sum(Inventory.quantity))
|
||||
.filter(Inventory.store_id == store_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
reserved = (
|
||||
db.query(func.sum(Inventory.reserved_quantity))
|
||||
.filter(Inventory.store_id == store_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
locations = (
|
||||
db.query(func.count(func.distinct(Inventory.bin_location)))
|
||||
.filter(Inventory.store_id == store_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
return {
|
||||
"total": total,
|
||||
"reserved": reserved,
|
||||
"available": total - reserved,
|
||||
"locations": locations,
|
||||
}
|
||||
|
||||
def get_total_inventory_count(self, db: Session) -> int:
|
||||
"""
|
||||
Get total inventory record count across all stores.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Total inventory records
|
||||
"""
|
||||
return db.query(func.count(Inventory.id)).scalar() or 0
|
||||
|
||||
def get_total_inventory_quantity(self, db: Session) -> int:
|
||||
"""
|
||||
Get sum of all inventory quantities across all stores.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Total quantity
|
||||
"""
|
||||
return db.query(func.sum(Inventory.quantity)).scalar() or 0
|
||||
|
||||
def get_total_reserved_quantity(self, db: Session) -> int:
|
||||
"""
|
||||
Get sum of all reserved quantities across all stores.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Total reserved quantity
|
||||
"""
|
||||
return db.query(func.sum(Inventory.reserved_quantity)).scalar() or 0
|
||||
|
||||
|
||||
def delete_inventory_by_gtin(self, db: Session, gtin: str) -> int:
|
||||
"""Delete all inventory entries matching a GTIN."""
|
||||
return db.query(Inventory).filter(Inventory.gtin == gtin).delete()
|
||||
|
||||
def get_inventory_by_gtin(self, db: Session, gtin: str) -> list[Inventory]:
|
||||
"""Get all inventory entries for a GTIN."""
|
||||
return db.query(Inventory).filter(Inventory.gtin == gtin).all()
|
||||
|
||||
|
||||
# Create service instance
|
||||
inventory_service = InventoryService()
|
||||
|
||||
@@ -13,11 +13,9 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.catalog.exceptions import ProductNotFoundException
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.inventory.models.inventory import Inventory
|
||||
from app.modules.inventory.models.inventory_transaction import InventoryTransaction
|
||||
from app.modules.orders.exceptions import OrderNotFoundException # IMPORT-002
|
||||
from app.modules.orders.models import Order # IMPORT-002
|
||||
from app.modules.orders.exceptions import OrderNotFoundException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -73,9 +71,11 @@ class InventoryTransactionService:
|
||||
)
|
||||
|
||||
# Build result with product details
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
|
||||
result = []
|
||||
for tx in transactions:
|
||||
product = db.query(Product).filter(Product.id == tx.product_id).first()
|
||||
product = product_service.get_product_by_id(db, tx.product_id)
|
||||
product_title = None
|
||||
product_sku = None
|
||||
if product:
|
||||
@@ -132,13 +132,11 @@ class InventoryTransactionService:
|
||||
ProductNotFoundException: If product not found or doesn't belong to store
|
||||
"""
|
||||
# Get product details
|
||||
product = (
|
||||
db.query(Product)
|
||||
.filter(Product.id == product_id, Product.store_id == store_id)
|
||||
.first()
|
||||
)
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
|
||||
if not product:
|
||||
product = product_service.get_product_by_id(db, product_id)
|
||||
|
||||
if not product or product.store_id != store_id:
|
||||
raise ProductNotFoundException(
|
||||
f"Product {product_id} not found in store catalog"
|
||||
)
|
||||
@@ -232,11 +230,9 @@ class InventoryTransactionService:
|
||||
OrderNotFoundException: If order not found or doesn't belong to store
|
||||
"""
|
||||
# Verify order belongs to store
|
||||
order = (
|
||||
db.query(Order)
|
||||
.filter(Order.id == order_id, Order.store_id == store_id)
|
||||
.first()
|
||||
)
|
||||
from app.modules.orders.services.order_service import order_service
|
||||
|
||||
order = order_service.get_order_by_id(db, order_id, store_id=store_id)
|
||||
|
||||
if not order:
|
||||
raise OrderNotFoundException(f"Order {order_id} not found")
|
||||
@@ -250,9 +246,11 @@ class InventoryTransactionService:
|
||||
)
|
||||
|
||||
# Build result with product details
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
|
||||
result = []
|
||||
for tx in transactions:
|
||||
product = db.query(Product).filter(Product.id == tx.product_id).first()
|
||||
product = product_service.get_product_by_id(db, tx.product_id)
|
||||
product_title = None
|
||||
product_sku = None
|
||||
if product:
|
||||
@@ -320,7 +318,8 @@ class InventoryTransactionService:
|
||||
Returns:
|
||||
Tuple of (transactions with details, total count)
|
||||
"""
|
||||
from app.modules.tenancy.models import Store
|
||||
from app.modules.catalog.services.product_service import product_service
|
||||
from app.modules.tenancy.services.store_service import store_service
|
||||
|
||||
# Build query
|
||||
query = db.query(InventoryTransaction)
|
||||
@@ -351,8 +350,8 @@ class InventoryTransactionService:
|
||||
# Build result with store and product details
|
||||
result = []
|
||||
for tx in transactions:
|
||||
store = db.query(Store).filter(Store.id == tx.store_id).first()
|
||||
product = db.query(Product).filter(Product.id == tx.product_id).first()
|
||||
store = store_service.get_store_by_id_optional(db, tx.store_id)
|
||||
product = product_service.get_product_by_id(db, tx.product_id)
|
||||
|
||||
product_title = None
|
||||
product_sku = None
|
||||
|
||||
Reference in New Issue
Block a user