fix: loyalty storefront and store card detail — enrollment, context, and Alpine.js
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 46m41s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 30s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- 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:
2026-02-24 14:28:37 +01:00
parent 3de69e55a1
commit 53dfe018c2
13 changed files with 148 additions and 19 deletions

View File

@@ -138,6 +138,10 @@ class CardService:
customer_id: int | None,
email: str | None,
store_id: int,
create_if_missing: bool = False,
customer_name: str | None = None,
customer_phone: str | None = None,
customer_birthday: str | None = None,
) -> int:
"""
Resolve a customer ID from either a direct ID or email lookup.
@@ -147,13 +151,18 @@ class CardService:
customer_id: Direct customer ID (used if provided)
email: Customer email to look up
store_id: Store ID for scoping the email lookup
create_if_missing: If True, create customer when email not found
(used for self-enrollment)
customer_name: Full name for customer creation
customer_phone: Phone for customer creation
customer_birthday: Birthday (YYYY-MM-DD) for customer creation
Returns:
Resolved customer ID
Raises:
CustomerIdentifierRequiredException: If neither customer_id nor email provided
CustomerNotFoundByEmailException: If email lookup fails
CustomerNotFoundByEmailException: If email lookup fails and create_if_missing is False
"""
if customer_id:
return customer_id
@@ -166,9 +175,53 @@ class CardService:
.filter(Customer.email == email, Customer.store_id == store_id)
.first()
)
if not customer:
raise CustomerNotFoundByEmailException(email)
return customer.id
if customer:
return customer.id
if create_if_missing:
import secrets
from app.modules.customers.services.customer_service import (
customer_service,
)
from app.modules.tenancy.models.store import Store
store = db.query(Store).filter(Store.id == store_id).first()
store_code = store.store_code if store else "STORE"
# Parse name into first/last
first_name = customer_name or ""
last_name = ""
if customer_name and " " in customer_name:
parts = customer_name.split(" ", 1)
first_name = parts[0]
last_name = parts[1]
# Generate unusable password hash and unique customer number
unusable_hash = f"!loyalty-enroll!{secrets.token_hex(32)}"
cust_number = customer_service._generate_customer_number(
db, store_id, store_code
)
customer = Customer(
email=email,
first_name=first_name,
last_name=last_name,
phone=customer_phone,
hashed_password=unusable_hash,
customer_number=cust_number,
store_id=store_id,
is_active=True,
)
db.add(customer)
db.flush()
logger.info(
f"Created customer {customer.id} ({email}) "
f"number={cust_number} for self-enrollment"
)
return customer.id
raise CustomerNotFoundByEmailException(email)
raise CustomerIdentifierRequiredException()