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,7 @@ import logging
from math import ceil
from sqlalchemy import func
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from app.exceptions import (
BusinessLogicException,
@@ -27,7 +27,6 @@ from app.modules.billing.models import (
SubscriptionStatus,
SubscriptionTier,
)
from app.modules.tenancy.models import Merchant
logger = logging.getLogger(__name__)
@@ -143,8 +142,9 @@ class AdminSubscriptionService:
) -> dict:
"""List merchant subscriptions with filtering and pagination."""
query = (
db.query(MerchantSubscription, Merchant)
.join(Merchant, MerchantSubscription.merchant_id == Merchant.id)
db.query(MerchantSubscription)
.join(MerchantSubscription.merchant)
.options(joinedload(MerchantSubscription.merchant))
)
# Apply filters
@@ -155,20 +155,35 @@ class AdminSubscriptionService:
SubscriptionTier, MerchantSubscription.tier_id == SubscriptionTier.id
).filter(SubscriptionTier.code == tier)
if search:
query = query.filter(Merchant.name.ilike(f"%{search}%"))
from app.modules.tenancy.services.merchant_service import merchant_service
merchants, _ = merchant_service.get_merchants(db, search=search, limit=10000)
merchant_ids = [m.id for m in merchants]
if not merchant_ids:
return {
"results": [],
"total": 0,
"page": page,
"per_page": per_page,
"pages": 0,
}
query = query.filter(MerchantSubscription.merchant_id.in_(merchant_ids))
# Count total
total = query.count()
# Paginate
offset = (page - 1) * per_page
results = (
subs = (
query.order_by(MerchantSubscription.created_at.desc())
.offset(offset)
.limit(per_page)
.all()
)
# Return (sub, merchant) tuples for backward compatibility with callers
results = [(sub, sub.merchant) for sub in subs]
return {
"results": results,
"total": total,
@@ -181,9 +196,9 @@ class AdminSubscriptionService:
self, db: Session, merchant_id: int, platform_id: int
) -> tuple:
"""Get subscription for a specific merchant on a platform."""
result = (
db.query(MerchantSubscription, Merchant)
.join(Merchant, MerchantSubscription.merchant_id == Merchant.id)
sub = (
db.query(MerchantSubscription)
.options(joinedload(MerchantSubscription.merchant))
.filter(
MerchantSubscription.merchant_id == merchant_id,
MerchantSubscription.platform_id == platform_id,
@@ -191,13 +206,13 @@ class AdminSubscriptionService:
.first()
)
if not result:
if not sub:
raise ResourceNotFoundException(
"Subscription",
f"merchant_id={merchant_id}, platform_id={platform_id}",
)
return result
return sub, sub.merchant
def update_subscription(
self, db: Session, merchant_id: int, platform_id: int, update_data: dict
@@ -242,10 +257,7 @@ class AdminSubscriptionService:
status: str | None = None,
) -> dict:
"""List billing history across all merchants."""
query = (
db.query(BillingHistory, Merchant)
.join(Merchant, BillingHistory.merchant_id == Merchant.id)
)
query = db.query(BillingHistory)
if merchant_id:
query = query.filter(BillingHistory.merchant_id == merchant_id)
@@ -255,13 +267,29 @@ class AdminSubscriptionService:
total = query.count()
offset = (page - 1) * per_page
results = (
invoices = (
query.order_by(BillingHistory.invoice_date.desc())
.offset(offset)
.limit(per_page)
.all()
)
# Batch-fetch merchant names for display
from app.modules.tenancy.services.merchant_service import merchant_service
merchant_ids = {inv.merchant_id for inv in invoices if inv.merchant_id}
merchants_map = {}
for mid in merchant_ids:
m = merchant_service.get_merchant_by_id_optional(db, mid)
if m:
merchants_map[mid] = m
# Return (invoice, merchant) tuples for backward compatibility
results = [
(inv, merchants_map.get(inv.merchant_id))
for inv in invoices
]
return {
"results": results,
"total": total,
@@ -276,16 +304,20 @@ class AdminSubscriptionService:
def get_platform_names_map(self, db: Session) -> dict[int, str]:
"""Get mapping of platform_id -> platform_name."""
from app.modules.tenancy.models import Platform
from app.modules.tenancy.services.platform_service import platform_service
return {p.id: p.name for p in db.query(Platform).all()}
platforms = platform_service.list_platforms(db, include_inactive=True)
return {p.id: p.name for p in platforms}
def get_platform_name(self, db: Session, platform_id: int) -> str | None:
"""Get platform name by ID."""
from app.modules.tenancy.models import Platform
from app.modules.tenancy.services.platform_service import platform_service
p = db.query(Platform).filter(Platform.id == platform_id).first()
return p.name if p else None
try:
p = platform_service.get_platform_by_id(db, platform_id)
return p.name
except Exception:
return None
# =========================================================================
# Merchant Subscriptions with Usage
@@ -359,9 +391,9 @@ class AdminSubscriptionService:
Convenience method for admin store detail page. Resolves
store -> merchant -> all platform subscriptions.
"""
from app.modules.tenancy.models import Store
from app.modules.tenancy.services.store_service import store_service
store = db.query(Store).filter(Store.id == store_id).first()
store = store_service.get_store_by_id_optional(db, store_id)
if not store or not store.merchant_id:
raise ResourceNotFoundException("Store", str(store_id))