fix: loyalty module end-to-end — merchant route, store menus, sidebar, API error handling
Some checks failed
Some checks failed
- 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:
100
app/modules/loyalty/routes/pages/merchant.py
Normal file
100
app/modules/loyalty/routes/pages/merchant.py
Normal 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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user