Some checks failed
- 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>
174 lines
7.9 KiB
Markdown
174 lines
7.9 KiB
Markdown
# 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`](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`:
|
|
```python
|
|
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):
|
|
```python
|
|
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):
|
|
```python
|
|
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):
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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) |
|