fix: loyalty storefront and store card detail — enrollment, context, and Alpine.js
Some checks failed
Some checks failed
- Fix storefront enabled_modules always empty (page_context overwrote computed
set with empty default via extra_context)
- Fix storefront loyalty JS using store's data() instead of shopLayoutData()
- Remove defer from storefront loyalty scripts to prevent Alpine race condition
- Fix enrollment field name mismatch (customer_email → email) in both store
and storefront JS
- Add self-enrollment customer creation (resolve_customer_id with
create_if_missing) including hashed_password and customer_number
- Fix card list showing "Unknown" — add customer_name/email to CardResponse
- Add GET /cards/{card_id} detail endpoint for store card detail page
- Fix enroll-success.html using data() instead of shopLayoutData()
- Fix enrollment redirect reading response.card_number instead of
response.card.card_number
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ from app.api.deps import get_current_store_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.loyalty.schemas import (
|
||||
CardDetailResponse,
|
||||
CardEnrollRequest,
|
||||
CardListResponse,
|
||||
CardLookupResponse,
|
||||
@@ -286,6 +287,7 @@ def list_cards(
|
||||
|
||||
card_responses = []
|
||||
for card in cards:
|
||||
customer = card.customer
|
||||
response = CardResponse(
|
||||
id=card.id,
|
||||
card_number=card.card_number,
|
||||
@@ -293,6 +295,8 @@ def list_cards(
|
||||
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,
|
||||
stamp_count=card.stamp_count,
|
||||
stamps_target=program.stamps_target,
|
||||
stamps_until_reward=max(0, program.stamps_target - card.stamp_count),
|
||||
@@ -311,6 +315,58 @@ 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.program_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),
|
||||
)
|
||||
|
||||
|
||||
def _build_card_lookup_response(card, db=None) -> CardLookupResponse:
|
||||
"""Build a CardLookupResponse from a card object."""
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -78,12 +78,16 @@ def self_enroll(
|
||||
# Check if self-enrollment is allowed
|
||||
program_service.check_self_enrollment_allowed(db, store.merchant_id)
|
||||
|
||||
# Resolve customer_id
|
||||
# Resolve customer_id (create customer if not found for self-enrollment)
|
||||
customer_id = card_service.resolve_customer_id(
|
||||
db,
|
||||
customer_id=data.customer_id,
|
||||
email=data.email,
|
||||
store_id=store.id,
|
||||
create_if_missing=True,
|
||||
customer_name=data.customer_name,
|
||||
customer_phone=data.customer_phone,
|
||||
customer_birthday=data.customer_birthday,
|
||||
)
|
||||
|
||||
logger.info(f"Self-enrollment for customer {customer_id} at store {store.subdomain}")
|
||||
|
||||
Reference in New Issue
Block a user