feat: loyalty feature provider, admin data fixes, storefront mobile menu
Some checks failed
CI / ruff (push) Successful in 9s
CI / pytest (push) Failing after 37m24s
CI / validate (push) Failing after 22s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- Add LoyaltyFeatureProvider with 11 BINARY/MERCHANT features for billing
  feature gating, wired into loyalty module definition
- Fix subscription-tiers admin page showing 0 features by populating
  feature_codes from tier relationship in all admin tier endpoints
- Fix merchants admin page showing 0 stores and N/A owner by adding
  store_count and owner_email to MerchantResponse and eager-loading owner
- Add "no tiers" warning with link in subscription creation modal when
  platform has no configured tiers
- Add missing mobile menu panel to storefront base template so hamburger
  toggle actually shows navigation links

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 18:59:24 +01:00
parent 2c710ad416
commit a8b29750a5
10 changed files with 350 additions and 13 deletions

View File

View File

@@ -0,0 +1,92 @@
# tests/unit/modules/loyalty/test_loyalty_features.py
"""
Unit tests for the loyalty feature provider.
Tests cover:
- Feature category
- Feature declarations (count, codes, types, scopes)
- Usage methods return empty lists
- Protocol compliance
"""
import pytest
from app.modules.contracts.features import (
FeatureProviderProtocol,
FeatureScope,
FeatureType,
)
from app.modules.loyalty.definition import loyalty_module
from app.modules.loyalty.services.loyalty_features import (
LoyaltyFeatureProvider,
loyalty_feature_provider,
)
@pytest.mark.unit
class TestLoyaltyFeatureProvider:
"""Tests for LoyaltyFeatureProvider."""
def test_feature_category(self):
"""Feature category should be 'loyalty'."""
assert loyalty_feature_provider.feature_category == "loyalty"
def test_get_feature_declarations_count(self):
"""Should return exactly 11 feature declarations."""
declarations = loyalty_feature_provider.get_feature_declarations()
assert len(declarations) == 11
def test_feature_codes_match_module_definition(self):
"""All declared feature codes should match loyalty_module.features."""
declarations = loyalty_feature_provider.get_feature_declarations()
declared_codes = {d.code for d in declarations}
module_features = set(loyalty_module.features)
assert declared_codes == module_features
def test_all_features_are_binary(self):
"""All loyalty features should be BINARY type."""
declarations = loyalty_feature_provider.get_feature_declarations()
for decl in declarations:
assert decl.feature_type == FeatureType.BINARY, (
f"Feature {decl.code} should be BINARY, got {decl.feature_type}"
)
def test_all_features_are_merchant_scoped(self):
"""All loyalty features should be MERCHANT scoped."""
declarations = loyalty_feature_provider.get_feature_declarations()
for decl in declarations:
assert decl.scope == FeatureScope.MERCHANT, (
f"Feature {decl.code} should be MERCHANT scoped, got {decl.scope}"
)
def test_all_features_have_name_key(self):
"""All features should have a non-empty name_key."""
declarations = loyalty_feature_provider.get_feature_declarations()
for decl in declarations:
assert decl.name_key, f"Feature {decl.code} missing name_key"
def test_all_features_have_description_key(self):
"""All features should have a non-empty description_key."""
declarations = loyalty_feature_provider.get_feature_declarations()
for decl in declarations:
assert decl.description_key, f"Feature {decl.code} missing description_key"
def test_get_store_usage_returns_empty(self):
"""get_store_usage should return an empty list."""
result = loyalty_feature_provider.get_store_usage(db=None, store_id=1)
assert result == []
def test_get_merchant_usage_returns_empty(self):
"""get_merchant_usage should return an empty list."""
result = loyalty_feature_provider.get_merchant_usage(
db=None, merchant_id=1, platform_id=1
)
assert result == []
def test_satisfies_feature_provider_protocol(self):
"""Provider should satisfy FeatureProviderProtocol."""
assert isinstance(loyalty_feature_provider, FeatureProviderProtocol)
def test_singleton_is_correct_type(self):
"""Singleton instance should be a LoyaltyFeatureProvider."""
assert isinstance(loyalty_feature_provider, LoyaltyFeatureProvider)