Files
orion/tests/unit/services/test_usage_service.py
Samir Boulahtit d7a0ff8818 refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:02:56 +01:00

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, VendorSubscription
from app.modules.tenancy.models import VendorUser
@pytest.mark.unit
@pytest.mark.usage
class TestUsageServiceGetUsage:
"""Test suite for get_vendor_usage operation."""
def setup_method(self):
"""Initialize service instance before each test."""
self.service = UsageService()
def test_get_vendor_usage_basic(self, db, test_vendor_with_subscription):
"""Test getting basic usage data."""
vendor_id = test_vendor_with_subscription.id
usage = self.service.get_vendor_usage(db, vendor_id)
assert usage.tier.code == "essential"
assert usage.tier.name == "Essential"
assert len(usage.usage) == 3
def test_get_vendor_usage_metrics(self, db, test_vendor_with_subscription):
"""Test usage metrics are calculated correctly."""
vendor_id = test_vendor_with_subscription.id
usage = self.service.get_vendor_usage(db, vendor_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_vendor_usage_at_limit(self, db, test_vendor_at_limit):
"""Test usage shows at limit correctly."""
vendor_id = test_vendor_at_limit.id
usage = self.service.get_vendor_usage(db, vendor_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_vendor_usage_approaching_limit(self, db, test_vendor_approaching_limit):
"""Test usage shows approaching limit correctly."""
vendor_id = test_vendor_approaching_limit.id
usage = self.service.get_vendor_usage(db, vendor_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_vendor_usage_upgrade_available(
self, db, test_vendor_with_subscription, test_professional_tier
):
"""Test upgrade info when not on highest tier."""
vendor_id = test_vendor_with_subscription.id
usage = self.service.get_vendor_usage(db, vendor_id)
assert usage.upgrade_available is True
assert usage.upgrade_tier is not None
assert usage.upgrade_tier.code == "professional"
def test_get_vendor_usage_highest_tier(self, db, test_vendor_on_professional):
"""Test no upgrade when on highest tier."""
vendor_id = test_vendor_on_professional.id
usage = self.service.get_vendor_usage(db, vendor_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_vendor_with_subscription):
"""Test checking orders limit when under limit."""
vendor_id = test_vendor_with_subscription.id
result = self.service.check_limit(db, vendor_id, "orders")
assert result.can_proceed is True
assert result.current == 10
assert result.limit == 100
def test_check_products_limit(self, db, test_vendor_with_products):
"""Test checking products limit."""
vendor_id = test_vendor_with_products.id
result = self.service.check_limit(db, vendor_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_vendor_with_team):
"""Test checking team members limit when at limit."""
vendor_id = test_vendor_with_team.id
result = self.service.check_limit(db, vendor_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_vendor_with_subscription):
"""Test checking unknown limit type."""
vendor_id = test_vendor_with_subscription.id
result = self.service.check_limit(db, vendor_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_vendor_at_limit):
"""Test upgrade info is provided when at limit."""
vendor_id = test_vendor_at_limit.id
result = self.service.check_limit(db, vendor_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_vendor_with_subscription(db, test_vendor, test_essential_tier):
"""Create vendor with active subscription."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
subscription = VendorSubscription(
vendor_id=test_vendor.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_vendor)
return test_vendor
@pytest.fixture
def test_vendor_at_limit(db, test_vendor, test_essential_tier, test_professional_tier):
"""Create vendor at order limit."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
subscription = VendorSubscription(
vendor_id=test_vendor.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_vendor)
return test_vendor
@pytest.fixture
def test_vendor_approaching_limit(db, test_vendor, test_essential_tier):
"""Create vendor approaching order limit (>=80%)."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
subscription = VendorSubscription(
vendor_id=test_vendor.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_vendor)
return test_vendor
@pytest.fixture
def test_vendor_on_professional(db, test_vendor, test_professional_tier):
"""Create vendor on highest tier."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
subscription = VendorSubscription(
vendor_id=test_vendor.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_vendor)
return test_vendor
@pytest.fixture
def test_vendor_with_products(db, test_vendor_with_subscription, marketplace_product_factory):
"""Create vendor with products."""
for i in range(5):
# Create marketplace product first
mp = marketplace_product_factory(db, title=f"Test Product {i}")
product = Product(
vendor_id=test_vendor_with_subscription.id,
marketplace_product_id=mp.id,
price_cents=1000,
is_active=True,
)
db.add(product)
db.commit()
return test_vendor_with_subscription
@pytest.fixture
def test_vendor_with_team(db, test_vendor_with_subscription, test_user, other_user):
"""Create vendor with team members (owner + team member = 2)."""
from app.modules.tenancy.models import VendorUserType
# Add owner
owner = VendorUser(
vendor_id=test_vendor_with_subscription.id,
user_id=test_user.id,
user_type=VendorUserType.OWNER.value,
is_active=True,
)
db.add(owner)
# Add team member
team_member = VendorUser(
vendor_id=test_vendor_with_subscription.id,
user_id=other_user.id,
user_type=VendorUserType.TEAM_MEMBER.value,
is_active=True,
)
db.add(team_member)
db.commit()
return test_vendor_with_subscription