fix(billing): resolve 3 IMPORT-001 architecture violations in billing module

Replace direct imports from optional modules (catalog, orders, analytics)
with provider pattern calls (stats_aggregator, feature_aggregator) and
move usage_service from analytics to billing where it belongs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 15:34:29 +01:00
parent 82585b1363
commit 55751d95b9
5 changed files with 43 additions and 57 deletions

View File

@@ -9,26 +9,9 @@ from app.modules.analytics.services.stats_service import (
stats_service, stats_service,
StatsService, StatsService,
) )
from app.modules.analytics.services.usage_service import (
usage_service,
UsageService,
UsageData,
UsageMetricData,
TierInfoData,
UpgradeTierData,
LimitCheckData,
)
__all__ = [ __all__ = [
# Stats service # Stats service
"stats_service", "stats_service",
"StatsService", "StatsService",
# Usage service
"usage_service",
"UsageService",
"UsageData",
"UsageMetricData",
"TierInfoData",
"UpgradeTierData",
"LimitCheckData",
] ]

View File

@@ -18,7 +18,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_store_api, require_module_access from app.api.deps import get_current_store_api, require_module_access
from app.core.database import get_db from app.core.database import get_db
from app.modules.analytics.services.usage_service import usage_service from app.modules.billing.services.usage_service import usage_service
from app.modules.enums import FrontendType from app.modules.enums import FrontendType
from models.schema.auth import UserContext from models.schema.auth import UserContext

View File

@@ -41,6 +41,15 @@ from app.modules.billing.services.platform_pricing_service import (
PlatformPricingService, PlatformPricingService,
platform_pricing_service, platform_pricing_service,
) )
from app.modules.billing.services.usage_service import (
UsageService,
usage_service,
UsageData,
UsageMetricData,
TierInfoData,
UpgradeTierData,
LimitCheckData,
)
__all__ = [ __all__ = [
"SubscriptionService", "SubscriptionService",
@@ -63,4 +72,11 @@ __all__ = [
"capacity_forecast_service", "capacity_forecast_service",
"PlatformPricingService", "PlatformPricingService",
"platform_pricing_service", "platform_pricing_service",
"UsageService",
"usage_service",
"UsageData",
"UsageMetricData",
"TierInfoData",
"UpgradeTierData",
"LimitCheckData",
] ]

View File

@@ -16,13 +16,14 @@ from decimal import Decimal
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.modules.catalog.models import Product
from app.modules.billing.models import ( from app.modules.billing.models import (
CapacitySnapshot, CapacitySnapshot,
MerchantSubscription, MerchantSubscription,
SubscriptionStatus, SubscriptionStatus,
) )
from app.modules.tenancy.models import Store, StoreUser from app.modules.contracts.metrics import MetricsContext
from app.modules.core.services.stats_aggregator import stats_aggregator
from app.modules.tenancy.models import Platform, Store, StoreUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -87,8 +88,17 @@ class CapacityForecastService:
or 0 or 0
) )
# Resource metrics # Resource metrics via provider pattern (avoids direct catalog/orders imports)
total_products = db.query(func.count(Product.id)).scalar() or 0 start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
platform = db.query(Platform).first()
platform_id = platform.id if platform else 1
stats = stats_aggregator.get_admin_stats_flat(
db, platform_id,
context=MetricsContext(date_from=start_of_month),
)
total_products = stats.get("catalog.total_products", 0)
total_team = ( total_team = (
db.query(func.count(StoreUser.id)) db.query(func.count(StoreUser.id))
.filter(StoreUser.is_active == True) # noqa: E712 .filter(StoreUser.is_active == True) # noqa: E712
@@ -96,15 +106,8 @@ class CapacityForecastService:
or 0 or 0
) )
# Orders this month # Orders this month (from stats aggregator)
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) total_orders = stats.get("orders.in_period", 0)
from app.modules.orders.models import Order
total_orders = (
db.query(func.count(Order.id))
.filter(Order.created_at >= start_of_month)
.scalar() or 0
)
# Storage metrics # Storage metrics
try: try:

View File

@@ -1,4 +1,4 @@
# app/modules/analytics/services/usage_service.py # app/modules/billing/services/usage_service.py
""" """
Usage and limits service. Usage and limits service.
@@ -17,8 +17,8 @@ from dataclasses import dataclass
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.modules.catalog.models import Product
from app.modules.billing.models import MerchantSubscription, SubscriptionTier from app.modules.billing.models import MerchantSubscription, SubscriptionTier
from app.modules.billing.services.feature_aggregator import feature_aggregator
from app.modules.tenancy.models import StoreUser from app.modules.tenancy.models import StoreUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -213,13 +213,10 @@ class UsageService:
# ========================================================================= # =========================================================================
def _get_product_count(self, db: Session, store_id: int) -> int: def _get_product_count(self, db: Session, store_id: int) -> int:
"""Get product count for store.""" """Get product count for store via feature aggregator."""
return ( usage = feature_aggregator.get_store_usage(db, store_id)
db.query(func.count(Product.id)) products = usage.get("products_limit")
.filter(Product.store_id == store_id) return products.current_count if products else 0
.scalar()
or 0
)
def _get_team_member_count(self, db: Session, store_id: int) -> int: def _get_team_member_count(self, db: Session, store_id: int) -> int:
"""Get active team member count for store.""" """Get active team member count for store."""
@@ -271,23 +268,10 @@ class UsageService:
def _get_orders_this_period( def _get_orders_this_period(
self, db: Session, store_id: int, subscription: MerchantSubscription | None self, db: Session, store_id: int, subscription: MerchantSubscription | None
) -> int: ) -> int:
"""Get order count for the current billing period.""" """Get order count for the current billing period via feature aggregator."""
from app.modules.orders.models import Order usage = feature_aggregator.get_store_usage(db, store_id)
orders = usage.get("orders_per_month")
period_start = subscription.period_start if subscription else None return orders.current_count if orders else 0
if not period_start:
from datetime import datetime, UTC
period_start = datetime.now(UTC).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
return (
db.query(func.count(Order.id))
.filter(
Order.store_id == store_id,
Order.created_at >= period_start,
)
.scalar()
or 0
)
def _get_next_tier( def _get_next_tier(
self, db: Session, current_tier: SubscriptionTier | None self, db: Session, current_tier: SubscriptionTier | None