# app/modules/loyalty/exceptions.py """ Loyalty module exceptions. Custom exceptions for loyalty program operations including stamp/points management, card operations, and wallet integration. """ from typing import Any from app.exceptions.base import ( BusinessLogicException, ConflictException, ResourceNotFoundException, ValidationException, ) class LoyaltyException(BusinessLogicException): """Base exception for loyalty module errors.""" def __init__( self, message: str, error_code: str = "LOYALTY_ERROR", details: dict[str, Any] | None = None, ): super().__init__(message=message, error_code=error_code, details=details) # ============================================================================= # Program Exceptions # ============================================================================= class LoyaltyProgramNotFoundException(ResourceNotFoundException): """Raised when a loyalty program is not found.""" def __init__(self, identifier: str): super().__init__("LoyaltyProgram", identifier) class LoyaltyProgramAlreadyExistsException(ConflictException): """Raised when store already has a loyalty program.""" def __init__(self, merchant_id: int): super().__init__( message=f"Merchant {merchant_id} already has a loyalty program", error_code="LOYALTY_PROGRAM_ALREADY_EXISTS", details={"merchant_id": merchant_id}, ) class LoyaltyProgramInactiveException(LoyaltyException): """Raised when trying to use an inactive loyalty program.""" def __init__(self, program_id: int): super().__init__( message="Loyalty program is not active", error_code="LOYALTY_PROGRAM_INACTIVE", details={"program_id": program_id}, ) # ============================================================================= # Card Exceptions # ============================================================================= class LoyaltyCardNotFoundException(ResourceNotFoundException): """Raised when a loyalty card is not found.""" def __init__(self, identifier: str): super().__init__("LoyaltyCard", identifier) class LoyaltyCardAlreadyExistsException(ConflictException): """Raised when customer already has a card for this program.""" def __init__(self, customer_id: int, program_id: int): super().__init__( message="Customer already enrolled in this loyalty program", error_code="LOYALTY_CARD_ALREADY_EXISTS", details={"customer_id": customer_id, "program_id": program_id}, ) class LoyaltyCardInactiveException(LoyaltyException): """Raised when trying to use an inactive loyalty card.""" def __init__(self, card_id: int): super().__init__( message="Loyalty card is not active", error_code="LOYALTY_CARD_INACTIVE", details={"card_id": card_id}, ) # ============================================================================= # Anti-Fraud Exceptions # ============================================================================= class StaffPinNotFoundException(ResourceNotFoundException): """Raised when a staff PIN is not found.""" def __init__(self, identifier: str): super().__init__("StaffPin", identifier) class StaffPinRequiredException(LoyaltyException): """Raised when staff PIN is required but not provided.""" def __init__(self): super().__init__( message="Staff PIN is required for this operation", error_code="STAFF_PIN_REQUIRED", ) class InvalidStaffPinException(LoyaltyException): """Raised when staff PIN is invalid.""" def __init__(self, remaining_attempts: int | None = None): details = {} if remaining_attempts is not None: details["remaining_attempts"] = remaining_attempts super().__init__( message="Invalid staff PIN", error_code="INVALID_STAFF_PIN", details=details if details else None, ) class StaffPinLockedException(LoyaltyException): """Raised when staff PIN is locked due to too many failed attempts.""" def __init__(self, locked_until: str): super().__init__( message="Staff PIN is locked due to too many failed attempts", error_code="STAFF_PIN_LOCKED", details={"locked_until": locked_until}, ) class StampCooldownException(LoyaltyException): """Raised when trying to stamp before cooldown period ends.""" def __init__(self, cooldown_ends: str, cooldown_minutes: int): super().__init__( message=f"Please wait {cooldown_minutes} minutes between stamps", error_code="STAMP_COOLDOWN", details={"cooldown_ends": cooldown_ends, "cooldown_minutes": cooldown_minutes}, ) class DailyStampLimitException(LoyaltyException): """Raised when daily stamp limit is exceeded.""" def __init__(self, max_daily_stamps: int, stamps_today: int): super().__init__( message=f"Daily stamp limit of {max_daily_stamps} reached", error_code="DAILY_STAMP_LIMIT", details={"max_daily_stamps": max_daily_stamps, "stamps_today": stamps_today}, ) # ============================================================================= # Redemption Exceptions # ============================================================================= class InsufficientStampsException(LoyaltyException): """Raised when card doesn't have enough stamps to redeem.""" def __init__(self, current_stamps: int, required_stamps: int): super().__init__( message=f"Insufficient stamps: {current_stamps}/{required_stamps}", error_code="INSUFFICIENT_STAMPS", details={"current_stamps": current_stamps, "required_stamps": required_stamps}, ) class InsufficientPointsException(LoyaltyException): """Raised when card doesn't have enough points to redeem.""" def __init__(self, current_points: int, required_points: int): super().__init__( message=f"Insufficient points: {current_points}/{required_points}", error_code="INSUFFICIENT_POINTS", details={"current_points": current_points, "required_points": required_points}, ) class InvalidRewardException(LoyaltyException): """Raised when trying to redeem an invalid or unavailable reward.""" def __init__(self, reward_id: str): super().__init__( message="Invalid or unavailable reward", error_code="INVALID_REWARD", details={"reward_id": reward_id}, ) # ============================================================================= # Wallet Exceptions # ============================================================================= class WalletIntegrationException(LoyaltyException): """Raised when wallet integration fails.""" def __init__(self, provider: str, message: str): super().__init__( message=f"Wallet integration error: {message}", error_code="WALLET_INTEGRATION_ERROR", details={"provider": provider}, ) class GoogleWalletNotConfiguredException(LoyaltyException): """Raised when Google Wallet is not configured.""" def __init__(self): super().__init__( message="Google Wallet is not configured for this program", error_code="GOOGLE_WALLET_NOT_CONFIGURED", ) class AppleWalletNotConfiguredException(LoyaltyException): """Raised when Apple Wallet is not configured.""" def __init__(self): super().__init__( message="Apple Wallet is not configured for this program", error_code="APPLE_WALLET_NOT_CONFIGURED", ) # ============================================================================= # Authentication Exceptions # ============================================================================= class InvalidAppleAuthTokenException(LoyaltyException): """Raised when Apple Wallet auth token is invalid.""" def __init__(self): super().__init__( message="Invalid Apple Wallet authentication token", error_code="INVALID_APPLE_AUTH_TOKEN", ) self.status_code = 401 class ApplePassGenerationException(LoyaltyException): """Raised when Apple Wallet pass generation fails.""" def __init__(self, card_id: int): super().__init__( message="Failed to generate Apple Wallet pass", error_code="APPLE_PASS_GENERATION_FAILED", details={"card_id": card_id}, ) self.status_code = 500 class DeviceRegistrationException(LoyaltyException): """Raised when Apple Wallet device registration/unregistration fails.""" def __init__(self, device_id: str, operation: str = "register"): super().__init__( message=f"Failed to {operation} device", error_code="DEVICE_REGISTRATION_FAILED", details={"device_id": device_id, "operation": operation}, ) self.status_code = 500 # ============================================================================= # Enrollment Exceptions # ============================================================================= class SelfEnrollmentDisabledException(LoyaltyException): """Raised when self-enrollment is not allowed.""" def __init__(self): super().__init__( message="Self-enrollment is not available", error_code="SELF_ENROLLMENT_DISABLED", ) self.status_code = 403 class CustomerNotFoundByEmailException(LoyaltyException): """Raised when customer is not found by email during enrollment.""" def __init__(self, email: str): super().__init__( message="Customer not found with provided email", error_code="CUSTOMER_NOT_FOUND_BY_EMAIL", details={"email": email}, ) class CustomerIdentifierRequiredException(LoyaltyException): """Raised when neither customer_id nor email is provided.""" def __init__(self): super().__init__( message="Either customer_id or email is required", error_code="CUSTOMER_IDENTIFIER_REQUIRED", ) # ============================================================================= # Order Exceptions # ============================================================================= class OrderReferenceRequiredException(LoyaltyException): """Raised when order reference is required but not provided.""" def __init__(self): super().__init__( message="Order reference required", error_code="ORDER_REFERENCE_REQUIRED", ) # ============================================================================= # Validation Exceptions # ============================================================================= class LoyaltyValidationException(ValidationException): # noqa: MOD025 """Raised when loyalty data validation fails.""" def __init__( self, message: str = "Loyalty validation failed", field: str | None = None, details: dict[str, Any] | None = None, ): super().__init__(message=message, field=field, details=details) self.error_code = "LOYALTY_VALIDATION_FAILED" __all__ = [ # Base "LoyaltyException", # Program "LoyaltyProgramNotFoundException", "LoyaltyProgramAlreadyExistsException", "LoyaltyProgramInactiveException", # Card "LoyaltyCardNotFoundException", "LoyaltyCardAlreadyExistsException", "LoyaltyCardInactiveException", # Anti-Fraud "StaffPinNotFoundException", "StaffPinRequiredException", "InvalidStaffPinException", "StaffPinLockedException", "StampCooldownException", "DailyStampLimitException", # Redemption "InsufficientStampsException", "InsufficientPointsException", "InvalidRewardException", # Wallet "WalletIntegrationException", "GoogleWalletNotConfiguredException", "AppleWalletNotConfiguredException", # Authentication "InvalidAppleAuthTokenException", "ApplePassGenerationException", "DeviceRegistrationException", # Enrollment "SelfEnrollmentDisabledException", "CustomerNotFoundByEmailException", "CustomerIdentifierRequiredException", # Order "OrderReferenceRequiredException", # Validation "LoyaltyValidationException", ]