Replace hardcoded subscription fields (orders_limit, products_limit,
team_members_limit) across 5 frontend pages with dynamic feature
provider APIs. Add admin convenience endpoint for store subscription
lookup. Remove legacy stubs (StoreSubscription, FeatureCode, Feature,
TIER_LIMITS, FeatureInfo, FeatureUpgradeInfo) and schema aliases.
Pages updated:
- Admin subscriptions: dynamic feature overrides editor
- Admin tiers: correct feature catalog/limits API URLs
- Store billing: usage metrics from /store/billing/usage
- Merchant subscription detail: tier.feature_limits rendering
- Admin store detail: new GET /admin/subscriptions/store/{id} endpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
309 lines
10 KiB
Python
309 lines
10 KiB
Python
# tests/unit/services/test_usage_service.py
|
|
"""Unit tests for UsageService."""
|
|
|
|
import pytest
|
|
|
|
from app.modules.analytics.services.usage_service import UsageService, usage_service
|
|
from app.modules.catalog.models import Product
|
|
from app.modules.billing.models import SubscriptionTier, MerchantSubscription
|
|
from app.modules.tenancy.models import StoreUser
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.usage
|
|
class TestUsageServiceGetUsage:
|
|
"""Test suite for get_store_usage operation."""
|
|
|
|
def setup_method(self):
|
|
"""Initialize service instance before each test."""
|
|
self.service = UsageService()
|
|
|
|
def test_get_store_usage_basic(self, db, test_store_with_subscription):
|
|
"""Test getting basic usage data."""
|
|
store_id = test_store_with_subscription.id
|
|
usage = self.service.get_store_usage(db, store_id)
|
|
|
|
assert usage.tier.code == "essential"
|
|
assert usage.tier.name == "Essential"
|
|
assert len(usage.usage) == 3
|
|
|
|
def test_get_store_usage_metrics(self, db, test_store_with_subscription):
|
|
"""Test usage metrics are calculated correctly."""
|
|
store_id = test_store_with_subscription.id
|
|
usage = self.service.get_store_usage(db, store_id)
|
|
|
|
orders_metric = next((m for m in usage.usage if m.name == "orders"), None)
|
|
assert orders_metric is not None
|
|
assert orders_metric.current == 10
|
|
assert orders_metric.limit == 100
|
|
assert orders_metric.percentage == 10.0
|
|
assert orders_metric.is_unlimited is False
|
|
|
|
def test_get_store_usage_at_limit(self, db, test_store_at_limit):
|
|
"""Test usage shows at limit correctly."""
|
|
store_id = test_store_at_limit.id
|
|
usage = self.service.get_store_usage(db, store_id)
|
|
|
|
orders_metric = next((m for m in usage.usage if m.name == "orders"), None)
|
|
assert orders_metric.is_at_limit is True
|
|
assert usage.has_limits_reached is True
|
|
|
|
def test_get_store_usage_approaching_limit(self, db, test_store_approaching_limit):
|
|
"""Test usage shows approaching limit correctly."""
|
|
store_id = test_store_approaching_limit.id
|
|
usage = self.service.get_store_usage(db, store_id)
|
|
|
|
orders_metric = next((m for m in usage.usage if m.name == "orders"), None)
|
|
assert orders_metric.is_approaching_limit is True
|
|
assert usage.has_limits_approaching is True
|
|
|
|
def test_get_store_usage_upgrade_available(
|
|
self, db, test_store_with_subscription, test_professional_tier
|
|
):
|
|
"""Test upgrade info when not on highest tier."""
|
|
store_id = test_store_with_subscription.id
|
|
usage = self.service.get_store_usage(db, store_id)
|
|
|
|
assert usage.upgrade_available is True
|
|
assert usage.upgrade_tier is not None
|
|
assert usage.upgrade_tier.code == "professional"
|
|
|
|
def test_get_store_usage_highest_tier(self, db, test_store_on_professional):
|
|
"""Test no upgrade when on highest tier."""
|
|
store_id = test_store_on_professional.id
|
|
usage = self.service.get_store_usage(db, store_id)
|
|
|
|
assert usage.tier.is_highest_tier is True
|
|
assert usage.upgrade_available is False
|
|
assert usage.upgrade_tier is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.usage
|
|
class TestUsageServiceCheckLimit:
|
|
"""Test suite for check_limit operation."""
|
|
|
|
def setup_method(self):
|
|
"""Initialize service instance before each test."""
|
|
self.service = UsageService()
|
|
|
|
def test_check_orders_limit_can_proceed(self, db, test_store_with_subscription):
|
|
"""Test checking orders limit when under limit."""
|
|
store_id = test_store_with_subscription.id
|
|
result = self.service.check_limit(db, store_id, "orders")
|
|
|
|
assert result.can_proceed is True
|
|
assert result.current == 10
|
|
assert result.limit == 100
|
|
|
|
def test_check_products_limit(self, db, test_store_with_products):
|
|
"""Test checking products limit."""
|
|
store_id = test_store_with_products.id
|
|
result = self.service.check_limit(db, store_id, "products")
|
|
|
|
assert result.can_proceed is True
|
|
assert result.current == 5
|
|
assert result.limit == 500
|
|
|
|
def test_check_team_members_limit(self, db, test_store_with_team):
|
|
"""Test checking team members limit when at limit."""
|
|
store_id = test_store_with_team.id
|
|
result = self.service.check_limit(db, store_id, "team_members")
|
|
|
|
# At limit (2/2) - can_proceed should be False
|
|
assert result.can_proceed is False
|
|
assert result.current == 2
|
|
assert result.limit == 2
|
|
assert result.percentage == 100.0
|
|
|
|
def test_check_unknown_limit_type(self, db, test_store_with_subscription):
|
|
"""Test checking unknown limit type."""
|
|
store_id = test_store_with_subscription.id
|
|
result = self.service.check_limit(db, store_id, "unknown")
|
|
|
|
assert result.can_proceed is True
|
|
assert "Unknown limit type" in result.message
|
|
|
|
def test_check_limit_upgrade_info_when_blocked(self, db, test_store_at_limit):
|
|
"""Test upgrade info is provided when at limit."""
|
|
store_id = test_store_at_limit.id
|
|
result = self.service.check_limit(db, store_id, "orders")
|
|
|
|
assert result.can_proceed is False
|
|
assert result.upgrade_tier_code == "professional"
|
|
assert result.upgrade_tier_name == "Professional"
|
|
|
|
|
|
# ==================== Fixtures ====================
|
|
|
|
|
|
@pytest.fixture
|
|
def test_essential_tier(db):
|
|
"""Create essential subscription tier."""
|
|
tier = SubscriptionTier(
|
|
code="essential",
|
|
name="Essential",
|
|
description="Essential plan",
|
|
price_monthly_cents=4900,
|
|
price_annual_cents=49000,
|
|
orders_per_month=100,
|
|
products_limit=500,
|
|
team_members=2,
|
|
features=["basic_reports"],
|
|
is_active=True,
|
|
display_order=1,
|
|
)
|
|
db.add(tier)
|
|
db.commit()
|
|
db.refresh(tier)
|
|
return tier
|
|
|
|
|
|
@pytest.fixture
|
|
def test_professional_tier(db, test_essential_tier):
|
|
"""Create professional subscription tier."""
|
|
tier = SubscriptionTier(
|
|
code="professional",
|
|
name="Professional",
|
|
description="Professional plan",
|
|
price_monthly_cents=9900,
|
|
price_annual_cents=99000,
|
|
orders_per_month=500,
|
|
products_limit=2000,
|
|
team_members=10,
|
|
features=["basic_reports", "api_access", "analytics_dashboard"],
|
|
is_active=True,
|
|
display_order=2,
|
|
)
|
|
db.add(tier)
|
|
db.commit()
|
|
db.refresh(tier)
|
|
return tier
|
|
|
|
|
|
@pytest.fixture
|
|
def test_store_with_subscription(db, test_store, test_essential_tier):
|
|
"""Create store with active subscription."""
|
|
from datetime import datetime, timezone
|
|
|
|
now = datetime.now(timezone.utc)
|
|
subscription = StoreSubscription(
|
|
store_id=test_store.id,
|
|
tier="essential",
|
|
tier_id=test_essential_tier.id,
|
|
status="active",
|
|
period_start=now,
|
|
period_end=now.replace(month=now.month + 1 if now.month < 12 else 1),
|
|
orders_this_period=10,
|
|
)
|
|
db.add(subscription)
|
|
db.commit()
|
|
db.refresh(test_store)
|
|
return test_store
|
|
|
|
|
|
@pytest.fixture
|
|
def test_store_at_limit(db, test_store, test_essential_tier, test_professional_tier):
|
|
"""Create store at order limit."""
|
|
from datetime import datetime, timezone
|
|
|
|
now = datetime.now(timezone.utc)
|
|
subscription = StoreSubscription(
|
|
store_id=test_store.id,
|
|
tier="essential",
|
|
tier_id=test_essential_tier.id,
|
|
status="active",
|
|
period_start=now,
|
|
period_end=now.replace(month=now.month + 1 if now.month < 12 else 1),
|
|
orders_this_period=100, # At limit
|
|
)
|
|
db.add(subscription)
|
|
db.commit()
|
|
db.refresh(test_store)
|
|
return test_store
|
|
|
|
|
|
@pytest.fixture
|
|
def test_store_approaching_limit(db, test_store, test_essential_tier):
|
|
"""Create store approaching order limit (>=80%)."""
|
|
from datetime import datetime, timezone
|
|
|
|
now = datetime.now(timezone.utc)
|
|
subscription = StoreSubscription(
|
|
store_id=test_store.id,
|
|
tier="essential",
|
|
tier_id=test_essential_tier.id,
|
|
status="active",
|
|
period_start=now,
|
|
period_end=now.replace(month=now.month + 1 if now.month < 12 else 1),
|
|
orders_this_period=85, # 85% of 100
|
|
)
|
|
db.add(subscription)
|
|
db.commit()
|
|
db.refresh(test_store)
|
|
return test_store
|
|
|
|
|
|
@pytest.fixture
|
|
def test_store_on_professional(db, test_store, test_professional_tier):
|
|
"""Create store on highest tier."""
|
|
from datetime import datetime, timezone
|
|
|
|
now = datetime.now(timezone.utc)
|
|
subscription = StoreSubscription(
|
|
store_id=test_store.id,
|
|
tier="professional",
|
|
tier_id=test_professional_tier.id,
|
|
status="active",
|
|
period_start=now,
|
|
period_end=now.replace(month=now.month + 1 if now.month < 12 else 1),
|
|
orders_this_period=50,
|
|
)
|
|
db.add(subscription)
|
|
db.commit()
|
|
db.refresh(test_store)
|
|
return test_store
|
|
|
|
|
|
@pytest.fixture
|
|
def test_store_with_products(db, test_store_with_subscription, marketplace_product_factory):
|
|
"""Create store with products."""
|
|
for i in range(5):
|
|
# Create marketplace product first
|
|
mp = marketplace_product_factory(db, title=f"Test Product {i}")
|
|
product = Product(
|
|
store_id=test_store_with_subscription.id,
|
|
marketplace_product_id=mp.id,
|
|
price_cents=1000,
|
|
is_active=True,
|
|
)
|
|
db.add(product)
|
|
db.commit()
|
|
return test_store_with_subscription
|
|
|
|
|
|
@pytest.fixture
|
|
def test_store_with_team(db, test_store_with_subscription, test_user, other_user):
|
|
"""Create store with team members (owner + team member = 2)."""
|
|
from app.modules.tenancy.models import StoreUserType
|
|
|
|
# Add owner
|
|
owner = StoreUser(
|
|
store_id=test_store_with_subscription.id,
|
|
user_id=test_user.id,
|
|
user_type=StoreUserType.OWNER.value,
|
|
is_active=True,
|
|
)
|
|
db.add(owner)
|
|
|
|
# Add team member
|
|
team_member = StoreUser(
|
|
store_id=test_store_with_subscription.id,
|
|
user_id=other_user.id,
|
|
user_type=StoreUserType.TEAM_MEMBER.value,
|
|
is_active=True,
|
|
)
|
|
db.add(team_member)
|
|
db.commit()
|
|
return test_store_with_subscription
|