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

@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
from app.modules.customers.exceptions import CustomerNotFoundException
from app.modules.customers.models import Customer
from app.modules.tenancy.models import Store
logger = logging.getLogger(__name__)
@@ -44,8 +43,10 @@ class AdminCustomerService:
Returns:
Tuple of (customers list, total count)
"""
from app.modules.tenancy.services.store_service import store_service
# Build query
query = db.query(Customer).join(Store, Customer.store_id == Store.id)
query = db.query(Customer)
# Apply filters
if store_id:
@@ -66,21 +67,26 @@ class AdminCustomerService:
# Get total count
total = query.count()
# Get paginated results with store info
# Get paginated results
customers = (
query.add_columns(Store.name.label("store_name"), Store.store_code)
.order_by(Customer.created_at.desc())
query.order_by(Customer.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
# Batch-resolve store names
store_ids = {c.store_id for c in customers}
store_map = {}
for sid in store_ids:
store = store_service.get_store_by_id_optional(db, sid)
if store:
store_map[sid] = (store.name, store.store_code)
# Format response
result = []
for row in customers:
customer = row[0]
store_name = row[1]
store_code = row[2]
for customer in customers:
store_name, store_code = store_map.get(customer.store_id, (None, None))
customer_dict = {
"id": customer.id,
@@ -167,18 +173,18 @@ class AdminCustomerService:
Raises:
CustomerNotFoundException: If customer not found
"""
result = (
from app.modules.tenancy.services.store_service import store_service
customer = (
db.query(Customer)
.join(Store, Customer.store_id == Store.id)
.add_columns(Store.name.label("store_name"), Store.store_code)
.filter(Customer.id == customer_id)
.first()
)
if not result:
if not customer:
raise CustomerNotFoundException(str(customer_id))
customer = result[0]
store = store_service.get_store_by_id_optional(db, customer.store_id)
return {
"id": customer.id,
"store_id": customer.store_id,
@@ -195,8 +201,8 @@ class AdminCustomerService:
"is_active": customer.is_active,
"created_at": customer.created_at,
"updated_at": customer.updated_at,
"store_name": result[1],
"store_code": result[2],
"store_name": store.name if store else None,
"store_code": store.store_code if store else None,
}
def toggle_customer_status(

View File

@@ -125,18 +125,11 @@ class CustomerMetricsProvider:
For platforms, aggregates customer data across all stores.
"""
from app.modules.customers.models import Customer
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 customers across all stores
total_customers = (
@@ -208,14 +201,11 @@ class CustomerMetricsProvider:
Aggregates customer counts across all stores owned by the merchant.
"""
from app.modules.customers.models import Customer
from app.modules.tenancy.models import Store
from app.modules.tenancy.services.store_service import store_service
try:
merchant_store_ids = (
db.query(Store.id)
.filter(Store.merchant_id == merchant_id)
.subquery()
)
merchant_stores = store_service.get_stores_by_merchant_id(db, merchant_id)
merchant_store_ids = [s.id for s in merchant_stores]
total_customers = (
db.query(Customer)

View File

@@ -30,7 +30,7 @@ from app.modules.tenancy.exceptions import (
StoreNotActiveException,
StoreNotFoundException,
)
from app.modules.tenancy.models import Store
from app.modules.tenancy.services.store_service import store_service as _store_service
logger = logging.getLogger(__name__)
@@ -62,7 +62,7 @@ class CustomerService:
CustomerValidationException: If customer data is invalid
"""
# Verify store exists and is active
store = db.query(Store).filter(Store.id == store_id).first()
store = _store_service.get_store_by_id_optional(db, store_id)
if not store:
raise StoreNotFoundException(str(store_id), identifier_type="id")
@@ -150,7 +150,7 @@ class CustomerService:
CustomerNotActiveException: If customer account is inactive
"""
# Verify store exists
store = db.query(Store).filter(Store.id == store_id).first()
store = _store_service.get_store_by_id_optional(db, store_id)
if not store:
raise StoreNotFoundException(str(store_id), identifier_type="id")
@@ -575,5 +575,96 @@ class CustomerService:
return customer
# ========================================================================
# Cross-module public API methods
# ========================================================================
def create_customer_for_enrollment(
self,
db: Session,
store_id: int,
email: str,
first_name: str = "",
last_name: str = "",
phone: str | None = None,
) -> Customer:
"""
Create a customer for loyalty/external enrollment.
Creates a customer with an unusable password hash.
Args:
db: Database session
store_id: Store ID
email: Customer email
first_name: First name
last_name: Last name
phone: Phone number
Returns:
Created Customer object
"""
import secrets
unusable_hash = f"!enrollment!{secrets.token_hex(32)}"
store_code = "STORE"
try:
from app.modules.tenancy.services.store_service import store_service
store = store_service.get_store_by_id_optional(db, store_id)
if store:
store_code = store.store_code
except Exception:
pass
cust_number = self._generate_customer_number(db, store_id, store_code)
customer = Customer(
email=email,
first_name=first_name,
last_name=last_name,
phone=phone,
hashed_password=unusable_hash,
customer_number=cust_number,
store_id=store_id,
is_active=True,
)
db.add(customer)
db.flush()
return customer
def get_customer_by_id(self, db: Session, customer_id: int) -> Customer | None:
"""
Get customer by ID without store scope.
Args:
db: Database session
customer_id: Customer ID
Returns:
Customer object or None
"""
return db.query(Customer).filter(Customer.id == customer_id).first()
def get_store_customer_count(self, db: Session, store_id: int) -> int:
"""
Count customers for a store.
Args:
db: Database session
store_id: Store ID
Returns:
Customer count
"""
from sqlalchemy import func
return (
db.query(func.count(Customer.id))
.filter(Customer.store_id == store_id)
.scalar()
or 0
)
# Singleton instance
customer_service = CustomerService()