feat(billing): migrate frontend templates to feature provider system
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>
This commit is contained in:
@@ -5,8 +5,7 @@ import pytest
|
||||
|
||||
from app.modules.billing.exceptions import FeatureNotFoundError, InvalidFeatureCodesError, TierNotFoundError
|
||||
from app.modules.billing.services.feature_service import FeatureService, feature_service
|
||||
from app.modules.billing.models import Feature
|
||||
from app.modules.billing.models import SubscriptionTier, VendorSubscription
|
||||
from app.modules.billing.models import SubscriptionTier, MerchantSubscription
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -18,27 +17,27 @@ class TestFeatureServiceAvailability:
|
||||
"""Initialize service instance before each test."""
|
||||
self.service = FeatureService()
|
||||
|
||||
def test_has_feature_true(self, db, test_vendor_with_subscription):
|
||||
def test_has_feature_true(self, db, test_store_with_subscription):
|
||||
"""Test has_feature returns True for available feature."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
result = self.service.has_feature(db, vendor_id, "basic_reports")
|
||||
store_id = test_store_with_subscription.id
|
||||
result = self.service.has_feature(db, store_id, "basic_reports")
|
||||
assert result is True
|
||||
|
||||
def test_has_feature_false(self, db, test_vendor_with_subscription):
|
||||
def test_has_feature_false(self, db, test_store_with_subscription):
|
||||
"""Test has_feature returns False for unavailable feature."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
result = self.service.has_feature(db, vendor_id, "api_access")
|
||||
store_id = test_store_with_subscription.id
|
||||
result = self.service.has_feature(db, store_id, "api_access")
|
||||
assert result is False
|
||||
|
||||
def test_has_feature_no_subscription(self, db, test_vendor):
|
||||
"""Test has_feature returns False for vendor without subscription."""
|
||||
result = self.service.has_feature(db, test_vendor.id, "basic_reports")
|
||||
def test_has_feature_no_subscription(self, db, test_store):
|
||||
"""Test has_feature returns False for store without subscription."""
|
||||
result = self.service.has_feature(db, test_store.id, "basic_reports")
|
||||
assert result is False
|
||||
|
||||
def test_get_vendor_feature_codes(self, db, test_vendor_with_subscription):
|
||||
"""Test getting all feature codes for vendor."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
features = self.service.get_vendor_feature_codes(db, vendor_id)
|
||||
def test_get_store_feature_codes(self, db, test_store_with_subscription):
|
||||
"""Test getting all feature codes for store."""
|
||||
store_id = test_store_with_subscription.id
|
||||
features = self.service.get_store_feature_codes(db, store_id)
|
||||
|
||||
assert isinstance(features, set)
|
||||
assert "basic_reports" in features
|
||||
@@ -54,10 +53,10 @@ class TestFeatureServiceListing:
|
||||
"""Initialize service instance before each test."""
|
||||
self.service = FeatureService()
|
||||
|
||||
def test_get_vendor_features(self, db, test_vendor_with_subscription, test_features):
|
||||
def test_get_store_features(self, db, test_store_with_subscription, test_features):
|
||||
"""Test getting all features with availability."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
features = self.service.get_vendor_features(db, vendor_id)
|
||||
store_id = test_store_with_subscription.id
|
||||
features = self.service.get_store_features(db, store_id)
|
||||
|
||||
assert len(features) > 0
|
||||
basic_reports = next((f for f in features if f.code == "basic_reports"), None)
|
||||
@@ -68,30 +67,30 @@ class TestFeatureServiceListing:
|
||||
assert api_access is not None
|
||||
assert api_access.is_available is False
|
||||
|
||||
def test_get_vendor_features_by_category(
|
||||
self, db, test_vendor_with_subscription, test_features
|
||||
def test_get_store_features_by_category(
|
||||
self, db, test_store_with_subscription, test_features
|
||||
):
|
||||
"""Test filtering features by category."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
features = self.service.get_vendor_features(db, vendor_id, category="analytics")
|
||||
store_id = test_store_with_subscription.id
|
||||
features = self.service.get_store_features(db, store_id, category="analytics")
|
||||
|
||||
assert all(f.category == "analytics" for f in features)
|
||||
|
||||
def test_get_vendor_features_available_only(
|
||||
self, db, test_vendor_with_subscription, test_features
|
||||
def test_get_store_features_available_only(
|
||||
self, db, test_store_with_subscription, test_features
|
||||
):
|
||||
"""Test getting only available features."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
features = self.service.get_vendor_features(
|
||||
db, vendor_id, include_unavailable=False
|
||||
store_id = test_store_with_subscription.id
|
||||
features = self.service.get_store_features(
|
||||
db, store_id, include_unavailable=False
|
||||
)
|
||||
|
||||
assert all(f.is_available for f in features)
|
||||
|
||||
def test_get_available_feature_codes(self, db, test_vendor_with_subscription):
|
||||
def test_get_available_feature_codes(self, db, test_store_with_subscription):
|
||||
"""Test getting simple list of available codes."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
codes = self.service.get_available_feature_codes(db, vendor_id)
|
||||
store_id = test_store_with_subscription.id
|
||||
codes = self.service.get_available_feature_codes(db, store_id)
|
||||
|
||||
assert isinstance(codes, list)
|
||||
assert "basic_reports" in codes
|
||||
@@ -159,28 +158,28 @@ class TestFeatureServiceCache:
|
||||
"""Initialize service instance before each test."""
|
||||
self.service = FeatureService()
|
||||
|
||||
def test_cache_invalidation(self, db, test_vendor_with_subscription):
|
||||
"""Test cache invalidation for vendor."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
def test_cache_invalidation(self, db, test_store_with_subscription):
|
||||
"""Test cache invalidation for store."""
|
||||
store_id = test_store_with_subscription.id
|
||||
|
||||
# Prime the cache
|
||||
self.service.get_vendor_feature_codes(db, vendor_id)
|
||||
assert self.service._cache.get(vendor_id) is not None
|
||||
self.service.get_store_feature_codes(db, store_id)
|
||||
assert self.service._cache.get(store_id) is not None
|
||||
|
||||
# Invalidate
|
||||
self.service.invalidate_vendor_cache(vendor_id)
|
||||
assert self.service._cache.get(vendor_id) is None
|
||||
self.service.invalidate_store_cache(store_id)
|
||||
assert self.service._cache.get(store_id) is None
|
||||
|
||||
def test_cache_invalidate_all(self, db, test_vendor_with_subscription):
|
||||
def test_cache_invalidate_all(self, db, test_store_with_subscription):
|
||||
"""Test invalidating entire cache."""
|
||||
vendor_id = test_vendor_with_subscription.id
|
||||
store_id = test_store_with_subscription.id
|
||||
|
||||
# Prime the cache
|
||||
self.service.get_vendor_feature_codes(db, vendor_id)
|
||||
self.service.get_store_feature_codes(db, store_id)
|
||||
|
||||
# Invalidate all
|
||||
self.service.invalidate_all_cache()
|
||||
assert self.service._cache.get(vendor_id) is None
|
||||
assert self.service._cache.get(store_id) is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -301,14 +300,14 @@ def test_subscription_tiers(db):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_vendor_with_subscription(db, test_vendor, test_subscription_tiers):
|
||||
"""Create a vendor with an active subscription."""
|
||||
def test_store_with_subscription(db, test_store, test_subscription_tiers):
|
||||
"""Create a store with an active subscription."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
essential_tier = test_subscription_tiers[0] # Use the essential tier from tiers list
|
||||
now = datetime.now(timezone.utc)
|
||||
subscription = VendorSubscription(
|
||||
vendor_id=test_vendor.id,
|
||||
subscription = StoreSubscription(
|
||||
store_id=test_store.id,
|
||||
tier="essential",
|
||||
tier_id=essential_tier.id,
|
||||
status="active",
|
||||
@@ -318,8 +317,8 @@ def test_vendor_with_subscription(db, test_vendor, test_subscription_tiers):
|
||||
)
|
||||
db.add(subscription)
|
||||
db.commit()
|
||||
db.refresh(test_vendor)
|
||||
return test_vendor
|
||||
db.refresh(test_store)
|
||||
return test_store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user