All checks were successful
- Add centralized _is_noqa_suppressed() to BaseValidator with normalization (accepts both SEC001 and SEC-001 formats for ruff compatibility) - Wire noqa support into all 21 security and 18 performance check functions - Add ruff external config for SEC/PERF/MOD/EXC codes in pyproject.toml - Convert all 280 Python noqa comments to dashless format (ruff-compatible) - Add site/ to IGNORE_PATTERNS (excludes mkdocs build output) - Suppress 152 false positive findings (test passwords, seed data, validator self-references, Apple Wallet SHA1, etc.) - Security: 79 errors → 0, 60 warnings → 0 - Performance: 80 warnings → 77 (3 test script suppressions) - Add proposal doc with noqa inventory and remaining findings recommendations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
391 lines
12 KiB
Python
391 lines
12 KiB
Python
# 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, store_id: int):
|
|
super().__init__(
|
|
message=f"Store {store_id} already has a loyalty program",
|
|
error_code="LOYALTY_PROGRAM_ALREADY_EXISTS",
|
|
details={"store_id": store_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",
|
|
]
|