feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
CI / ruff (push) Successful in 12s
CI / pytest (push) Successful in 50m57s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 40s
CI / deploy (push) Successful in 51s

- 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:
2026-03-08 23:48:25 +01:00
parent f141cc4e6a
commit a77a8a3a98
113 changed files with 3741 additions and 2923 deletions

View File

@@ -6,8 +6,6 @@ Provides menu visibility configuration for admin and store frontends:
- GET /menu-config/platforms/{platform_id} - Get menu config for a platform
- PUT /menu-config/platforms/{platform_id} - Update menu visibility for a platform
- POST /menu-config/platforms/{platform_id}/reset - Reset to defaults
- GET /menu-config/user - Get current user's menu config (super admins)
- PUT /menu-config/user - Update current user's menu config (super admins)
- GET /menu/admin - Get rendered admin menu for current user
- GET /menu/store - Get rendered store menu for current platform
@@ -316,108 +314,6 @@ async def reset_platform_menu_config(
return {"success": True, "message": "Menu configuration reset to defaults"}
# =============================================================================
# User Menu Configuration (Super Admin Only)
# =============================================================================
@router.get("/user", response_model=MenuConfigResponse)
async def get_user_menu_config(
request: Request,
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Get the current super admin's personal menu configuration.
Only super admins can configure their own admin menu.
"""
items = menu_service.get_user_menu_config(db, current_user.id)
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} fetched their personal menu config"
)
# Use user's preferred language, falling back to middleware-resolved language
language = current_user.preferred_language or getattr(
request.state, "language", "en"
)
return _build_menu_config_response(
items, FrontendType.ADMIN, language=language, user_id=current_user.id
)
@router.put("/user")
async def update_user_menu_visibility(
update_data: MenuVisibilityUpdateRequest,
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Update visibility for a single menu item for the current super admin.
Super admin only. Cannot hide mandatory items.
"""
menu_service.update_menu_visibility(
db=db,
frontend_type=FrontendType.ADMIN,
menu_item_id=update_data.menu_item_id,
is_visible=update_data.is_visible,
user_id=current_user.id,
)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} updated personal menu: "
f"{update_data.menu_item_id}={update_data.is_visible}"
)
return {"success": True, "message": "Menu visibility updated"}
@router.post("/user/reset", response_model=MenuActionResponse)
async def reset_user_menu_config(
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Reset the current super admin's menu configuration (hide all except mandatory).
Super admin only.
"""
menu_service.reset_user_menu_config(db, current_user.id)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} reset their personal menu config (hide all)"
)
return MenuActionResponse(
success=True, message="Menu configuration reset - all items hidden"
)
@router.post("/user/show-all", response_model=MenuActionResponse)
async def show_all_user_menu_config(
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Show all menu items for the current super admin.
Super admin only.
"""
menu_service.show_all_user_menu_config(db, current_user.id)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items"
)
return MenuActionResponse(success=True, message="All menu items are now visible")
@router.post("/platforms/{platform_id}/show-all")
async def show_all_platform_menu_config(
platform_id: int = Path(..., description="Platform ID"),
@@ -467,11 +363,12 @@ async def get_rendered_admin_menu(
Used by the frontend to render the sidebar.
"""
if current_user.is_super_admin:
# Super admin: use user-level config
# Super admin: use platform config if platform selected, else global (all modules)
platform_id = current_user.token_platform_id
menu = menu_service.get_menu_for_rendering(
db=db,
frontend_type=FrontendType.ADMIN,
user_id=current_user.id,
platform_id=platform_id,
is_super_admin=True,
)
else:

View File

@@ -5,9 +5,11 @@ Merchant menu rendering endpoint.
Provides the dynamic sidebar menu for the merchant portal:
- GET /menu/render/merchant - Get rendered merchant menu for current user
Menu sections are driven by module definitions (FrontendType.MERCHANT).
Only modules enabled on platforms the merchant is actively subscribed to
will appear in the sidebar.
Menu sections are grouped by platform:
- Core items (dashboard, billing, account) appear at root level
- Platform-specific items are grouped under their platform name
- No AdminMenuConfig visibility filtering — menu is purely driven by
module definitions + module enablement + subscription status
"""
import logging
@@ -38,6 +40,9 @@ class MenuSectionResponse(BaseModel):
id: str
label: str | None = None
icon: str | None = None
platform_code: str | None = None
is_collapsible: bool = False
items: list[dict[str, Any]]
@@ -83,10 +88,12 @@ async def get_rendered_merchant_menu(
"""
Get the rendered merchant menu for the current user.
Returns the filtered menu structure based on modules enabled
on platforms the merchant is subscribed to.
Returns a platform-grouped menu structure:
- Core sections (dashboard, billing, account) at root level
- Platform-specific sections grouped under platform name
Used by the merchant frontend to render the sidebar dynamically.
Menu visibility is driven by module definitions + module enablement
+ subscription status. No AdminMenuConfig filtering.
"""
# Resolve the merchant for this user (via service layer)
merchant = menu_service.get_merchant_for_menu(db, current_user.id)
@@ -98,32 +105,25 @@ async def get_rendered_merchant_menu(
sections=[],
)
# Get union of enabled module codes across all subscribed platforms
enabled_codes = menu_service.get_merchant_enabled_module_codes(db, merchant.id)
# Resolve primary platform for AdminMenuConfig visibility lookup
primary_platform_id = menu_service.get_merchant_primary_platform_id(
# Get platform-grouped menu
core_sections, platform_sections = menu_service.get_merchant_menu_by_platform(
db, merchant.id
)
# Get filtered menu using enabled_module_codes override + platform visibility
menu = menu_service.get_menu_for_rendering(
db=db,
frontend_type=FrontendType.MERCHANT,
platform_id=primary_platform_id,
enabled_module_codes=enabled_codes,
)
# Resolve language
language = current_user.preferred_language or getattr(
request.state, "language", "en"
)
# Translate section and item labels
sections = []
for section in menu:
# Build response sections
all_sections = []
# Core sections first (translated labels)
for section in core_sections:
translated_items = []
for item in section.items:
if not item.is_module_enabled:
continue
translated_items.append(
{
"id": item.id,
@@ -133,15 +133,59 @@ async def get_rendered_merchant_menu(
}
)
sections.append(
MenuSectionResponse(
id=section.id,
label=_translate_label(section.label_key, language),
items=translated_items,
if translated_items:
all_sections.append(
MenuSectionResponse(
id=section.id,
label=_translate_label(section.label_key, language),
icon=section.icon,
is_collapsible=section.is_collapsible,
items=translated_items,
)
)
)
# Platform sections (platform name as label, collapsible)
for section in platform_sections:
translated_items = []
for item in section.items:
if not item.is_module_enabled:
continue
translated_items.append(
{
"id": item.id,
"label": _translate_label(item.label_key, language),
"icon": item.icon,
"url": item.route,
}
)
if translated_items:
# Extract platform_code from section id (format: "platform-{code}")
platform_code = section.id.removeprefix("platform-")
all_sections.append(
MenuSectionResponse(
id=section.id,
label=section.label_key, # Already platform name, not a translation key
icon=section.icon,
platform_code=platform_code,
is_collapsible=True,
items=translated_items,
)
)
# Sort order: 1) Dashboard (main), 2) Platform sections, 3) Billing, 4) Account
# Core sections use their definition order; platform sections slot in between
# main=0, platform=25, billing=50, account=900
def _section_sort_key(s):
if s.platform_code:
return 25, s.id
# Map known core section IDs to their display order
core_order = {"main": 0, "billing": 50, "account": 900}
return core_order.get(s.id, 500), s.id
all_sections.sort(key=_section_sort_key)
return RenderedMenuResponse(
frontend_type=FrontendType.MERCHANT.value,
sections=sections,
sections=all_sections,
)

View File

@@ -224,14 +224,6 @@ def get_store_settings(
"is_verified": domain.is_verified,
})
# Get Stripe info from subscription (read-only, masked)
stripe_info = None
if store.subscription and store.subscription.stripe_customer_id:
stripe_info = {
"has_stripe_customer": True,
"customer_id_masked": f"cus_***{store.subscription.stripe_customer_id[-4:]}",
}
return {
# General info
"store_code": store.store_code,
@@ -297,9 +289,6 @@ def get_store_settings(
"domains": domains,
"default_subdomain": f"{store.subdomain}.letzshop.lu",
# Stripe info (read-only)
"stripe_info": stripe_info,
# Options for dropdowns
"options": {
"supported_languages": SUPPORTED_LANGUAGES,