fix(loyalty): show category column on card-detail for all 3 personas

The transaction-history table on the card-detail page rendered a
Category column only on the store frontend. Merchant and admin saw
five columns instead of six, even though the merchant report
prompted the audit (rewardflow.lu/merchants/loyalty/cards/6 vs
fashionhub.rewardflow.lu/store/.../cards/6).

Root cause was two layers:
- API: only store's GET /cards/{id}/transactions enriched
  tx.category_names from tx.category_ids; merchant's and admin's
  endpoints returned raw rows with category_names=null.
- Template: the shared partial's show_category_column flag was set
  to true only on the store wrapper.

Backfill the same `category_service.validate_category_for_store`
lookup loop into merchant.py::get_card_transactions and
admin.py::get_merchant_card_transactions, accepting Request to read
request.state.language for localised category names. Add
`{% set show_category_column = true %}` to the merchant and admin
card-detail wrappers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 23:02:07 +02:00
parent 58a9e3f740
commit d32c1fd545
4 changed files with 46 additions and 10 deletions

View File

@@ -10,7 +10,7 @@ Platform admin endpoints for:
import logging import logging
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -412,6 +412,7 @@ def get_merchant_card(
response_model=TransactionListResponse, response_model=TransactionListResponse,
) )
def get_merchant_card_transactions( def get_merchant_card_transactions(
request: Request,
merchant_id: int = Path(..., gt=0), merchant_id: int = Path(..., gt=0),
card_id: int = Path(..., gt=0), card_id: int = Path(..., gt=0),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
@@ -430,10 +431,26 @@ def get_merchant_card_transactions(
db, card_id, skip=skip, limit=limit db, card_id, skip=skip, limit=limit
) )
return TransactionListResponse( # Enrich category_names from category_ids (same as store + merchant routes
transactions=[TransactionResponse.model_validate(t) for t in transactions], # — keeps the transaction history aligned across personas).
total=total, from app.modules.loyalty.services.category_service import category_service
)
lang = getattr(request.state, "language", "en") or "en"
tx_responses = []
for t in transactions:
tx = TransactionResponse.model_validate(t)
if t.category_ids and isinstance(t.category_ids, list):
names = []
for cid in t.category_ids:
name = category_service.validate_category_for_store(
db, cid, t.store_id or 0, lang=lang
)
if name:
names.append(name)
tx.category_names = names if names else None
tx_responses.append(tx)
return TransactionListResponse(transactions=tx_responses, total=total)
@router.get("/merchants/{merchant_id}/transactions", response_model=TransactionListResponse) @router.get("/merchants/{merchant_id}/transactions", response_model=TransactionListResponse)

View File

@@ -23,7 +23,7 @@ registration under /api/v1/merchants/loyalty/*).
import logging import logging
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_merchant_api, get_merchant_for_current_user from app.api.deps import get_current_merchant_api, get_merchant_for_current_user
@@ -256,6 +256,7 @@ def get_card_detail(
@router.get("/cards/{card_id}/transactions", response_model=TransactionListResponse) @router.get("/cards/{card_id}/transactions", response_model=TransactionListResponse)
def get_card_transactions( def get_card_transactions(
request: Request,
card_id: int = Path(..., gt=0), card_id: int = Path(..., gt=0),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
@@ -271,10 +272,26 @@ def get_card_transactions(
db, card_id, skip=skip, limit=limit db, card_id, skip=skip, limit=limit
) )
return TransactionListResponse( # Enrich category_names from category_ids (same as store route — keeps the
transactions=[TransactionResponse.model_validate(t) for t in transactions], # transaction history aligned across personas).
total=total, from app.modules.loyalty.services.category_service import category_service
)
lang = getattr(request.state, "language", "en") or "en"
tx_responses = []
for t in transactions:
tx = TransactionResponse.model_validate(t)
if t.category_ids and isinstance(t.category_ids, list):
names = []
for cid in t.category_ids:
name = category_service.validate_category_for_store(
db, cid, t.store_id or 0, lang=lang
)
if name:
names.append(name)
tx.category_names = names if names else None
tx_responses.append(tx)
return TransactionListResponse(transactions=tx_responses, total=total)
# ============================================================================= # =============================================================================

View File

@@ -17,6 +17,7 @@
{% set card_detail_api_prefix = '/admin/loyalty/merchants/' + merchant_id|string %} {% set card_detail_api_prefix = '/admin/loyalty/merchants/' + merchant_id|string %}
{% set card_detail_back_url = '/admin/loyalty/merchants/' + merchant_id|string + '/cards' %} {% set card_detail_back_url = '/admin/loyalty/merchants/' + merchant_id|string + '/cards' %}
{% set show_category_column = true %}
{% include 'loyalty/shared/card-detail-view.html' %} {% include 'loyalty/shared/card-detail-view.html' %}
{% endblock %} {% endblock %}

View File

@@ -20,6 +20,7 @@
{% set card_detail_api_prefix = '/merchants/loyalty' %} {% set card_detail_api_prefix = '/merchants/loyalty' %}
{% set card_detail_back_url = '/merchants/loyalty/cards' %} {% set card_detail_back_url = '/merchants/loyalty/cards' %}
{% set show_category_column = true %}
{% include 'loyalty/shared/card-detail-view.html' %} {% include 'loyalty/shared/card-detail-view.html' %}
{% endblock %} {% endblock %}