Files
orion/app/modules/billing/exceptions.py
Samir Boulahtit 1b8a40f1ff
All checks were successful
CI / dependency-scanning (push) Successful in 27s
CI / docs (push) Successful in 35s
CI / ruff (push) Successful in 8s
CI / pytest (push) Successful in 34m22s
CI / validate (push) Successful in 19s
CI / deploy (push) Successful in 2m25s
feat(validators): add noqa suppression support to security and performance validators
- 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>
2026-02-14 22:56:56 +01:00

250 lines
7.7 KiB
Python

# app/modules/billing/exceptions.py
"""
Billing module exceptions.
This module provides exception classes for billing operations including:
- Subscription management
- Payment processing (Stripe)
- Feature management
- Tier management
"""
from app.exceptions.base import (
BusinessLogicException,
ResourceNotFoundException,
ServiceUnavailableException,
ValidationException,
)
__all__ = [
# Base billing exception
"BillingException",
# Subscription exceptions
"SubscriptionNotFoundException",
"NoActiveSubscriptionException",
"SubscriptionNotCancelledException",
"SubscriptionAlreadyCancelledException",
# Tier exceptions
"TierNotFoundException",
"TierLimitExceededException",
# Payment exceptions
"PaymentSystemNotConfiguredException",
"StripeNotConfiguredException",
"StripePriceNotConfiguredException",
"PaymentFailedException",
# Webhook exceptions
"InvalidWebhookSignatureException",
"WebhookMissingSignatureException",
"WebhookVerificationException",
# Feature exceptions
"FeatureNotFoundException",
"InvalidFeatureCodesError",
]
# =============================================================================
# Base Billing Exception
# =============================================================================
class BillingException(BusinessLogicException):
"""Base exception for billing module errors."""
def __init__(self, message: str, error_code: str = "BILLING_ERROR", details: dict | None = None):
super().__init__(message=message, error_code=error_code, details=details)
# =============================================================================
# Subscription Exceptions
# =============================================================================
class SubscriptionNotFoundException(ResourceNotFoundException):
"""Raised when a subscription is not found."""
def __init__(self, store_id: int):
super().__init__(
resource_type="Subscription",
identifier=str(store_id),
error_code="SUBSCRIPTION_NOT_FOUND",
)
class NoActiveSubscriptionException(BusinessLogicException):
"""Raised when no active subscription exists for an operation that requires one."""
def __init__(self, message: str = "No active subscription found"):
super().__init__(
message=message,
error_code="NO_ACTIVE_SUBSCRIPTION",
)
class SubscriptionNotCancelledException(BusinessLogicException):
"""Raised when trying to reactivate a subscription that is not cancelled."""
def __init__(self):
super().__init__(
message="Subscription is not cancelled",
error_code="SUBSCRIPTION_NOT_CANCELLED",
)
class SubscriptionAlreadyCancelledException(BusinessLogicException): # noqa: MOD025
"""Raised when trying to cancel an already cancelled subscription."""
def __init__(self):
super().__init__(
message="Subscription is already cancelled",
error_code="SUBSCRIPTION_ALREADY_CANCELLED",
)
# =============================================================================
# Tier Exceptions
# =============================================================================
class TierNotFoundException(ResourceNotFoundException):
"""Raised when a subscription tier is not found."""
def __init__(self, tier_code: str):
super().__init__(
resource_type="SubscriptionTier",
identifier=tier_code,
message=f"Subscription tier '{tier_code}' not found",
error_code="TIER_NOT_FOUND",
)
self.tier_code = tier_code
class TierLimitExceededException(BillingException):
"""Raised when a tier limit is exceeded."""
def __init__(self, message: str, limit_type: str, current: int, limit: int):
super().__init__(
message=message,
error_code="TIER_LIMIT_EXCEEDED",
details={
"limit_type": limit_type,
"current": current,
"limit": limit,
},
)
self.limit_type = limit_type
self.current = current
self.limit = limit
# =============================================================================
# Payment Exceptions
# =============================================================================
class PaymentSystemNotConfiguredException(ServiceUnavailableException):
"""Raised when the payment system (Stripe) is not configured."""
def __init__(self):
super().__init__(message="Payment system not configured")
class StripeNotConfiguredException(BillingException):
"""Raised when Stripe is not configured."""
def __init__(self):
super().__init__(
message="Stripe is not configured",
error_code="STRIPE_NOT_CONFIGURED",
)
class StripePriceNotConfiguredException(BusinessLogicException):
"""Raised when Stripe price is not configured for a tier."""
def __init__(self, tier_code: str):
super().__init__(
message=f"Stripe price not configured for tier '{tier_code}'",
error_code="STRIPE_PRICE_NOT_CONFIGURED",
details={"tier_code": tier_code},
)
self.tier_code = tier_code
class PaymentFailedException(BillingException): # noqa: MOD025
"""Raised when a payment fails."""
def __init__(self, message: str, stripe_error: str | None = None):
details = {}
if stripe_error:
details["stripe_error"] = stripe_error
super().__init__(
message=message,
error_code="PAYMENT_FAILED",
details=details if details else None,
)
self.stripe_error = stripe_error
# =============================================================================
# Webhook Exceptions
# =============================================================================
class InvalidWebhookSignatureException(BusinessLogicException):
"""Raised when Stripe webhook signature verification fails."""
def __init__(self, message: str = "Invalid webhook signature"):
super().__init__(
message=message,
error_code="INVALID_WEBHOOK_SIGNATURE",
)
class WebhookMissingSignatureException(BusinessLogicException):
"""Raised when Stripe webhook is missing the signature header."""
def __init__(self):
super().__init__(
message="Missing Stripe-Signature header",
error_code="WEBHOOK_MISSING_SIGNATURE",
)
class WebhookVerificationException(BillingException):
"""Raised when webhook signature verification fails."""
def __init__(self, message: str = "Invalid webhook signature"):
super().__init__(
message=message,
error_code="WEBHOOK_VERIFICATION_FAILED",
)
# =============================================================================
# Feature Exceptions
# =============================================================================
class FeatureNotFoundException(ResourceNotFoundException):
"""Raised when a feature is not found."""
def __init__(self, feature_code: str):
super().__init__(
resource_type="Feature",
identifier=feature_code,
message=f"Feature '{feature_code}' not found",
)
self.feature_code = feature_code
class InvalidFeatureCodesError(ValidationException):
"""Invalid feature codes provided."""
def __init__(self, invalid_codes: set[str]):
codes_str = ", ".join(sorted(invalid_codes))
super().__init__(
message=f"Invalid feature codes: {codes_str}",
details={"invalid_codes": list(invalid_codes)},
)
self.invalid_codes = invalid_codes