fix(loyalty): fix runtime bugs in storefront routes, point expiration, and enforce settings
- Add total_points_voided column to LoyaltyCard with migration (loyalty_002) - Fix storefront self_enroll to use correct service method signature and schema fields - Fix get_my_card/get_my_transactions to use get_card_by_customer_and_merchant - Fix transaction history field reference (balance_after -> points_balance_after) - Fix point_expiration task: wrong field names and manual balance update -> card.expire_points() - Register storefront_router in definition.py and export all routers from __init__.py - Enforce MerchantLoyaltySettings in storefront enrollment, points, and stamp void operations - Fix test fixture using non-existent balance_after column - Suppress intentional architecture validator warnings in templates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,13 @@ Loyalty module API routes.
|
||||
Provides REST API endpoints for:
|
||||
- Admin: Platform-wide loyalty program management
|
||||
- Store: Store loyalty operations (stamps, points, cards)
|
||||
- Public: Customer enrollment and wallet passes
|
||||
- Platform: Platform administration
|
||||
- Storefront: Customer enrollment and wallet passes
|
||||
"""
|
||||
|
||||
__all__: list[str] = []
|
||||
from app.modules.loyalty.routes.api.admin import admin_router
|
||||
from app.modules.loyalty.routes.api.store import store_router
|
||||
from app.modules.loyalty.routes.api.platform import platform_router
|
||||
from app.modules.loyalty.routes.api.storefront import storefront_router
|
||||
|
||||
__all__ = ["admin_router", "store_router", "platform_router", "storefront_router"]
|
||||
|
||||
@@ -13,7 +13,7 @@ Uses store from middleware context (StoreContextMiddleware).
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_customer_api
|
||||
@@ -29,7 +29,7 @@ from app.modules.loyalty.schemas import (
|
||||
)
|
||||
from app.modules.tenancy.exceptions import StoreNotFoundException
|
||||
|
||||
router = APIRouter()
|
||||
storefront_router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ logger = logging.getLogger(__name__)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/loyalty/program")
|
||||
@storefront_router.get("/loyalty/program")
|
||||
def get_program_info(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -63,7 +63,7 @@ def get_program_info(
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/loyalty/enroll")
|
||||
@storefront_router.post("/loyalty/enroll")
|
||||
def self_enroll(
|
||||
request: Request,
|
||||
data: CardEnrollRequest,
|
||||
@@ -77,15 +77,31 @@ def self_enroll(
|
||||
if not store:
|
||||
raise StoreNotFoundException("context", identifier_type="subdomain")
|
||||
|
||||
logger.info(f"Self-enrollment for {data.customer_email} at store {store.subdomain}")
|
||||
# Check if self-enrollment is allowed
|
||||
settings = program_service.get_merchant_settings(db, store.merchant_id)
|
||||
if settings and not settings.allow_self_enrollment:
|
||||
raise HTTPException(403, "Self-enrollment is not available")
|
||||
|
||||
card = card_service.enroll_customer(
|
||||
db,
|
||||
store_id=store.id,
|
||||
customer_email=data.customer_email,
|
||||
customer_phone=data.customer_phone,
|
||||
customer_name=data.customer_name,
|
||||
)
|
||||
# Resolve customer_id
|
||||
customer_id = data.customer_id
|
||||
if not customer_id and data.email:
|
||||
from app.modules.customers.models.customer import Customer
|
||||
|
||||
customer = (
|
||||
db.query(Customer)
|
||||
.filter(Customer.email == data.email, Customer.store_id == store.id)
|
||||
.first()
|
||||
)
|
||||
if not customer:
|
||||
raise HTTPException(400, "Customer not found with provided email")
|
||||
customer_id = customer.id
|
||||
|
||||
if not customer_id:
|
||||
raise HTTPException(400, "Either customer_id or email is required")
|
||||
|
||||
logger.info(f"Self-enrollment for customer {customer_id} at store {store.subdomain}")
|
||||
|
||||
card = card_service.enroll_customer_for_store(db, customer_id, store.id)
|
||||
|
||||
return CardResponse.model_validate(card)
|
||||
|
||||
@@ -95,7 +111,7 @@ def self_enroll(
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/loyalty/card")
|
||||
@storefront_router.get("/loyalty/card")
|
||||
def get_my_card(
|
||||
request: Request,
|
||||
customer: CustomerContext = Depends(get_current_customer_api),
|
||||
@@ -116,11 +132,11 @@ def get_my_card(
|
||||
if not program:
|
||||
return {"card": None, "program": None, "locations": []}
|
||||
|
||||
# Look up card by customer email
|
||||
card = card_service.get_card_by_customer_email(
|
||||
# Look up card by customer ID and merchant
|
||||
card = card_service.get_card_by_customer_and_merchant(
|
||||
db,
|
||||
customer_id=customer.id,
|
||||
merchant_id=program.merchant_id,
|
||||
customer_email=customer.email,
|
||||
)
|
||||
|
||||
if not card:
|
||||
@@ -146,7 +162,7 @@ def get_my_card(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/loyalty/transactions")
|
||||
@storefront_router.get("/loyalty/transactions")
|
||||
def get_my_transactions(
|
||||
request: Request,
|
||||
skip: int = Query(0, ge=0),
|
||||
@@ -169,17 +185,16 @@ def get_my_transactions(
|
||||
return {"transactions": [], "total": 0}
|
||||
|
||||
# Get card
|
||||
card = card_service.get_card_by_customer_email(
|
||||
card = card_service.get_card_by_customer_and_merchant(
|
||||
db,
|
||||
customer_id=customer.id,
|
||||
merchant_id=program.merchant_id,
|
||||
customer_email=customer.email,
|
||||
)
|
||||
|
||||
if not card:
|
||||
return {"transactions": [], "total": 0}
|
||||
|
||||
# Get transactions
|
||||
from sqlalchemy import func
|
||||
from app.modules.loyalty.models import LoyaltyTransaction
|
||||
from app.modules.tenancy.models import Store as StoreModel
|
||||
|
||||
@@ -200,7 +215,8 @@ def get_my_transactions(
|
||||
"transaction_type": tx.transaction_type.value if hasattr(tx.transaction_type, 'value') else str(tx.transaction_type),
|
||||
"points_delta": tx.points_delta,
|
||||
"stamps_delta": tx.stamps_delta,
|
||||
"balance_after": tx.balance_after,
|
||||
"points_balance_after": tx.points_balance_after,
|
||||
"stamps_balance_after": tx.stamps_balance_after,
|
||||
"transaction_at": tx.transaction_at.isoformat() if tx.transaction_at else None,
|
||||
"notes": tx.notes,
|
||||
"store_name": None,
|
||||
|
||||
Reference in New Issue
Block a user