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)
|
||||
|
||||
|
||||
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)
|
||||
def lookup_card(
|
||||
request: Request,
|
||||
@@ -337,46 +404,7 @@ def lookup_card(
|
||||
card_number=card_number,
|
||||
)
|
||||
|
||||
program = card.program
|
||||
|
||||
# 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,
|
||||
)
|
||||
return _build_card_lookup_response(card, db)
|
||||
|
||||
|
||||
@store_router.post("/cards/enroll", response_model=CardResponse, status_code=201)
|
||||
|
||||
@@ -255,6 +255,50 @@ class CardService:
|
||||
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(
|
||||
self,
|
||||
db: Session,
|
||||
|
||||
Reference in New Issue
Block a user