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

@@ -0,0 +1,100 @@
# app/modules/loyalty/routes/pages/merchant.py
"""
Loyalty Merchant Page Routes (HTML rendering).
Merchant portal pages for:
- Loyalty overview (aggregate stats across all stores)
Authentication: merchant_token cookie or Authorization header.
Auto-discovered by the route system (merchant.py in routes/pages/ triggers
registration under /merchants/loyalty/*).
"""
import logging
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import (
get_current_merchant_from_cookie_or_header,
get_merchant_for_current_user_page,
)
from app.core.database import get_db
from app.modules.core.utils.page_context import get_context_for_frontend
from app.modules.enums import FrontendType
from app.modules.loyalty.services import program_service
from app.modules.tenancy.models import Merchant
from app.templates_config import templates
from models.schema.auth import UserContext
logger = logging.getLogger(__name__)
ROUTE_CONFIG = {
"prefix": "/loyalty",
}
router = APIRouter()
# ============================================================================
# Helper
# ============================================================================
def _get_merchant_context(
request: Request,
db: Session,
current_user: UserContext,
**extra_context,
) -> dict:
"""Build template context for merchant loyalty pages."""
return get_context_for_frontend(
FrontendType.MERCHANT,
request,
db,
user=current_user,
**extra_context,
)
# ============================================================================
# LOYALTY OVERVIEW
# ============================================================================
@router.get("/overview", response_class=HTMLResponse, include_in_schema=False)
async def merchant_loyalty_overview(
request: Request,
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
merchant: Merchant = Depends(get_merchant_for_current_user_page),
db: Session = Depends(get_db),
):
"""
Render merchant loyalty overview page.
Shows aggregate loyalty program stats across all merchant stores.
"""
# Get merchant stats server-side
merchant_id = merchant.id
stats = {}
try:
stats = program_service.get_merchant_stats(db, merchant_id)
except Exception:
logger.warning(
f"Failed to load loyalty stats for merchant {merchant_id}",
exc_info=True,
)
context = _get_merchant_context(
request,
db,
current_user,
loyalty_stats=stats,
merchant_id=merchant_id,
)
return templates.TemplateResponse(
"loyalty/merchant/overview.html",
context,
)

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,
)