- Add admin SQL query tool with saved queries, schema explorer presets, and collapsible category sections (dev_tools module) - Add platform debug tool for admin diagnostics - Add loyalty settings page with owner-only access control - Fix loyalty settings owner check (use currentUser instead of window.__userData) - Replace HTTPException with AuthorizationException in loyalty routes - Expand loyalty module with PIN service, Apple Wallet, program management - Improve store login with platform detection and multi-platform support - Update billing feature gates and subscription services - Add store platform sync improvements and remove is_primary column - Add unit tests for loyalty (PIN, points, stamps, program services) - Update i18n translations across dev_tools locales Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.9 KiB
Store Menu: Multi-Platform Module Visibility
Date: 2026-03-08 Status: Resolved — login platform detection fixed, secondary issues fixed Affects: Store sidebar menu for merchants subscribed to multiple platforms
Problem Statement
When a merchant subscribes to multiple platforms (e.g., OMS + Loyalty), their stores should see menu items from all subscribed platforms. Currently, the store sidebar only shows menu items from the store's primary platform, hiding items from other subscribed platforms entirely.
Example: Fashion Group S.A. subscribes to both OMS and Loyalty platforms. Their store FASHIONHUB should see loyalty menu items (Terminal, Cards, Statistics) in the sidebar, but doesn't — despite the Loyalty platform having the loyalty module enabled and menu items configured.
Prior Work: Platform Detection in Store Login
This problem was partially identified in commit cfce6c0c (2026-02-24) and documented in docs/proposals/store-login-platform-detection.md.
What was done then
-
Identified the root cause: The middleware-detected platform is unreliable for API paths on localhost (e.g.,
/api/v1/store/auth/logindefaults to "main" instead of the store's actual platform). -
Applied an interim fix in
store_auth.py: Instead of using the middleware-detected platform, the login endpoint now resolves the platform fromStorePlatform.is_primary:primary_pid = menu_service.get_store_primary_platform_id(db, store.id)This was explicitly labeled an "interim fix" — it works for single-platform stores but breaks for multi-platform stores.
-
Added production routing support (commit
ce5b54f2, 2026-02-26):StoreContextMiddlewarenow checksStorePlatform.custom_subdomainfor per-platform subdomain overrides. In production,acme-rewards.rewardflow.luresolves via custom_subdomain → StorePlatform → Store, and the platform is known from the domain. -
Documented the open question: How should the store login determine the correct platform when a store belongs to multiple platforms?
What was NOT solved
- The store menu endpoint still uses a single
platform_idfrom the JWT - No multi-platform module aggregation for the store sidebar
- The
is_primaryinterim fix always picks the same platform regardless of login context
Root Cause (Full Trace)
1. Login bakes ONE platform into the JWT
app/modules/tenancy/routes/api/store_auth.py (line ~128):
primary_pid = menu_service.get_store_primary_platform_id(db, store.id)
# Returns the ONE StorePlatform row with is_primary=True (OMS)
token_data = auth_service.create_access_token(
platform_id=platform_id, # Only OMS baked into JWT
)
2. Menu endpoint reads that single platform from JWT
app/modules/core/routes/api/store_menu.py (line ~101):
platform_id = current_user.token_platform_id # OMS from JWT
menu = menu_service.get_menu_for_rendering(
platform_id=platform_id, # Only OMS passed here
# enabled_module_codes is NOT passed (defaults to None)
)
3. Module enablement checked against single platform
app/modules/core/services/menu_discovery_service.py (line ~154):
# Since enabled_module_codes=None, falls into per-platform check:
is_module_enabled = module_service.is_module_enabled(db, OMS_platform_id, "loyalty")
# Returns False — loyalty is enabled on Loyalty platform, not OMS
4. AdminMenuConfig also queried for single platform
Visibility rows in AdminMenuConfig are filtered by platform_id=OMS, so Loyalty platform's menu config rows are never consulted.
How Production Routing Affects This
In production, each platform has its own domain:
omsflow.lu→ OMS platformrewardflow.lu→ Loyalty platform
When a store manager goes to fashionhub.rewardflow.lu/login:
PlatformContextMiddlewaredetectsrewardflow.lu→ Loyalty platform ✓StoreContextMiddlewarechecksStorePlatform.custom_subdomain="fashionhub"on Loyalty platform → resolves store ✓- Login POST goes to same domain → platform context is Loyalty ✓
- JWT gets
platform_id=Loyalty✓ - Menu shows only Loyalty items ✓ — but OMS items are now hidden!
The production routing solves "wrong platform" but introduces "single platform" — the store manager sees different menus depending on which domain they logged in from, but never sees items from both platforms simultaneously.
Why the Merchant Portal Works
The merchant menu endpoint aggregates across all subscribed platforms:
# app/modules/core/routes/api/merchant_menu.py
for platform_id in all_subscribed_platform_ids:
all_enabled |= module_service.get_enabled_module_codes(db, platform_id)
menu = get_menu_for_rendering(enabled_module_codes=all_enabled)
Proposed Fix Direction
Option A: Aggregate across platforms (like merchant menu)
The store menu endpoint gathers enabled modules from ALL platforms the store is linked to:
# store_menu.py
platform_ids = platform_service.get_active_platform_ids_for_store(db, store.id)
all_enabled = set()
for pid in platform_ids:
all_enabled |= module_service.get_enabled_module_codes(db, pid)
menu = get_menu_for_rendering(enabled_module_codes=all_enabled)
Pros: Simple, mirrors merchant pattern, store manager sees everything Cons: AdminMenuConfig visibility still per-platform (needs aggregation too), no visual distinction between platform sources
Option B: Platform-grouped store menu (like merchant sidebar)
Show items grouped by platform in the store sidebar, similar to how the merchant sidebar groups items under platform headers.
Pros: Clear visual separation, respects per-platform menu config Cons: More complex, may be overkill for store context
Option C: JWT carries login platform, menu aggregates all
Keep the JWT's platform_id for audit/context purposes, but change the menu endpoint to always aggregate across all store platforms.
Pros: Login context preserved for other uses, menu shows everything Cons: JWT platform becomes informational only
Secondary Issues (Fix Regardless of Approach)
A. Loyalty route /loyalty/programs does not exist (causes 500)
The onboarding step in loyalty_onboarding.py points to /store/{store_code}/loyalty/programs but no handler exists. Available: /loyalty/terminal, /loyalty/cards, /loyalty/stats, /loyalty/enroll.
Fix: Change route_template to /store/{store_code}/loyalty/terminal.
B. Broken ORM query in loyalty onboarding
count = db.query(LoyaltyProgram).filter(...).limit(1).count()
# .limit(1).count() is invalid in SQLAlchemy
Fix: Replace with .first() is not None.
C. Menu item ID mismatch in loyalty module definition
| System | IDs |
|---|---|
Legacy menu_items |
"loyalty", "loyalty-cards", "loyalty-stats" |
New menus |
"terminal", "cards", "stats" |
Fix: Sync legacy IDs with new IDs, re-initialize AdminMenuConfig.
Key Files
| File | Role |
|---|---|
app/modules/tenancy/routes/api/store_auth.py |
Login — bakes platform_id into JWT |
app/modules/core/routes/api/store_menu.py |
Menu endpoint — reads single platform from JWT |
app/modules/core/services/menu_discovery_service.py |
Module enablement filtering |
app/modules/core/services/menu_service.py |
get_store_primary_platform_id(), get_menu_for_rendering() |
app/modules/core/routes/api/merchant_menu.py |
Working multi-platform pattern (for reference) |
app/modules/loyalty/definition.py |
Menu item ID mismatch |
app/modules/loyalty/services/loyalty_onboarding.py |
Broken route + ORM query |
middleware/store_context.py |
Production subdomain/custom_subdomain detection |
middleware/platform_context.py |
Platform detection from domain/URL |
docs/proposals/store-login-platform-detection.md |
Prior analysis of this problem |
scripts/seed/init_production.py |
Platform/module seeding (no menu config seeding) |