feat(loyalty): wallet debug page, Google Wallet fixes, and module config env_file standardization
Some checks failed
CI / ruff (push) Successful in 12s
CI / validate (push) Successful in 27s
CI / dependency-scanning (push) Successful in 32s
CI / pytest (push) Failing after 1h13m39s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled

- Add wallet diagnostics page at /admin/loyalty/wallet-debug (super admin only)
  with explorer-sidebar pattern: config validation, class status, card inspector,
  save URL tester, recent enrollments, and Apple Wallet status panels
- Fix Google Wallet fat JWT: include both loyaltyClasses and loyaltyObjects in
  payload, use UNDER_REVIEW instead of DRAFT for class reviewStatus
- Fix StorefrontProgramResponse schema: accept google_class_id values while
  keeping exclude=True (was rejecting non-None values)
- Standardize all module configs to read from .env file directly
  (env_file=".env", extra="ignore") matching core Settings pattern
- Add MOD-026 architecture rule enforcing env_file in module configs
- Add SVC-005 noqa support in architecture validator
- Add test files for dev_tools domain_health and isolation_audit services
- Add google_wallet_status.py script for querying Google Wallet API
- Use table_wrapper macro in wallet-debug.html (FE-005 compliance)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 22:18:39 +01:00
parent 11b8e31a29
commit f89c0382f0
31 changed files with 1721 additions and 64 deletions

View File

@@ -0,0 +1,70 @@
# app/modules/dev_tools/tests/unit/test_domain_health_service.py
"""Unit tests for domain_health_service."""
import pytest
from app.modules.dev_tools.services.domain_health_service import _entry
@pytest.mark.unit
@pytest.mark.dev
class TestEntry:
"""Tests for the _entry helper function."""
def test_entry_pass(self):
"""Passing entry has status 'pass'."""
class FakeStore:
store_code = "ACME"
subdomain = "acme"
result = _entry(
domain="acme.rewardflow.lu",
entry_type="custom subdomain",
platform_code="RF",
expected_store=FakeStore(),
resolved_store=FakeStore(),
passed=True,
note="",
)
assert result["status"] == "pass"
assert result["domain"] == "acme.rewardflow.lu"
assert result["type"] == "custom subdomain"
assert result["platform_code"] == "RF"
assert result["expected_store"] == "ACME"
assert result["resolved_store"] == "ACME"
def test_entry_fail(self):
"""Failing entry has status 'fail' and includes note."""
class FakeStore:
store_code = "ACME"
subdomain = "acme"
result = _entry(
domain="acme.rewardflow.lu",
entry_type="custom subdomain",
platform_code="RF",
expected_store=FakeStore(),
resolved_store=None,
passed=False,
note="custom_subdomain lookup failed",
)
assert result["status"] == "fail"
assert result["resolved_store"] is None
assert result["note"] == "custom_subdomain lookup failed"
def test_entry_none_stores(self):
"""Entry handles None expected/resolved stores."""
result = _entry(
domain="test.example.com",
entry_type="custom domain",
platform_code=None,
expected_store=None,
resolved_store=None,
passed=False,
note="not found",
)
assert result["expected_store"] is None
assert result["resolved_store"] is None
assert result["platform_code"] is None

View File

@@ -0,0 +1,193 @@
# app/modules/dev_tools/tests/unit/test_isolation_audit_service.py
"""Unit tests for isolation_audit_service."""
from types import SimpleNamespace
import pytest
from app.modules.dev_tools.services.isolation_audit_service import (
_check_contact_inheritance,
_check_language_config,
_check_locale_fallback,
_check_merchant_active,
_check_merchant_domain_primary,
_check_theme_fallback,
_finding,
)
def _make_store(**kwargs):
"""Create a minimal fake store for testing."""
defaults = {
"contact_email": None,
"contact_phone": None,
"website": None,
"business_address": None,
"tax_number": None,
"merchant": None,
"store_platforms": [],
"domains": [],
"store_theme": None,
"storefront_locale": None,
"storefront_languages": None,
}
defaults.update(kwargs)
return SimpleNamespace(**defaults)
def _make_merchant(**kwargs):
defaults = {"name": "TestMerchant", "is_active": True}
defaults.update({k: None for k in ["contact_email", "contact_phone", "website", "business_address", "tax_number"]})
defaults.update(kwargs)
return SimpleNamespace(**defaults)
@pytest.mark.unit
@pytest.mark.dev
class TestFinding:
"""Tests for the _finding helper."""
def test_finding_structure(self):
result = _finding(
check="test",
check_label="Test",
risk="high",
resolved_value="val",
source="store",
source_label="Store value",
is_explicit=True,
note="a note",
)
assert result["check"] == "test"
assert result["risk"] == "high"
assert result["is_explicit"] is True
assert result["note"] == "a note"
@pytest.mark.unit
@pytest.mark.dev
class TestCheckContactInheritance:
"""Tests for _check_contact_inheritance."""
def test_no_findings_when_store_has_values(self):
store = _make_store(
contact_email="shop@test.com",
contact_phone="+352123",
website="https://test.com",
business_address="123 Main St",
tax_number="LU12345",
merchant=_make_merchant(),
)
assert _check_contact_inheritance(store) == []
def test_critical_when_inheriting_from_merchant(self):
merchant = _make_merchant(contact_email="merchant@test.com")
store = _make_store(merchant=merchant)
findings = _check_contact_inheritance(store)
email_findings = [f for f in findings if f["check"] == "contact_email"]
assert len(email_findings) == 1
assert email_findings[0]["risk"] == "critical"
assert email_findings[0]["source"] == "merchant"
def test_critical_when_no_value_anywhere(self):
store = _make_store(merchant=_make_merchant())
findings = _check_contact_inheritance(store)
email_findings = [f for f in findings if f["check"] == "contact_email"]
assert len(email_findings) == 1
assert email_findings[0]["source"] == "none"
@pytest.mark.unit
@pytest.mark.dev
class TestCheckMerchantActive:
"""Tests for _check_merchant_active."""
def test_no_finding_when_active(self):
store = _make_store(merchant=_make_merchant(is_active=True))
assert _check_merchant_active(store) == []
def test_finding_when_inactive(self):
store = _make_store(merchant=_make_merchant(is_active=False))
findings = _check_merchant_active(store)
assert len(findings) == 1
assert findings[0]["risk"] == "high"
@pytest.mark.unit
@pytest.mark.dev
class TestCheckMerchantDomainPrimary:
"""Tests for _check_merchant_domain_primary."""
def test_no_finding_single_primary(self):
domains = [SimpleNamespace(domain="test.com", is_primary=True, is_active=True)]
assert _check_merchant_domain_primary(domains) == []
def test_finding_multiple_primaries(self):
domains = [
SimpleNamespace(domain="a.com", is_primary=True, is_active=True),
SimpleNamespace(domain="b.com", is_primary=True, is_active=True),
]
findings = _check_merchant_domain_primary(domains)
assert len(findings) == 1
assert findings[0]["risk"] == "high"
@pytest.mark.unit
@pytest.mark.dev
class TestCheckThemeFallback:
"""Tests for _check_theme_fallback."""
def test_no_finding_with_active_theme(self):
store = _make_store(store_theme=SimpleNamespace(is_active=True))
assert _check_theme_fallback(store) == []
def test_finding_no_theme(self):
store = _make_store(store_theme=None)
findings = _check_theme_fallback(store)
assert len(findings) == 1
assert findings[0]["risk"] == "medium"
@pytest.mark.unit
@pytest.mark.dev
class TestCheckLocaleFallback:
"""Tests for _check_locale_fallback."""
def test_no_finding_with_locale(self):
store = _make_store(storefront_locale="de-LU")
assert _check_locale_fallback(store, "fr-LU") == []
def test_finding_platform_default(self):
store = _make_store(storefront_locale=None)
findings = _check_locale_fallback(store, "de-LU")
assert len(findings) == 1
assert findings[0]["resolved_value"] == "de-LU"
assert findings[0]["source"] == "platform_default"
def test_finding_hardcoded_fallback(self):
store = _make_store(storefront_locale=None)
findings = _check_locale_fallback(store, None)
assert len(findings) == 1
assert findings[0]["resolved_value"] == "fr-LU"
assert findings[0]["source"] == "hardcoded"
@pytest.mark.unit
@pytest.mark.dev
class TestCheckLanguageConfig:
"""Tests for _check_language_config."""
def test_no_finding_custom_languages(self):
store = _make_store(storefront_languages=["fr", "en"])
assert _check_language_config(store) == []
def test_finding_default_languages(self):
store = _make_store(storefront_languages=["fr", "de", "en", "lb"])
findings = _check_language_config(store)
assert len(findings) == 1
assert findings[0]["risk"] == "medium"
def test_finding_none_languages(self):
store = _make_store(storefront_languages=None)
findings = _check_language_config(store)
assert len(findings) == 1