From 1194731f3327852d1f9688a3c09753d3d09cbada Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 24 Feb 2026 22:10:10 +0100 Subject: [PATCH] fix: card lookup 422 caused by route ordering conflict 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 --- app/modules/loyalty/routes/api/store.py | 146 ++++++++++++------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/app/modules/loyalty/routes/api/store.py b/app/modules/loyalty/routes/api/store.py index 94d132c3..1b075432 100644 --- a/app/modules/loyalty/routes/api/store.py +++ b/app/modules/loyalty/routes/api/store.py @@ -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,