# app/modules/loyalty/routes/api/storefront.py """ Loyalty Module - Storefront API Routes Customer-facing endpoints for: - View loyalty card and balance - View transaction history - Self-service enrollment - Get program information Uses vendor from middleware context (VendorContextMiddleware). """ import logging from fastapi import APIRouter, Depends, Query, Request from sqlalchemy.orm import Session from app.api.deps import get_current_customer_api from app.core.database import get_db from app.modules.customers.schemas import CustomerContext from app.modules.loyalty.services import card_service, program_service from app.modules.loyalty.schemas import ( CardResponse, CardEnrollRequest, TransactionListResponse, TransactionResponse, ProgramResponse, ) from app.modules.tenancy.exceptions import VendorNotFoundException router = APIRouter() logger = logging.getLogger(__name__) # ============================================================================= # Public Endpoints (No Authentication Required) # ============================================================================= @router.get("/loyalty/program") def get_program_info( request: Request, db: Session = Depends(get_db), ): """ Get loyalty program information for current vendor. Public endpoint - no authentication required. """ vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") program = program_service.get_program_by_vendor(db, vendor.id) if not program: return None response = ProgramResponse.model_validate(program) response.is_stamps_enabled = program.is_stamps_enabled response.is_points_enabled = program.is_points_enabled response.display_name = program.display_name return response @router.post("/loyalty/enroll") def self_enroll( request: Request, data: CardEnrollRequest, db: Session = Depends(get_db), ): """ Self-service enrollment. Public endpoint - customers can enroll via QR code without authentication. """ vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.info(f"Self-enrollment for {data.customer_email} at vendor {vendor.subdomain}") card = card_service.enroll_customer( db, vendor_id=vendor.id, customer_email=data.customer_email, customer_phone=data.customer_phone, customer_name=data.customer_name, ) return CardResponse.model_validate(card) # ============================================================================= # Authenticated Endpoints # ============================================================================= @router.get("/loyalty/card") def get_my_card( request: Request, customer: CustomerContext = Depends(get_current_customer_api), db: Session = Depends(get_db), ): """ Get customer's loyalty card and program info. Returns card details, program info, and available rewards. """ vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug(f"Getting loyalty card for customer {customer.id}") # Get program program = program_service.get_program_by_vendor(db, vendor.id) if not program: return {"card": None, "program": None, "locations": []} # Look up card by customer email card = card_service.get_card_by_customer_email( db, company_id=program.company_id, customer_email=customer.email, ) if not card: return {"card": None, "program": None, "locations": []} # Get company locations from app.modules.tenancy.models import Vendor as VendorModel locations = ( db.query(VendorModel) .filter(VendorModel.company_id == program.company_id, VendorModel.is_active == True) .all() ) program_response = ProgramResponse.model_validate(program) program_response.is_stamps_enabled = program.is_stamps_enabled program_response.is_points_enabled = program.is_points_enabled program_response.display_name = program.display_name return { "card": CardResponse.model_validate(card), "program": program_response, "locations": [{"id": v.id, "name": v.name} for v in locations], } @router.get("/loyalty/transactions") def get_my_transactions( request: Request, skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), customer: CustomerContext = Depends(get_current_customer_api), db: Session = Depends(get_db), ): """ Get customer's loyalty transaction history. """ vendor = getattr(request.state, "vendor", None) if not vendor: raise VendorNotFoundException("context", identifier_type="subdomain") logger.debug(f"Getting transactions for customer {customer.id}") # Get program program = program_service.get_program_by_vendor(db, vendor.id) if not program: return {"transactions": [], "total": 0} # Get card card = card_service.get_card_by_customer_email( db, company_id=program.company_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 Vendor as VendorModel query = ( db.query(LoyaltyTransaction) .filter(LoyaltyTransaction.card_id == card.id) .order_by(LoyaltyTransaction.transaction_at.desc()) ) total = query.count() transactions = query.offset(skip).limit(limit).all() # Build response with vendor names tx_responses = [] for tx in transactions: tx_data = { "id": tx.id, "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, "transaction_at": tx.transaction_at.isoformat() if tx.transaction_at else None, "notes": tx.notes, "vendor_name": None, } if tx.vendor_id: vendor_obj = db.query(VendorModel).filter(VendorModel.id == tx.vendor_id).first() if vendor_obj: tx_data["vendor_name"] = vendor_obj.name tx_responses.append(tx_data) return {"transactions": tx_responses, "total": total}