feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
All checks were successful
- Fix platform-grouped merchant sidebar menu with core items at root level - Add merchant store management (detail page, create store, team page) - Fix store settings 500 error by removing dead stripe/API tab - Move onboarding translations to module-owned locale files - Fix onboarding banner i18n with server-side rendering + context inheritance - Refactor login language selectors to use languageSelector() function (LANG-002) - Move HTTPException handling to global exception handler in merchant routes (API-003) - Add language selector to all login pages and portal headers - Fix customer module: drop order stats from customer model, add to orders module - Fix admin menu config visibility for super admin platform context - Fix storefront auth and layout issues - Add missing i18n translations for onboarding steps (en/fr/de/lb) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -263,34 +263,49 @@ class TestMerchantMenuModuleGating:
|
||||
|
||||
def test_loyalty_appears_when_module_enabled(
|
||||
self, client, db, menu_auth, menu_merchant, menu_subscription,
|
||||
menu_loyalty_module,
|
||||
menu_loyalty_module, menu_platform,
|
||||
):
|
||||
"""Loyalty section appears when loyalty module is enabled on subscribed platform."""
|
||||
"""Loyalty items appear under platform section when module is enabled."""
|
||||
response = client.get(BASE, headers=menu_auth)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# Loyalty now appears under a platform-{code} section
|
||||
platform_section_id = f"platform-{menu_platform.code}"
|
||||
section_ids = {s["id"] for s in data["sections"]}
|
||||
assert "loyalty" in section_ids
|
||||
assert platform_section_id in section_ids
|
||||
# Check loyalty item exists in that platform section
|
||||
platform_section = next(
|
||||
s for s in data["sections"] if s["id"] == platform_section_id
|
||||
)
|
||||
item_ids = {i["id"] for i in platform_section["items"]}
|
||||
assert "loyalty-overview" in item_ids
|
||||
|
||||
def test_loyalty_hidden_when_module_not_enabled(
|
||||
self, client, db, menu_auth, menu_merchant, menu_subscription,
|
||||
menu_platform,
|
||||
):
|
||||
"""Loyalty section is hidden when loyalty module is NOT enabled."""
|
||||
"""No platform section when no non-core modules are enabled."""
|
||||
response = client.get(BASE, headers=menu_auth)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
platform_section_id = f"platform-{menu_platform.code}"
|
||||
section_ids = {s["id"] for s in data["sections"]}
|
||||
assert "loyalty" not in section_ids
|
||||
assert platform_section_id not in section_ids
|
||||
|
||||
def test_loyalty_item_has_correct_route(
|
||||
self, client, db, menu_auth, menu_merchant, menu_subscription,
|
||||
menu_loyalty_module,
|
||||
menu_loyalty_module, menu_platform,
|
||||
):
|
||||
"""Loyalty overview item has the correct URL."""
|
||||
response = client.get(BASE, headers=menu_auth)
|
||||
data = response.json()
|
||||
loyalty = next(s for s in data["sections"] if s["id"] == "loyalty")
|
||||
overview = next(i for i in loyalty["items"] if i["id"] == "loyalty-overview")
|
||||
platform_section_id = f"platform-{menu_platform.code}"
|
||||
platform_section = next(
|
||||
s for s in data["sections"] if s["id"] == platform_section_id
|
||||
)
|
||||
overview = next(
|
||||
i for i in platform_section["items"] if i["id"] == "loyalty-overview"
|
||||
)
|
||||
assert overview["url"] == "/merchants/loyalty/overview"
|
||||
|
||||
|
||||
@@ -352,7 +367,8 @@ class TestMerchantMenuSubscriptionStatus:
|
||||
response = client.get(BASE, headers=menu_auth)
|
||||
assert response.status_code == 200
|
||||
section_ids = {s["id"] for s in response.json()["sections"]}
|
||||
assert "loyalty" in section_ids
|
||||
platform_section_id = f"platform-{menu_platform.code}"
|
||||
assert platform_section_id in section_ids
|
||||
|
||||
def test_expired_subscription_hides_non_core_modules(
|
||||
self, client, db, menu_auth, menu_merchant, menu_platform, menu_tier,
|
||||
@@ -374,8 +390,9 @@ class TestMerchantMenuSubscriptionStatus:
|
||||
response = client.get(BASE, headers=menu_auth)
|
||||
assert response.status_code == 200
|
||||
section_ids = {s["id"] for s in response.json()["sections"]}
|
||||
# Loyalty should NOT appear because subscription is expired
|
||||
assert "loyalty" not in section_ids
|
||||
platform_section_id = f"platform-{menu_platform.code}"
|
||||
# Platform section should NOT appear because subscription is expired
|
||||
assert platform_section_id not in section_ids
|
||||
# Core sections always appear
|
||||
assert "main" in section_ids
|
||||
assert "billing" in section_ids
|
||||
@@ -468,9 +485,20 @@ class TestMerchantMenuMultiPlatform:
|
||||
|
||||
response = client.get(BASE, headers=menu_auth)
|
||||
assert response.status_code == 200
|
||||
section_ids = {s["id"] for s in response.json()["sections"]}
|
||||
# Loyalty enabled on Platform A should appear in the union
|
||||
assert "loyalty" in section_ids
|
||||
data = response.json()
|
||||
section_ids = {s["id"] for s in data["sections"]}
|
||||
# Loyalty enabled on Platform A appears under platform-a's section
|
||||
platform_a_section_id = f"platform-{platform_a.code}"
|
||||
assert platform_a_section_id in section_ids
|
||||
# Platform B has no non-core modules, so no section
|
||||
platform_b_section_id = f"platform-{platform_b.code}"
|
||||
assert platform_b_section_id not in section_ids
|
||||
# Check loyalty item exists in Platform A section
|
||||
pa_section = next(
|
||||
s for s in data["sections"] if s["id"] == platform_a_section_id
|
||||
)
|
||||
item_ids = {i["id"] for i in pa_section["items"]}
|
||||
assert "loyalty-overview" in item_ids
|
||||
# Core sections always present
|
||||
assert "main" in section_ids
|
||||
assert "billing" in section_ids
|
||||
|
||||
@@ -110,3 +110,62 @@ class TestMenuServiceMerchantRendering:
|
||||
without_ids = {s.id for s in without_loyalty}
|
||||
assert "loyalty" in with_ids
|
||||
assert "loyalty" not in without_ids
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.core
|
||||
class TestMenuServiceMerchantByPlatform:
|
||||
"""Test get_merchant_menu_by_platform grouping logic."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MenuService()
|
||||
|
||||
def test_core_sections_contain_only_core_items(self, db):
|
||||
"""Core sections should only have items from core modules."""
|
||||
from app.modules.core.services.menu_discovery_service import (
|
||||
menu_discovery_service,
|
||||
)
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
core_codes = {code for code, mod in MODULES.items() if mod.is_core}
|
||||
|
||||
core_sections = menu_discovery_service.get_menu_sections_for_frontend(
|
||||
db,
|
||||
FrontendType.MERCHANT,
|
||||
enabled_module_codes=core_codes,
|
||||
)
|
||||
for section in core_sections:
|
||||
section.items = [i for i in section.items if i.module_code in core_codes]
|
||||
|
||||
for section in core_sections:
|
||||
for item in section.items:
|
||||
assert item.module_code in core_codes, (
|
||||
f"Non-core item '{item.id}' (module={item.module_code}) "
|
||||
f"found in core section '{section.id}'"
|
||||
)
|
||||
|
||||
def test_non_core_sections_for_loyalty(self, db):
|
||||
"""Non-core modules like loyalty produce merchant menu sections."""
|
||||
from app.modules.core.services.menu_discovery_service import (
|
||||
menu_discovery_service,
|
||||
)
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
core_codes = {code for code, mod in MODULES.items() if mod.is_core}
|
||||
non_core = {"loyalty"}
|
||||
|
||||
sections = menu_discovery_service.get_menu_sections_for_frontend(
|
||||
db,
|
||||
FrontendType.MERCHANT,
|
||||
enabled_module_codes=non_core,
|
||||
)
|
||||
sections = [
|
||||
s
|
||||
for s in sections
|
||||
if any(i.module_code in non_core for i in s.items)
|
||||
]
|
||||
|
||||
assert len(sections) > 0, "Loyalty module should produce merchant menu sections"
|
||||
for section in sections:
|
||||
for item in section.items:
|
||||
assert item.module_code not in core_codes
|
||||
|
||||
Reference in New Issue
Block a user