refactor: move capacity_forecast_service from billing to monitoring
Some checks failed
Some checks failed
Resolves the billing (core) → monitoring (optional) architecture violation by moving CapacityForecastService to the monitoring module where it belongs. - Create BillingMetricsProvider to expose subscription counts via stats_aggregator - Move CapacitySnapshot model from billing to monitoring - Replace direct MerchantSubscription queries with stats_aggregator calls - Fix middleware test mocks to cover StoreDomain/MerchantDomain fallback chains Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,13 @@ def _get_store_router():
|
|||||||
return store_router
|
return store_router
|
||||||
|
|
||||||
|
|
||||||
|
def _get_metrics_provider():
|
||||||
|
"""Lazy import of metrics provider to avoid circular imports."""
|
||||||
|
from app.modules.billing.services.billing_metrics import billing_metrics_provider
|
||||||
|
|
||||||
|
return billing_metrics_provider
|
||||||
|
|
||||||
|
|
||||||
def _get_feature_provider():
|
def _get_feature_provider():
|
||||||
"""Lazy import of feature provider to avoid circular imports."""
|
"""Lazy import of feature provider to avoid circular imports."""
|
||||||
from app.modules.billing.services.billing_features import billing_feature_provider
|
from app.modules.billing.services.billing_features import billing_feature_provider
|
||||||
@@ -271,6 +278,8 @@ billing_module = ModuleDefinition(
|
|||||||
],
|
],
|
||||||
# Feature provider for feature flags
|
# Feature provider for feature flags
|
||||||
feature_provider=_get_feature_provider,
|
feature_provider=_get_feature_provider,
|
||||||
|
# Metrics provider for subscription metrics
|
||||||
|
metrics_provider=_get_metrics_provider,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from app.modules.billing.models.subscription import (
|
|||||||
AddOnProduct,
|
AddOnProduct,
|
||||||
BillingHistory,
|
BillingHistory,
|
||||||
BillingPeriod,
|
BillingPeriod,
|
||||||
CapacitySnapshot,
|
|
||||||
StoreAddOn,
|
StoreAddOn,
|
||||||
StripeWebhookEvent,
|
StripeWebhookEvent,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
@@ -46,7 +45,6 @@ __all__ = [
|
|||||||
"StoreAddOn",
|
"StoreAddOn",
|
||||||
"StripeWebhookEvent",
|
"StripeWebhookEvent",
|
||||||
"BillingHistory",
|
"BillingHistory",
|
||||||
"CapacitySnapshot",
|
|
||||||
# Merchant Subscription
|
# Merchant Subscription
|
||||||
"MerchantSubscription",
|
"MerchantSubscription",
|
||||||
# Feature Limits
|
# Feature Limits
|
||||||
|
|||||||
@@ -345,61 +345,3 @@ class BillingHistory(Base, TimestampMixin):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<BillingHistory(store_id={self.store_id}, invoice='{self.invoice_number}', status='{self.status}')>"
|
return f"<BillingHistory(store_id={self.store_id}, invoice='{self.invoice_number}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Capacity Planning
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
class CapacitySnapshot(Base, TimestampMixin):
|
|
||||||
"""
|
|
||||||
Daily snapshot of platform capacity metrics.
|
|
||||||
|
|
||||||
Used for growth trending and capacity forecasting.
|
|
||||||
Captured daily by background job.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "capacity_snapshots"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
snapshot_date = Column(DateTime(timezone=True), nullable=False, unique=True, index=True)
|
|
||||||
|
|
||||||
# Store metrics
|
|
||||||
total_stores = Column(Integer, default=0, nullable=False)
|
|
||||||
active_stores = Column(Integer, default=0, nullable=False)
|
|
||||||
trial_stores = Column(Integer, default=0, nullable=False)
|
|
||||||
|
|
||||||
# Subscription metrics
|
|
||||||
total_subscriptions = Column(Integer, default=0, nullable=False)
|
|
||||||
active_subscriptions = Column(Integer, default=0, nullable=False)
|
|
||||||
|
|
||||||
# Resource metrics
|
|
||||||
total_products = Column(Integer, default=0, nullable=False)
|
|
||||||
total_orders_month = Column(Integer, default=0, nullable=False)
|
|
||||||
total_team_members = Column(Integer, default=0, nullable=False)
|
|
||||||
|
|
||||||
# Storage metrics
|
|
||||||
storage_used_gb = Column(Numeric(10, 2), default=0, nullable=False)
|
|
||||||
db_size_mb = Column(Numeric(10, 2), default=0, nullable=False)
|
|
||||||
|
|
||||||
# Capacity metrics (theoretical limits from subscriptions)
|
|
||||||
theoretical_products_limit = Column(Integer, nullable=True)
|
|
||||||
theoretical_orders_limit = Column(Integer, nullable=True)
|
|
||||||
theoretical_team_limit = Column(Integer, nullable=True)
|
|
||||||
|
|
||||||
# Tier distribution (JSON: {"essential": 10, "professional": 5, ...})
|
|
||||||
tier_distribution = Column(JSON, nullable=True)
|
|
||||||
|
|
||||||
# Performance metrics
|
|
||||||
avg_response_ms = Column(Integer, nullable=True)
|
|
||||||
peak_cpu_percent = Column(Numeric(5, 2), nullable=True)
|
|
||||||
peak_memory_percent = Column(Numeric(5, 2), nullable=True)
|
|
||||||
|
|
||||||
# Indexes
|
|
||||||
__table_args__ = (
|
|
||||||
Index("ix_capacity_snapshots_date", "snapshot_date"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<CapacitySnapshot(date={self.snapshot_date}, stores={self.total_stores})>"
|
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ from app.modules.billing.services.billing_service import (
|
|||||||
BillingService,
|
BillingService,
|
||||||
billing_service,
|
billing_service,
|
||||||
)
|
)
|
||||||
from app.modules.billing.services.capacity_forecast_service import (
|
|
||||||
CapacityForecastService,
|
|
||||||
capacity_forecast_service,
|
|
||||||
)
|
|
||||||
from app.modules.billing.services.feature_service import (
|
from app.modules.billing.services.feature_service import (
|
||||||
FeatureService,
|
FeatureService,
|
||||||
feature_service,
|
feature_service,
|
||||||
@@ -68,8 +64,6 @@ __all__ = [
|
|||||||
"SubscriptionNotCancelledError",
|
"SubscriptionNotCancelledError",
|
||||||
"FeatureService",
|
"FeatureService",
|
||||||
"feature_service",
|
"feature_service",
|
||||||
"CapacityForecastService",
|
|
||||||
"capacity_forecast_service",
|
|
||||||
"PlatformPricingService",
|
"PlatformPricingService",
|
||||||
"platform_pricing_service",
|
"platform_pricing_service",
|
||||||
"UsageService",
|
"UsageService",
|
||||||
|
|||||||
115
app/modules/billing/services/billing_metrics.py
Normal file
115
app/modules/billing/services/billing_metrics.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# app/modules/billing/services/billing_metrics.py
|
||||||
|
"""
|
||||||
|
Metrics provider for the billing module.
|
||||||
|
|
||||||
|
Provides metrics for:
|
||||||
|
- Subscription counts (total, active, trial)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.modules.contracts.metrics import (
|
||||||
|
MetricsContext,
|
||||||
|
MetricValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BillingMetricsProvider:
|
||||||
|
"""
|
||||||
|
Metrics provider for billing module.
|
||||||
|
|
||||||
|
Provides subscription metrics at the platform level.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metrics_category(self) -> str:
|
||||||
|
return "billing"
|
||||||
|
|
||||||
|
def get_store_metrics(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
store_id: int,
|
||||||
|
context: MetricsContext | None = None,
|
||||||
|
) -> list[MetricValue]:
|
||||||
|
"""
|
||||||
|
Get metrics for a specific store.
|
||||||
|
|
||||||
|
Subscriptions are merchant-level, not store-level, so no store metrics.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_platform_metrics(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
platform_id: int,
|
||||||
|
context: MetricsContext | None = None,
|
||||||
|
) -> list[MetricValue]:
|
||||||
|
"""
|
||||||
|
Get subscription metrics aggregated for a platform.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- Total subscriptions
|
||||||
|
- Active subscriptions (active + trial)
|
||||||
|
- Trial subscriptions
|
||||||
|
"""
|
||||||
|
from app.modules.billing.models import MerchantSubscription, SubscriptionStatus
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_subs = (
|
||||||
|
db.query(func.count(MerchantSubscription.id)).scalar() or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
active_subs = (
|
||||||
|
db.query(func.count(MerchantSubscription.id))
|
||||||
|
.filter(MerchantSubscription.status.in_(["active", "trial"]))
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
trial_subs = (
|
||||||
|
db.query(func.count(MerchantSubscription.id))
|
||||||
|
.filter(MerchantSubscription.status == SubscriptionStatus.TRIAL.value)
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
MetricValue(
|
||||||
|
key="billing.total_subscriptions",
|
||||||
|
value=total_subs,
|
||||||
|
label="Total Subscriptions",
|
||||||
|
category="billing",
|
||||||
|
icon="credit-card",
|
||||||
|
description="Total number of merchant subscriptions",
|
||||||
|
),
|
||||||
|
MetricValue(
|
||||||
|
key="billing.active_subscriptions",
|
||||||
|
value=active_subs,
|
||||||
|
label="Active Subscriptions",
|
||||||
|
category="billing",
|
||||||
|
icon="check-circle",
|
||||||
|
description="Subscriptions with active or trial status",
|
||||||
|
),
|
||||||
|
MetricValue(
|
||||||
|
key="billing.trial_subscriptions",
|
||||||
|
value=trial_subs,
|
||||||
|
label="Trial Subscriptions",
|
||||||
|
category="billing",
|
||||||
|
icon="clock",
|
||||||
|
description="Subscriptions currently in trial period",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get billing platform metrics: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton instance
|
||||||
|
billing_metrics_provider = BillingMetricsProvider()
|
||||||
|
|
||||||
|
__all__ = ["BillingMetricsProvider", "billing_metrics_provider"]
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
"""
|
"""
|
||||||
Monitoring module database models.
|
Monitoring module database models.
|
||||||
|
|
||||||
Re-exports monitoring-related models from their source locations.
|
Provides monitoring-related models including capacity snapshots.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# CapacitySnapshot is in billing module (tracks system capacity over time)
|
from app.modules.monitoring.models.capacity_snapshot import CapacitySnapshot
|
||||||
from app.modules.billing.models import CapacitySnapshot
|
|
||||||
|
|
||||||
# Admin notification and logging models
|
# Admin notification and logging models
|
||||||
from app.modules.messaging.models import AdminNotification
|
from app.modules.messaging.models import AdminNotification
|
||||||
|
|||||||
71
app/modules/monitoring/models/capacity_snapshot.py
Normal file
71
app/modules/monitoring/models/capacity_snapshot.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# app/modules/monitoring/models/capacity_snapshot.py
|
||||||
|
"""
|
||||||
|
Capacity snapshot model for platform capacity monitoring.
|
||||||
|
|
||||||
|
Stores daily snapshots of platform metrics for growth trending and capacity forecasting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
Index,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
)
|
||||||
|
from sqlalchemy.dialects.sqlite import JSON
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
from models.database.base import TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class CapacitySnapshot(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Daily snapshot of platform capacity metrics.
|
||||||
|
|
||||||
|
Used for growth trending and capacity forecasting.
|
||||||
|
Captured daily by background job.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "capacity_snapshots"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
snapshot_date = Column(DateTime(timezone=True), nullable=False, unique=True, index=True)
|
||||||
|
|
||||||
|
# Store metrics
|
||||||
|
total_stores = Column(Integer, default=0, nullable=False)
|
||||||
|
active_stores = Column(Integer, default=0, nullable=False)
|
||||||
|
trial_stores = Column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
|
# Subscription metrics
|
||||||
|
total_subscriptions = Column(Integer, default=0, nullable=False)
|
||||||
|
active_subscriptions = Column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
|
# Resource metrics
|
||||||
|
total_products = Column(Integer, default=0, nullable=False)
|
||||||
|
total_orders_month = Column(Integer, default=0, nullable=False)
|
||||||
|
total_team_members = Column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
|
# Storage metrics
|
||||||
|
storage_used_gb = Column(Numeric(10, 2), default=0, nullable=False)
|
||||||
|
db_size_mb = Column(Numeric(10, 2), default=0, nullable=False)
|
||||||
|
|
||||||
|
# Capacity metrics (theoretical limits from subscriptions)
|
||||||
|
theoretical_products_limit = Column(Integer, nullable=True)
|
||||||
|
theoretical_orders_limit = Column(Integer, nullable=True)
|
||||||
|
theoretical_team_limit = Column(Integer, nullable=True)
|
||||||
|
|
||||||
|
# Tier distribution (JSON: {"essential": 10, "professional": 5, ...})
|
||||||
|
tier_distribution = Column(JSON, nullable=True)
|
||||||
|
|
||||||
|
# Performance metrics
|
||||||
|
avg_response_ms = Column(Integer, nullable=True)
|
||||||
|
peak_cpu_percent = Column(Numeric(5, 2), nullable=True)
|
||||||
|
peak_memory_percent = Column(Numeric(5, 2), nullable=True)
|
||||||
|
|
||||||
|
# Indexes
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_capacity_snapshots_date", "snapshot_date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<CapacitySnapshot(date={self.snapshot_date}, stores={self.total_stores})>"
|
||||||
@@ -172,7 +172,7 @@ async def get_growth_trends(
|
|||||||
|
|
||||||
Returns growth rates and projections for key metrics.
|
Returns growth rates and projections for key metrics.
|
||||||
"""
|
"""
|
||||||
from app.modules.billing.services.capacity_forecast_service import (
|
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||||
capacity_forecast_service,
|
capacity_forecast_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ async def get_scaling_recommendations(
|
|||||||
|
|
||||||
Returns prioritized list of recommendations.
|
Returns prioritized list of recommendations.
|
||||||
"""
|
"""
|
||||||
from app.modules.billing.services.capacity_forecast_service import (
|
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||||
capacity_forecast_service,
|
capacity_forecast_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ async def capture_snapshot(
|
|||||||
|
|
||||||
Normally run automatically by daily background job.
|
Normally run automatically by daily background job.
|
||||||
"""
|
"""
|
||||||
from app.modules.billing.services.capacity_forecast_service import (
|
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||||
capacity_forecast_service,
|
capacity_forecast_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ from app.modules.monitoring.services.admin_audit_service import (
|
|||||||
AdminAuditService,
|
AdminAuditService,
|
||||||
admin_audit_service,
|
admin_audit_service,
|
||||||
)
|
)
|
||||||
|
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||||
|
CapacityForecastService,
|
||||||
|
capacity_forecast_service,
|
||||||
|
)
|
||||||
from app.modules.monitoring.services.background_tasks_service import (
|
from app.modules.monitoring.services.background_tasks_service import (
|
||||||
BackgroundTasksService,
|
BackgroundTasksService,
|
||||||
background_tasks_service,
|
background_tasks_service,
|
||||||
@@ -25,6 +29,8 @@ from app.modules.monitoring.services.platform_health_service import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"admin_audit_service",
|
"admin_audit_service",
|
||||||
"AdminAuditService",
|
"AdminAuditService",
|
||||||
|
"capacity_forecast_service",
|
||||||
|
"CapacityForecastService",
|
||||||
"background_tasks_service",
|
"background_tasks_service",
|
||||||
"BackgroundTasksService",
|
"BackgroundTasksService",
|
||||||
"log_service",
|
"log_service",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# app/modules/billing/services/capacity_forecast_service.py
|
# app/modules/monitoring/services/capacity_forecast_service.py
|
||||||
"""
|
"""
|
||||||
Capacity forecasting service for growth trends and scaling recommendations.
|
Capacity forecasting service for growth trends and scaling recommendations.
|
||||||
|
|
||||||
@@ -16,13 +16,9 @@ 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.billing.models import (
|
|
||||||
CapacitySnapshot,
|
|
||||||
MerchantSubscription,
|
|
||||||
SubscriptionStatus,
|
|
||||||
)
|
|
||||||
from app.modules.contracts.metrics import MetricsContext
|
from app.modules.contracts.metrics import MetricsContext
|
||||||
from app.modules.core.services.stats_aggregator import stats_aggregator
|
from app.modules.core.services.stats_aggregator import stats_aggregator
|
||||||
|
from app.modules.monitoring.models.capacity_snapshot import CapacitySnapshot
|
||||||
from app.modules.tenancy.models import Platform, Store, StoreUser
|
from app.modules.tenancy.models import Platform, Store, StoreUser
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -75,22 +71,7 @@ class CapacityForecastService:
|
|||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subscription metrics
|
# Resource metrics via provider pattern (avoids cross-module imports)
|
||||||
total_subs = db.query(func.count(MerchantSubscription.id)).scalar() or 0
|
|
||||||
active_subs = (
|
|
||||||
db.query(func.count(MerchantSubscription.id))
|
|
||||||
.filter(MerchantSubscription.status.in_(["active", "trial"]))
|
|
||||||
.scalar()
|
|
||||||
or 0
|
|
||||||
)
|
|
||||||
trial_stores = (
|
|
||||||
db.query(func.count(MerchantSubscription.id))
|
|
||||||
.filter(MerchantSubscription.status == SubscriptionStatus.TRIAL.value)
|
|
||||||
.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)
|
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
platform = db.query(Platform).first()
|
platform = db.query(Platform).first()
|
||||||
platform_id = platform.id if platform else 1
|
platform_id = platform.id if platform else 1
|
||||||
@@ -100,6 +81,11 @@ class CapacityForecastService:
|
|||||||
context=MetricsContext(date_from=start_of_month),
|
context=MetricsContext(date_from=start_of_month),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Subscription metrics via stats aggregator (avoids billing → monitoring violation)
|
||||||
|
total_subs = stats.get("billing.total_subscriptions", 0)
|
||||||
|
active_subs = stats.get("billing.active_subscriptions", 0)
|
||||||
|
trial_stores = stats.get("billing.trial_subscriptions", 0)
|
||||||
|
|
||||||
total_products = stats.get("catalog.total_products", 0)
|
total_products = stats.get("catalog.total_products", 0)
|
||||||
total_team = (
|
total_team = (
|
||||||
db.query(func.count(StoreUser.id))
|
db.query(func.count(StoreUser.id))
|
||||||
@@ -27,7 +27,7 @@ def capture_capacity_snapshot(self):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Snapshot summary with store and product counts.
|
dict: Snapshot summary with store and product counts.
|
||||||
"""
|
"""
|
||||||
from app.modules.billing.services.capacity_forecast_service import (
|
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||||
capacity_forecast_service,
|
capacity_forecast_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
0
app/modules/monitoring/tests/__init__.py
Normal file
0
app/modules/monitoring/tests/__init__.py
Normal file
0
app/modules/monitoring/tests/unit/__init__.py
Normal file
0
app/modules/monitoring/tests/unit/__init__.py
Normal file
@@ -1,4 +1,4 @@
|
|||||||
# tests/unit/services/test_capacity_forecast_service.py
|
# app/modules/monitoring/tests/unit/test_capacity_forecast_service.py
|
||||||
"""
|
"""
|
||||||
Unit tests for CapacityForecastService.
|
Unit tests for CapacityForecastService.
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.modules.billing.models import CapacitySnapshot
|
from app.modules.monitoring.models import CapacitySnapshot
|
||||||
from app.modules.billing.services.capacity_forecast_service import (
|
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||||
INFRASTRUCTURE_SCALING,
|
INFRASTRUCTURE_SCALING,
|
||||||
CapacityForecastService,
|
CapacityForecastService,
|
||||||
capacity_forecast_service,
|
capacity_forecast_service,
|
||||||
@@ -312,7 +312,10 @@ class TestPlatformContextManager:
|
|||||||
def test_get_platform_from_domain_not_found(self):
|
def test_get_platform_from_domain_not_found(self):
|
||||||
"""Test domain lookup when platform not found."""
|
"""Test domain lookup when platform not found."""
|
||||||
mock_db = Mock(spec=Session)
|
mock_db = Mock(spec=Session)
|
||||||
mock_db.query.return_value.filter.return_value.filter.return_value.first.return_value = None
|
# Ensure all query chain variants return None for .first()
|
||||||
|
query_mock = mock_db.query.return_value
|
||||||
|
query_mock.filter.return_value.first.return_value = None
|
||||||
|
query_mock.filter.return_value.filter.return_value.first.return_value = None
|
||||||
|
|
||||||
context = {"detection_method": "domain", "domain": "unknown.lu"}
|
context = {"detection_method": "domain", "domain": "unknown.lu"}
|
||||||
|
|
||||||
@@ -367,8 +370,11 @@ class TestPlatformContextManager:
|
|||||||
def test_get_platform_inactive_not_returned(self):
|
def test_get_platform_inactive_not_returned(self):
|
||||||
"""Test that inactive platforms are not returned."""
|
"""Test that inactive platforms are not returned."""
|
||||||
mock_db = Mock(spec=Session)
|
mock_db = Mock(spec=Session)
|
||||||
# First call returns None (is_active filter excludes it)
|
# Ensure all query chain variants return None for .first()
|
||||||
mock_db.query.return_value.filter.return_value.filter.return_value.first.return_value = None
|
# (primary Platform lookup and StoreDomain/MerchantDomain fallbacks)
|
||||||
|
query_mock = mock_db.query.return_value
|
||||||
|
query_mock.filter.return_value.first.return_value = None
|
||||||
|
query_mock.filter.return_value.filter.return_value.first.return_value = None
|
||||||
|
|
||||||
context = {"detection_method": "domain", "domain": "inactive.lu"}
|
context = {"detection_method": "domain", "domain": "inactive.lu"}
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,13 @@ class TestStoreContextManager:
|
|||||||
def test_get_store_from_custom_domain_not_found(self):
|
def test_get_store_from_custom_domain_not_found(self):
|
||||||
"""Test custom domain not found in database."""
|
"""Test custom domain not found in database."""
|
||||||
mock_db = Mock(spec=Session)
|
mock_db = Mock(spec=Session)
|
||||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = None
|
# Ensure all query chain variants return None for .first()
|
||||||
|
# (primary StoreDomain lookup and MerchantDomain fallback)
|
||||||
|
query_mock = mock_db.query.return_value
|
||||||
|
query_mock.filter.return_value.first.return_value = None
|
||||||
|
query_mock.filter.return_value.filter.return_value.first.return_value = None
|
||||||
|
query_mock.filter.return_value.filter.return_value.filter.return_value.first.return_value = None
|
||||||
|
query_mock.filter.return_value.order_by.return_value.first.return_value = None
|
||||||
|
|
||||||
context = {"detection_method": "custom_domain", "domain": "nonexistent.com"}
|
context = {"detection_method": "custom_domain", "domain": "nonexistent.com"}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user