fix: loyalty module end-to-end — merchant route, store menus, sidebar, API error handling
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

- Add merchant loyalty overview route and template (was 404)
- Fix store loyalty route paths to match menu URLs (/{store_code}/loyalty/...)
- Add loyalty rewards card to storefront account dashboard
- Fix merchant overview to resolve merchant via get_merchant_for_current_user_page
- Fix store login to use store's primary platform for JWT token (interim fix)
- Fix apiClient to attach status/errorCode to thrown errors (fixes error.status
  checks in 12+ JS files — loyalty settings, terminal, email templates, etc.)
- Hide "Add Product" sidebar button when catalog module is not enabled
- Add proposal doc for proper platform detection in store login flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 13:52:11 +01:00
parent 2833ff1476
commit cfce6c0ca4
8 changed files with 364 additions and 14 deletions

View File

@@ -7,12 +7,15 @@ Store pages for:
- Loyalty members management
- Program settings
- Stats dashboard
Routes follow the standard store convention: /{store_code}/loyalty/...
so they match the menu URLs in definition.py.
"""
import logging
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_store_from_cookie_or_header, get_db
@@ -27,8 +30,9 @@ logger = logging.getLogger(__name__)
router = APIRouter()
# Route configuration for module route discovery
# No custom prefix — routes include /loyalty/ in their paths to follow
# the standard store convention: /store/{store_code}/loyalty/...
ROUTE_CONFIG = {
"prefix": "/loyalty",
"tags": ["store-loyalty"],
}
@@ -76,13 +80,37 @@ def get_store_context(
return context
# ============================================================================
# LOYALTY ROOT (Redirect to Terminal)
# ============================================================================
@router.get(
"/{store_code}/loyalty",
response_class=RedirectResponse,
include_in_schema=False,
)
async def store_loyalty_root(
store_code: str = Path(..., description="Store code"),
current_user: User = Depends(get_current_store_from_cookie_or_header),
):
"""
Redirect loyalty root to the terminal (primary daily interface).
Menu item "Dashboard" points here.
"""
return RedirectResponse(
url=f"/store/{store_code}/loyalty/terminal",
status_code=302,
)
# ============================================================================
# LOYALTY TERMINAL (Primary Daily Interface)
# ============================================================================
@router.get(
"/{store_code}/terminal",
"/{store_code}/loyalty/terminal",
response_class=HTMLResponse,
include_in_schema=False,
)
@@ -108,7 +136,7 @@ async def store_loyalty_terminal(
@router.get(
"/{store_code}/cards",
"/{store_code}/loyalty/cards",
response_class=HTMLResponse,
include_in_schema=False,
)
@@ -129,7 +157,7 @@ async def store_loyalty_cards(
@router.get(
"/{store_code}/cards/{card_id}",
"/{store_code}/loyalty/cards/{card_id}",
response_class=HTMLResponse,
include_in_schema=False,
)
@@ -156,7 +184,7 @@ async def store_loyalty_card_detail(
@router.get(
"/{store_code}/settings",
"/{store_code}/loyalty/settings",
response_class=HTMLResponse,
include_in_schema=False,
)
@@ -182,7 +210,7 @@ async def store_loyalty_settings(
@router.get(
"/{store_code}/stats",
"/{store_code}/loyalty/stats",
response_class=HTMLResponse,
include_in_schema=False,
)
@@ -208,7 +236,7 @@ async def store_loyalty_stats(
@router.get(
"/{store_code}/enroll",
"/{store_code}/loyalty/enroll",
response_class=HTMLResponse,
include_in_schema=False,
)