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:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user