fix: add GET /cards/lookup endpoint for loyalty terminal customer search
Some checks failed
Some checks failed
The terminal JS uses GET with a free-text ?q= parameter, but only a POST endpoint existed with typed params (card_id, qr_code, card_number). - Add search_card_for_store service method (tries card number then email) - Add GET /cards/lookup route using the service method - Extract _build_card_lookup_response helper to DRY up POST and GET endpoints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -311,6 +311,73 @@ def list_cards(
|
|||||||
return CardListResponse(cards=card_responses, total=total)
|
return CardListResponse(cards=card_responses, total=total)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_card_lookup_response(card, db=None) -> CardLookupResponse:
|
||||||
|
"""Build a CardLookupResponse from a card object."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
program = card.program
|
||||||
|
|
||||||
|
can_stamp, _ = card.can_stamp(program.cooldown_minutes)
|
||||||
|
cooldown_ends = None
|
||||||
|
if not can_stamp and card.last_stamp_at:
|
||||||
|
cooldown_ends = card.last_stamp_at + timedelta(minutes=program.cooldown_minutes)
|
||||||
|
|
||||||
|
stamps_today = card_service.get_stamps_today(db, card.id) if db else 0
|
||||||
|
|
||||||
|
available_rewards = []
|
||||||
|
for reward in program.points_rewards or []:
|
||||||
|
if reward.get("is_active", True) and card.points_balance >= reward.get(
|
||||||
|
"points_required", 0
|
||||||
|
):
|
||||||
|
available_rewards.append(reward)
|
||||||
|
|
||||||
|
return CardLookupResponse(
|
||||||
|
card_id=card.id,
|
||||||
|
card_number=card.card_number,
|
||||||
|
customer_id=card.customer_id,
|
||||||
|
customer_name=card.customer.full_name if card.customer else None,
|
||||||
|
customer_email=card.customer.email if card.customer else "",
|
||||||
|
merchant_id=card.merchant_id,
|
||||||
|
merchant_name=card.merchant.name if card.merchant else None,
|
||||||
|
stamp_count=card.stamp_count,
|
||||||
|
stamps_target=program.stamps_target,
|
||||||
|
stamps_until_reward=max(0, program.stamps_target - card.stamp_count),
|
||||||
|
points_balance=card.points_balance,
|
||||||
|
can_redeem_stamps=card.stamp_count >= program.stamps_target,
|
||||||
|
stamp_reward_description=program.stamps_reward_description,
|
||||||
|
available_rewards=available_rewards,
|
||||||
|
can_stamp=can_stamp,
|
||||||
|
cooldown_ends_at=cooldown_ends,
|
||||||
|
stamps_today=stamps_today,
|
||||||
|
max_daily_stamps=program.max_daily_stamps,
|
||||||
|
can_earn_more_stamps=stamps_today < program.max_daily_stamps,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@store_router.get("/cards/lookup", response_model=CardLookupResponse)
|
||||||
|
def search_card(
|
||||||
|
request: Request,
|
||||||
|
q: str = Query(..., description="Search by email, card number, or name"),
|
||||||
|
current_user: User = Depends(get_current_store_api),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Search for a card by email, card number, or customer name.
|
||||||
|
|
||||||
|
Tries matching in order: card number, then customer email.
|
||||||
|
Card must belong to the same merchant as the store.
|
||||||
|
"""
|
||||||
|
store_id = current_user.token_store_id
|
||||||
|
|
||||||
|
card = card_service.search_card_for_store(db, store_id, q)
|
||||||
|
if not card:
|
||||||
|
from app.modules.loyalty.exceptions import LoyaltyCardNotFoundException
|
||||||
|
|
||||||
|
raise LoyaltyCardNotFoundException(q)
|
||||||
|
|
||||||
|
return _build_card_lookup_response(card, db)
|
||||||
|
|
||||||
|
|
||||||
@store_router.post("/cards/lookup", response_model=CardLookupResponse)
|
@store_router.post("/cards/lookup", response_model=CardLookupResponse)
|
||||||
def lookup_card(
|
def lookup_card(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -337,46 +404,7 @@ def lookup_card(
|
|||||||
card_number=card_number,
|
card_number=card_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
program = card.program
|
return _build_card_lookup_response(card, db)
|
||||||
|
|
||||||
# Check cooldown
|
|
||||||
can_stamp, _ = card.can_stamp(program.cooldown_minutes)
|
|
||||||
cooldown_ends = None
|
|
||||||
if not can_stamp and card.last_stamp_at:
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
cooldown_ends = card.last_stamp_at + timedelta(minutes=program.cooldown_minutes)
|
|
||||||
|
|
||||||
# Get stamps today
|
|
||||||
stamps_today = card_service.get_stamps_today(db, card.id)
|
|
||||||
|
|
||||||
# Get available points rewards
|
|
||||||
available_rewards = []
|
|
||||||
for reward in program.points_rewards or []:
|
|
||||||
if reward.get("is_active", True) and card.points_balance >= reward.get("points_required", 0):
|
|
||||||
available_rewards.append(reward)
|
|
||||||
|
|
||||||
return CardLookupResponse(
|
|
||||||
card_id=card.id,
|
|
||||||
card_number=card.card_number,
|
|
||||||
customer_id=card.customer_id,
|
|
||||||
customer_name=card.customer.full_name if card.customer else None,
|
|
||||||
customer_email=card.customer.email if card.customer else "",
|
|
||||||
merchant_id=card.merchant_id,
|
|
||||||
merchant_name=card.merchant.name if card.merchant else None,
|
|
||||||
stamp_count=card.stamp_count,
|
|
||||||
stamps_target=program.stamps_target,
|
|
||||||
stamps_until_reward=max(0, program.stamps_target - card.stamp_count),
|
|
||||||
points_balance=card.points_balance,
|
|
||||||
can_redeem_stamps=card.stamp_count >= program.stamps_target,
|
|
||||||
stamp_reward_description=program.stamps_reward_description,
|
|
||||||
available_rewards=available_rewards,
|
|
||||||
can_stamp=can_stamp,
|
|
||||||
cooldown_ends_at=cooldown_ends,
|
|
||||||
stamps_today=stamps_today,
|
|
||||||
max_daily_stamps=program.max_daily_stamps,
|
|
||||||
can_earn_more_stamps=stamps_today < program.max_daily_stamps,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@store_router.post("/cards/enroll", response_model=CardResponse, status_code=201)
|
@store_router.post("/cards/enroll", response_model=CardResponse, status_code=201)
|
||||||
|
|||||||
@@ -255,6 +255,50 @@ class CardService:
|
|||||||
merchant_id=store.merchant_id,
|
merchant_id=store.merchant_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def search_card_for_store(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
store_id: int,
|
||||||
|
query: str,
|
||||||
|
) -> LoyaltyCard | None:
|
||||||
|
"""
|
||||||
|
Search for a card by free-text query (card number or customer email).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database session
|
||||||
|
store_id: Store ID (to scope merchant and customer lookup)
|
||||||
|
query: Search string — card number or customer email
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Found card or None
|
||||||
|
"""
|
||||||
|
from app.modules.customers.models import Customer
|
||||||
|
from app.modules.tenancy.models import Store
|
||||||
|
|
||||||
|
store = db.query(Store).filter(Store.id == store_id).first()
|
||||||
|
if not store:
|
||||||
|
return None
|
||||||
|
|
||||||
|
merchant_id = store.merchant_id
|
||||||
|
|
||||||
|
# Try card number
|
||||||
|
card = self.get_card_by_number(db, query)
|
||||||
|
if card and card.merchant_id == merchant_id:
|
||||||
|
return card
|
||||||
|
|
||||||
|
# Try customer email
|
||||||
|
customer = (
|
||||||
|
db.query(Customer)
|
||||||
|
.filter(Customer.email == query, Customer.store_id == store_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if customer:
|
||||||
|
card = self.get_card_by_customer_and_merchant(db, customer.id, merchant_id)
|
||||||
|
if card:
|
||||||
|
return card
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def list_cards(
|
def list_cards(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
|
|||||||
Reference in New Issue
Block a user