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:
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
@@ -0,0 +1,334 @@
|
||||
# app/modules/monitoring/tests/unit/test_capacity_forecast_service.py
|
||||
"""
|
||||
Unit tests for CapacityForecastService.
|
||||
|
||||
Tests cover:
|
||||
- Daily snapshot capture
|
||||
- Growth trend calculation
|
||||
- Scaling recommendations
|
||||
- Days until threshold calculation
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.monitoring.models import CapacitySnapshot
|
||||
from app.modules.monitoring.services.capacity_forecast_service import (
|
||||
INFRASTRUCTURE_SCALING,
|
||||
CapacityForecastService,
|
||||
capacity_forecast_service,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.service
|
||||
class TestCapacityForecastServiceSnapshot:
|
||||
"""Test snapshot capture functionality"""
|
||||
|
||||
def test_capture_daily_snapshot_returns_existing(self, db):
|
||||
"""Test capture_daily_snapshot returns existing snapshot for today"""
|
||||
now = datetime.now(UTC)
|
||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Create existing snapshot
|
||||
existing = CapacitySnapshot(
|
||||
snapshot_date=today,
|
||||
total_stores=10,
|
||||
active_stores=8,
|
||||
trial_stores=2,
|
||||
total_subscriptions=10,
|
||||
active_subscriptions=8,
|
||||
total_products=1000,
|
||||
total_orders_month=500,
|
||||
total_team_members=20,
|
||||
storage_used_gb=Decimal("50.0"),
|
||||
db_size_mb=Decimal("100.0"),
|
||||
theoretical_products_limit=10000,
|
||||
theoretical_orders_limit=5000,
|
||||
theoretical_team_limit=100,
|
||||
tier_distribution={"starter": 5},
|
||||
)
|
||||
db.add(existing)
|
||||
db.commit()
|
||||
|
||||
service = CapacityForecastService()
|
||||
result = service.capture_daily_snapshot(db)
|
||||
|
||||
assert result.id == existing.id
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.service
|
||||
class TestCapacityForecastServiceTrends:
|
||||
"""Test growth trend functionality"""
|
||||
|
||||
def test_get_growth_trends_insufficient_data(self, db):
|
||||
"""Test get_growth_trends returns message when insufficient data"""
|
||||
service = CapacityForecastService()
|
||||
result = service.get_growth_trends(db, days=30)
|
||||
|
||||
assert result["snapshots_available"] < 2
|
||||
assert "Insufficient data" in result.get("message", "")
|
||||
|
||||
def test_get_growth_trends_with_data(self, db):
|
||||
"""Test get_growth_trends calculates trends correctly"""
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# Create two snapshots
|
||||
snapshot1 = CapacitySnapshot(
|
||||
snapshot_date=now - timedelta(days=30),
|
||||
total_stores=10,
|
||||
active_stores=8,
|
||||
trial_stores=2,
|
||||
total_subscriptions=10,
|
||||
active_subscriptions=8,
|
||||
total_products=1000,
|
||||
total_orders_month=500,
|
||||
total_team_members=20,
|
||||
storage_used_gb=Decimal("50.0"),
|
||||
db_size_mb=Decimal("100.0"),
|
||||
theoretical_products_limit=10000,
|
||||
theoretical_orders_limit=5000,
|
||||
theoretical_team_limit=100,
|
||||
tier_distribution={"starter": 5},
|
||||
)
|
||||
snapshot2 = CapacitySnapshot(
|
||||
snapshot_date=now.replace(hour=0, minute=0, second=0, microsecond=0),
|
||||
total_stores=15,
|
||||
active_stores=12,
|
||||
trial_stores=3,
|
||||
total_subscriptions=15,
|
||||
active_subscriptions=12,
|
||||
total_products=1500,
|
||||
total_orders_month=750,
|
||||
total_team_members=30,
|
||||
storage_used_gb=Decimal("75.0"),
|
||||
db_size_mb=Decimal("150.0"),
|
||||
theoretical_products_limit=15000,
|
||||
theoretical_orders_limit=7500,
|
||||
theoretical_team_limit=150,
|
||||
tier_distribution={"starter": 8, "professional": 4},
|
||||
)
|
||||
db.add(snapshot1)
|
||||
db.add(snapshot2)
|
||||
db.commit()
|
||||
|
||||
service = CapacityForecastService()
|
||||
result = service.get_growth_trends(db, days=60)
|
||||
|
||||
assert result["snapshots_available"] >= 2
|
||||
assert "trends" in result
|
||||
assert "stores" in result["trends"]
|
||||
assert result["trends"]["stores"]["start_value"] == 8
|
||||
assert result["trends"]["stores"]["current_value"] == 12
|
||||
|
||||
def test_get_growth_trends_zero_start_value(self, db):
|
||||
"""Test get_growth_trends handles zero start value"""
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# Create snapshots with zero start value
|
||||
snapshot1 = CapacitySnapshot(
|
||||
snapshot_date=now - timedelta(days=30),
|
||||
total_stores=0,
|
||||
active_stores=0,
|
||||
trial_stores=0,
|
||||
total_subscriptions=0,
|
||||
active_subscriptions=0,
|
||||
total_products=0,
|
||||
total_orders_month=0,
|
||||
total_team_members=0,
|
||||
storage_used_gb=Decimal("0"),
|
||||
db_size_mb=Decimal("0"),
|
||||
theoretical_products_limit=0,
|
||||
theoretical_orders_limit=0,
|
||||
theoretical_team_limit=0,
|
||||
tier_distribution={},
|
||||
)
|
||||
snapshot2 = CapacitySnapshot(
|
||||
snapshot_date=now.replace(hour=0, minute=0, second=0, microsecond=0),
|
||||
total_stores=10,
|
||||
active_stores=8,
|
||||
trial_stores=2,
|
||||
total_subscriptions=10,
|
||||
active_subscriptions=8,
|
||||
total_products=1000,
|
||||
total_orders_month=500,
|
||||
total_team_members=20,
|
||||
storage_used_gb=Decimal("50.0"),
|
||||
db_size_mb=Decimal("100.0"),
|
||||
theoretical_products_limit=10000,
|
||||
theoretical_orders_limit=5000,
|
||||
theoretical_team_limit=100,
|
||||
tier_distribution={"starter": 5},
|
||||
)
|
||||
db.add(snapshot1)
|
||||
db.add(snapshot2)
|
||||
db.commit()
|
||||
|
||||
service = CapacityForecastService()
|
||||
result = service.get_growth_trends(db, days=60)
|
||||
|
||||
assert result["snapshots_available"] >= 2
|
||||
# When start is 0 and end is not 0, growth should be 100%
|
||||
assert result["trends"]["stores"]["growth_rate_percent"] == 100
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.service
|
||||
class TestCapacityForecastServiceRecommendations:
|
||||
"""Test scaling recommendations functionality"""
|
||||
|
||||
def test_get_scaling_recommendations_returns_list(self, db):
|
||||
"""Test get_scaling_recommendations returns a list"""
|
||||
service = CapacityForecastService()
|
||||
try:
|
||||
result = service.get_scaling_recommendations(db)
|
||||
assert isinstance(result, list)
|
||||
except Exception:
|
||||
# May fail if health service dependencies are not set up
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.service
|
||||
class TestCapacityForecastServiceThreshold:
|
||||
"""Test days until threshold functionality"""
|
||||
|
||||
def test_get_days_until_threshold_insufficient_data(self, db):
|
||||
"""Test get_days_until_threshold returns None with insufficient data"""
|
||||
service = CapacityForecastService()
|
||||
result = service.get_days_until_threshold(db, "stores", 100)
|
||||
assert result is None
|
||||
|
||||
def test_get_days_until_threshold_no_growth(self, db):
|
||||
"""Test get_days_until_threshold returns None with no growth"""
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# Create two snapshots with no growth
|
||||
snapshot1 = CapacitySnapshot(
|
||||
snapshot_date=now - timedelta(days=30),
|
||||
total_stores=10,
|
||||
active_stores=10,
|
||||
trial_stores=0,
|
||||
total_subscriptions=10,
|
||||
active_subscriptions=10,
|
||||
total_products=1000,
|
||||
total_orders_month=500,
|
||||
total_team_members=20,
|
||||
storage_used_gb=Decimal("50.0"),
|
||||
db_size_mb=Decimal("100.0"),
|
||||
theoretical_products_limit=10000,
|
||||
theoretical_orders_limit=5000,
|
||||
theoretical_team_limit=100,
|
||||
tier_distribution={},
|
||||
)
|
||||
snapshot2 = CapacitySnapshot(
|
||||
snapshot_date=now.replace(hour=0, minute=0, second=0, microsecond=0),
|
||||
total_stores=10,
|
||||
active_stores=10, # Same as before
|
||||
trial_stores=0,
|
||||
total_subscriptions=10,
|
||||
active_subscriptions=10,
|
||||
total_products=1000,
|
||||
total_orders_month=500,
|
||||
total_team_members=20,
|
||||
storage_used_gb=Decimal("50.0"),
|
||||
db_size_mb=Decimal("100.0"),
|
||||
theoretical_products_limit=10000,
|
||||
theoretical_orders_limit=5000,
|
||||
theoretical_team_limit=100,
|
||||
tier_distribution={},
|
||||
)
|
||||
db.add(snapshot1)
|
||||
db.add(snapshot2)
|
||||
db.commit()
|
||||
|
||||
service = CapacityForecastService()
|
||||
result = service.get_days_until_threshold(db, "stores", 100)
|
||||
assert result is None
|
||||
|
||||
def test_get_days_until_threshold_already_exceeded(self, db):
|
||||
"""Test get_days_until_threshold returns None when already at threshold"""
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# Create two snapshots where current value exceeds threshold
|
||||
snapshot1 = CapacitySnapshot(
|
||||
snapshot_date=now - timedelta(days=30),
|
||||
total_stores=80,
|
||||
active_stores=80,
|
||||
trial_stores=0,
|
||||
total_subscriptions=80,
|
||||
active_subscriptions=80,
|
||||
total_products=8000,
|
||||
total_orders_month=4000,
|
||||
total_team_members=160,
|
||||
storage_used_gb=Decimal("400.0"),
|
||||
db_size_mb=Decimal("800.0"),
|
||||
theoretical_products_limit=80000,
|
||||
theoretical_orders_limit=40000,
|
||||
theoretical_team_limit=800,
|
||||
tier_distribution={},
|
||||
)
|
||||
snapshot2 = CapacitySnapshot(
|
||||
snapshot_date=now.replace(hour=0, minute=0, second=0, microsecond=0),
|
||||
total_stores=120,
|
||||
active_stores=120, # Already exceeds threshold of 100
|
||||
trial_stores=0,
|
||||
total_subscriptions=120,
|
||||
active_subscriptions=120,
|
||||
total_products=12000,
|
||||
total_orders_month=6000,
|
||||
total_team_members=240,
|
||||
storage_used_gb=Decimal("600.0"),
|
||||
db_size_mb=Decimal("1200.0"),
|
||||
theoretical_products_limit=120000,
|
||||
theoretical_orders_limit=60000,
|
||||
theoretical_team_limit=1200,
|
||||
tier_distribution={},
|
||||
)
|
||||
db.add(snapshot1)
|
||||
db.add(snapshot2)
|
||||
db.commit()
|
||||
|
||||
service = CapacityForecastService()
|
||||
result = service.get_days_until_threshold(db, "stores", 100)
|
||||
# Should return None since we're already past the threshold
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.service
|
||||
class TestInfrastructureScaling:
|
||||
"""Test infrastructure scaling constants"""
|
||||
|
||||
def test_infrastructure_scaling_defined(self):
|
||||
"""Test INFRASTRUCTURE_SCALING is properly defined"""
|
||||
assert len(INFRASTRUCTURE_SCALING) > 0
|
||||
|
||||
# Verify structure
|
||||
for tier in INFRASTRUCTURE_SCALING:
|
||||
assert "name" in tier
|
||||
assert "max_stores" in tier
|
||||
assert "max_products" in tier
|
||||
assert "cost_monthly" in tier
|
||||
|
||||
def test_infrastructure_scaling_ordered(self):
|
||||
"""Test INFRASTRUCTURE_SCALING is ordered by size"""
|
||||
# Cost should increase with each tier
|
||||
for i in range(1, len(INFRASTRUCTURE_SCALING)):
|
||||
current = INFRASTRUCTURE_SCALING[i]
|
||||
previous = INFRASTRUCTURE_SCALING[i - 1]
|
||||
assert current["cost_monthly"] > previous["cost_monthly"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.service
|
||||
class TestCapacityForecastServiceSingleton:
|
||||
"""Test singleton instance"""
|
||||
|
||||
def test_singleton_exists(self):
|
||||
"""Test capacity_forecast_service singleton exists"""
|
||||
assert capacity_forecast_service is not None
|
||||
assert isinstance(capacity_forecast_service, CapacityForecastService)
|
||||
Reference in New Issue
Block a user