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

@@ -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()