Files
orion/docs/proposals/store-menu-multi-platform-visibility.md
Samir Boulahtit 319900623a
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Failing after 50m12s
CI / validate (push) Successful in 25s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat: add SQL query tool, platform debug, loyalty settings, and multi-module improvements
- 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>
2026-03-10 20:08:07 +01:00

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

  1. Identified the root cause: The middleware-detected platform is unreliable for API paths on localhost (e.g., /api/v1/store/auth/login defaults to "main" instead of the store's actual platform).

  2. Applied an interim fix in store_auth.py: Instead of using the middleware-detected platform, the login endpoint now resolves the platform from StorePlatform.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.

  3. Added production routing support (commit ce5b54f2, 2026-02-26): StoreContextMiddleware now checks StorePlatform.custom_subdomain for per-platform subdomain overrides. In production, acme-rewards.rewardflow.lu resolves via custom_subdomain → StorePlatform → Store, and the platform is known from the domain.

  4. 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_id from the JWT
  • No multi-platform module aggregation for the store sidebar
  • The is_primary interim 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 platform
  • rewardflow.lu → Loyalty platform

When a store manager goes to fashionhub.rewardflow.lu/login:

  1. PlatformContextMiddleware detects rewardflow.lu → Loyalty platform ✓
  2. StoreContextMiddleware checks StorePlatform.custom_subdomain="fashionhub" on Loyalty platform → resolves store ✓
  3. Login POST goes to same domain → platform context is Loyalty ✓
  4. JWT gets platform_id=Loyalty
  5. 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)