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,
StatsService,
)
from app.modules.analytics.services.usage_service import (
usage_service,
UsageService,
UsageData,
UsageMetricData,
TierInfoData,
UpgradeTierData,
LimitCheckData,
)
__all__ = [
# Stats service
"stats_service",
"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.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 models.schema.auth import UserContext

View File

@@ -41,6 +41,15 @@ from app.modules.billing.services.platform_pricing_service import (
PlatformPricingService,
platform_pricing_service,
)
from app.modules.billing.services.usage_service import (
UsageService,
usage_service,
UsageData,
UsageMetricData,
TierInfoData,
UpgradeTierData,
LimitCheckData,
)
__all__ = [
"SubscriptionService",
@@ -63,4 +72,11 @@ __all__ = [
"capacity_forecast_service",
"PlatformPricingService",
"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.orm import Session
from app.modules.catalog.models import Product
from app.modules.billing.models import (
CapacitySnapshot,
MerchantSubscription,
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__)
@@ -87,8 +88,17 @@ class CapacityForecastService:
or 0
)
# Resource metrics
total_products = db.query(func.count(Product.id)).scalar() or 0
# Resource metrics via provider pattern (avoids direct catalog/orders imports)
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 = (
db.query(func.count(StoreUser.id))
.filter(StoreUser.is_active == True) # noqa: E712
@@ -96,15 +106,8 @@ class CapacityForecastService:
or 0
)
# Orders this month
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=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
)
# Orders this month (from stats aggregator)
total_orders = stats.get("orders.in_period", 0)
# Storage metrics
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.
@@ -17,8 +17,8 @@ from dataclasses import dataclass
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.catalog.models import Product
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
logger = logging.getLogger(__name__)
@@ -213,13 +213,10 @@ class UsageService:
# =========================================================================
def _get_product_count(self, db: Session, store_id: int) -> int:
"""Get product count for store."""
return (
db.query(func.count(Product.id))
.filter(Product.store_id == store_id)
.scalar()
or 0
)
"""Get product count for store via feature aggregator."""
usage = feature_aggregator.get_store_usage(db, store_id)
products = usage.get("products_limit")
return products.current_count if products else 0
def _get_team_member_count(self, db: Session, store_id: int) -> int:
"""Get active team member count for store."""
@@ -271,23 +268,10 @@ class UsageService:
def _get_orders_this_period(
self, db: Session, store_id: int, subscription: MerchantSubscription | None
) -> int:
"""Get order count for the current billing period."""
from app.modules.orders.models import Order
period_start = subscription.period_start if subscription else None
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
)
"""Get order count for the current billing period via feature aggregator."""
usage = feature_aggregator.get_store_usage(db, store_id)
orders = usage.get("orders_per_month")
return orders.current_count if orders else 0
def _get_next_tier(
self, db: Session, current_tier: SubscriptionTier | None