fix: card lookup 422 caused by route ordering conflict
Some checks failed
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
CI / ruff (push) Successful in 11s

Move /cards/lookup (GET and POST) before /cards/{card_id} so FastAPI
matches the literal path before the parameterized one. Previously,
"lookup" was parsed as card_id (int), causing a 422 validation error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 22:10:10 +01:00
parent 12c1c3c511
commit 1194731f33

View File

@@ -315,79 +315,6 @@ def list_cards(
return CardListResponse(cards=card_responses, total=total)
@store_router.get("/cards/{card_id}", response_model=CardDetailResponse)
def get_card_detail(
card_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""Get detailed loyalty card info by ID."""
store_id = current_user.token_store_id
merchant_id = get_store_merchant_id(db, store_id)
card = card_service.get_card(db, card_id)
if not card or card.merchant_id != merchant_id:
from app.modules.loyalty.exceptions import LoyaltyCardNotFoundException
raise LoyaltyCardNotFoundException(str(card_id))
program = card.program
customer = card.customer
return CardDetailResponse(
id=card.id,
card_number=card.card_number,
customer_id=card.customer_id,
merchant_id=card.merchant_id,
program_id=card.program_id,
enrolled_at_store_id=card.enrolled_at_store_id,
customer_name=customer.full_name if customer else None,
customer_email=customer.email if customer else None,
merchant_name=card.merchant.name if card.merchant else None,
qr_code_data=card.qr_code_data or card.card_number,
program_name=program.display_name,
program_type=program.loyalty_type,
reward_description=program.stamps_reward_description,
stamp_count=card.stamp_count,
stamps_target=program.stamps_target,
stamps_until_reward=max(0, program.stamps_target - card.stamp_count),
total_stamps_earned=card.total_stamps_earned,
stamps_redeemed=card.stamps_redeemed,
points_balance=card.points_balance,
total_points_earned=card.total_points_earned,
points_redeemed=card.points_redeemed,
is_active=card.is_active,
created_at=card.created_at,
last_stamp_at=card.last_stamp_at,
last_points_at=card.last_points_at,
last_redemption_at=card.last_redemption_at,
last_activity_at=card.last_activity_at,
has_google_wallet=bool(card.google_object_id),
has_apple_wallet=bool(card.apple_serial_number),
)
@store_router.get("/transactions", response_model=TransactionListResponse)
def list_store_transactions(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""List recent transactions for this merchant's loyalty program."""
store_id = current_user.token_store_id
merchant_id = get_store_merchant_id(db, store_id)
transactions, total = card_service.get_store_transactions(
db, merchant_id, skip=skip, limit=limit
)
return TransactionListResponse(
transactions=[TransactionResponse.model_validate(t) for t in transactions],
total=total,
)
def _build_card_lookup_response(card, db=None) -> CardLookupResponse:
"""Build a CardLookupResponse from a card object."""
from datetime import timedelta
@@ -484,6 +411,79 @@ def lookup_card(
return _build_card_lookup_response(card, db)
@store_router.get("/cards/{card_id}", response_model=CardDetailResponse)
def get_card_detail(
card_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""Get detailed loyalty card info by ID."""
store_id = current_user.token_store_id
merchant_id = get_store_merchant_id(db, store_id)
card = card_service.get_card(db, card_id)
if not card or card.merchant_id != merchant_id:
from app.modules.loyalty.exceptions import LoyaltyCardNotFoundException
raise LoyaltyCardNotFoundException(str(card_id))
program = card.program
customer = card.customer
return CardDetailResponse(
id=card.id,
card_number=card.card_number,
customer_id=card.customer_id,
merchant_id=card.merchant_id,
program_id=card.program_id,
enrolled_at_store_id=card.enrolled_at_store_id,
customer_name=customer.full_name if customer else None,
customer_email=customer.email if customer else None,
merchant_name=card.merchant.name if card.merchant else None,
qr_code_data=card.qr_code_data or card.card_number,
program_name=program.display_name,
program_type=program.loyalty_type,
reward_description=program.stamps_reward_description,
stamp_count=card.stamp_count,
stamps_target=program.stamps_target,
stamps_until_reward=max(0, program.stamps_target - card.stamp_count),
total_stamps_earned=card.total_stamps_earned,
stamps_redeemed=card.stamps_redeemed,
points_balance=card.points_balance,
total_points_earned=card.total_points_earned,
points_redeemed=card.points_redeemed,
is_active=card.is_active,
created_at=card.created_at,
last_stamp_at=card.last_stamp_at,
last_points_at=card.last_points_at,
last_redemption_at=card.last_redemption_at,
last_activity_at=card.last_activity_at,
has_google_wallet=bool(card.google_object_id),
has_apple_wallet=bool(card.apple_serial_number),
)
@store_router.get("/transactions", response_model=TransactionListResponse)
def list_store_transactions(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""List recent transactions for this merchant's loyalty program."""
store_id = current_user.token_store_id
merchant_id = get_store_merchant_id(db, store_id)
transactions, total = card_service.get_store_transactions(
db, merchant_id, skip=skip, limit=limit
)
return TransactionListResponse(
transactions=[TransactionResponse.model_validate(t) for t in transactions],
total=total,
)
@store_router.post("/cards/enroll", response_model=CardResponse, status_code=201)
def enroll_customer(
data: CardEnrollRequest,