Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1144 lines
36 KiB
Python
1144 lines
36 KiB
Python
# app/modules/tenancy/exceptions.py
|
|
"""
|
|
Tenancy module exceptions.
|
|
|
|
Exceptions for platform, merchant, store, admin user, team, and domain management.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from app.exceptions.base import (
|
|
AuthenticationException,
|
|
AuthorizationException,
|
|
BusinessLogicException,
|
|
ConflictException,
|
|
ExternalServiceException,
|
|
OrionException,
|
|
ResourceNotFoundException,
|
|
ValidationException,
|
|
)
|
|
|
|
# =============================================================================
|
|
# Authentication Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class InvalidCredentialsException(AuthenticationException):
|
|
"""Raised when login credentials are invalid."""
|
|
|
|
def __init__(self, message: str = "Invalid username or password"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="INVALID_CREDENTIALS",
|
|
)
|
|
|
|
|
|
class TokenExpiredException(AuthenticationException):
|
|
"""Raised when JWT token has expired."""
|
|
|
|
def __init__(self, message: str = "Token has expired"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="TOKEN_EXPIRED",
|
|
)
|
|
|
|
|
|
class InvalidTokenException(AuthenticationException):
|
|
"""Raised when JWT token is invalid or malformed."""
|
|
|
|
def __init__(self, message: str = "Invalid token"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="INVALID_TOKEN",
|
|
)
|
|
|
|
|
|
class InsufficientPermissionsException(AuthorizationException):
|
|
"""Raised when user lacks required permissions for an action."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Insufficient permissions for this action",
|
|
required_permission: str | None = None,
|
|
):
|
|
details = {}
|
|
if required_permission:
|
|
details["required_permission"] = required_permission
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="INSUFFICIENT_PERMISSIONS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class UserNotActiveException(AuthorizationException):
|
|
"""Raised when user account is not active."""
|
|
|
|
def __init__(self, message: str = "User account is not active"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="USER_NOT_ACTIVE",
|
|
)
|
|
|
|
|
|
class AdminRequiredException(AuthorizationException):
|
|
"""Raised when admin privileges are required."""
|
|
|
|
def __init__(self, message: str = "Admin privileges required"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="ADMIN_REQUIRED",
|
|
)
|
|
|
|
|
|
class UserAlreadyExistsException(ConflictException):
|
|
"""Raised when trying to register with existing username/email."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "User already exists",
|
|
field: str | None = None,
|
|
):
|
|
details = {}
|
|
if field:
|
|
details["field"] = field
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="USER_ALREADY_EXISTS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Platform Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class PlatformNotFoundException(OrionException):
|
|
"""Raised when a platform is not found."""
|
|
|
|
def __init__(self, code: str):
|
|
super().__init__(
|
|
message=f"Platform not found: {code}",
|
|
error_code="PLATFORM_NOT_FOUND",
|
|
status_code=404,
|
|
details={"platform_code": code},
|
|
)
|
|
|
|
|
|
class PlatformInactiveException(OrionException): # noqa: MOD-025
|
|
"""Raised when trying to access an inactive platform."""
|
|
|
|
def __init__(self, code: str):
|
|
super().__init__(
|
|
message=f"Platform is inactive: {code}",
|
|
error_code="PLATFORM_INACTIVE",
|
|
status_code=403,
|
|
details={"platform_code": code},
|
|
)
|
|
|
|
|
|
class PlatformUpdateException(OrionException): # noqa: MOD-025
|
|
"""Raised when platform update fails."""
|
|
|
|
def __init__(self, code: str, reason: str):
|
|
super().__init__(
|
|
message=f"Failed to update platform {code}: {reason}",
|
|
error_code="PLATFORM_UPDATE_FAILED",
|
|
status_code=400,
|
|
details={"platform_code": code, "reason": reason},
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Store Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class StoreNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a store is not found."""
|
|
|
|
def __init__(self, store_identifier: str, identifier_type: str = "code"):
|
|
if identifier_type.lower() == "id":
|
|
message = f"Store with ID '{store_identifier}' not found"
|
|
else:
|
|
message = f"Store with code '{store_identifier}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="Store",
|
|
identifier=store_identifier,
|
|
message=message,
|
|
error_code="STORE_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class StoreAlreadyExistsException(ConflictException):
|
|
"""Raised when trying to create a store that already exists."""
|
|
|
|
def __init__(self, store_code: str):
|
|
super().__init__(
|
|
message=f"Store with code '{store_code}' already exists",
|
|
error_code="STORE_ALREADY_EXISTS",
|
|
details={"store_code": store_code},
|
|
)
|
|
|
|
|
|
class StoreNotActiveException(BusinessLogicException):
|
|
"""Raised when trying to perform operations on inactive store."""
|
|
|
|
def __init__(self, store_code: str):
|
|
super().__init__(
|
|
message=f"Store '{store_code}' is not active",
|
|
error_code="STORE_NOT_ACTIVE",
|
|
details={"store_code": store_code},
|
|
)
|
|
|
|
|
|
class StoreNotVerifiedException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when trying to perform operations requiring verified store."""
|
|
|
|
def __init__(self, store_code: str):
|
|
super().__init__(
|
|
message=f"Store '{store_code}' is not verified",
|
|
error_code="STORE_NOT_VERIFIED",
|
|
details={"store_code": store_code},
|
|
)
|
|
|
|
|
|
class UnauthorizedStoreAccessException(AuthorizationException):
|
|
"""Raised when user tries to access store they don't own."""
|
|
|
|
def __init__(self, store_code: str, user_id: int | None = None):
|
|
details = {"store_code": store_code}
|
|
if user_id:
|
|
details["user_id"] = user_id
|
|
|
|
super().__init__(
|
|
message=f"Unauthorized access to store '{store_code}'",
|
|
error_code="UNAUTHORIZED_STORE_ACCESS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class InvalidStoreDataException(ValidationException):
|
|
"""Raised when store data is invalid or incomplete."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid store data",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_STORE_DATA"
|
|
|
|
|
|
class StoreValidationException(ValidationException):
|
|
"""Raised when store validation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Store validation failed",
|
|
field: str | None = None,
|
|
validation_errors: dict[str, str] | None = None,
|
|
):
|
|
details = {}
|
|
if validation_errors:
|
|
details["validation_errors"] = validation_errors
|
|
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "STORE_VALIDATION_FAILED"
|
|
|
|
|
|
class MaxStoresReachedException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when user tries to create more stores than allowed."""
|
|
|
|
def __init__(self, max_stores: int, user_id: int | None = None):
|
|
details = {"max_stores": max_stores}
|
|
if user_id:
|
|
details["user_id"] = user_id
|
|
|
|
super().__init__(
|
|
message=f"Maximum number of stores reached ({max_stores})",
|
|
error_code="MAX_STORES_REACHED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class StoreAccessDeniedException(AuthorizationException): # noqa: MOD-025
|
|
"""Raised when no store context is available for an authenticated endpoint."""
|
|
|
|
def __init__(self, message: str = "No store context available"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="STORE_ACCESS_DENIED",
|
|
)
|
|
|
|
|
|
class StoreOwnerOnlyException(AuthorizationException):
|
|
"""Raised when operation requires store owner role."""
|
|
|
|
def __init__(self, operation: str, store_code: str | None = None):
|
|
details = {"operation": operation}
|
|
if store_code:
|
|
details["store_code"] = store_code
|
|
|
|
super().__init__(
|
|
message=f"Operation '{operation}' requires store owner role",
|
|
error_code="STORE_OWNER_ONLY",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class InsufficientStorePermissionsException(AuthorizationException):
|
|
"""Raised when user lacks required store permission."""
|
|
|
|
def __init__(self, required_permission: str, store_code: str | None = None):
|
|
details = {"required_permission": required_permission}
|
|
if store_code:
|
|
details["store_code"] = store_code
|
|
|
|
super().__init__(
|
|
message=f"Permission required: {required_permission}",
|
|
error_code="INSUFFICIENT_STORE_PERMISSIONS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Merchant Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class MerchantNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a merchant is not found."""
|
|
|
|
def __init__(self, merchant_identifier: str | int, identifier_type: str = "id"):
|
|
if identifier_type.lower() == "id":
|
|
message = f"Merchant with ID '{merchant_identifier}' not found"
|
|
else:
|
|
message = f"Merchant with name '{merchant_identifier}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="Merchant",
|
|
identifier=str(merchant_identifier),
|
|
message=message,
|
|
error_code="MERCHANT_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class MerchantAlreadyExistsException(ConflictException): # noqa: MOD-025
|
|
"""Raised when trying to create a merchant that already exists."""
|
|
|
|
def __init__(self, merchant_name: str):
|
|
super().__init__(
|
|
message=f"Merchant with name '{merchant_name}' already exists",
|
|
error_code="MERCHANT_ALREADY_EXISTS",
|
|
details={"merchant_name": merchant_name},
|
|
)
|
|
|
|
|
|
class MerchantNotActiveException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when trying to perform operations on inactive merchant."""
|
|
|
|
def __init__(self, merchant_id: int):
|
|
super().__init__(
|
|
message=f"Merchant with ID '{merchant_id}' is not active",
|
|
error_code="MERCHANT_NOT_ACTIVE",
|
|
details={"merchant_id": merchant_id},
|
|
)
|
|
|
|
|
|
class MerchantNotVerifiedException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when trying to perform operations requiring verified merchant."""
|
|
|
|
def __init__(self, merchant_id: int):
|
|
super().__init__(
|
|
message=f"Merchant with ID '{merchant_id}' is not verified",
|
|
error_code="MERCHANT_NOT_VERIFIED",
|
|
details={"merchant_id": merchant_id},
|
|
)
|
|
|
|
|
|
class UnauthorizedMerchantAccessException(AuthorizationException): # noqa: MOD-025
|
|
"""Raised when user tries to access merchant they don't own."""
|
|
|
|
def __init__(self, merchant_id: int, user_id: int | None = None):
|
|
details = {"merchant_id": merchant_id}
|
|
if user_id:
|
|
details["user_id"] = user_id
|
|
|
|
super().__init__(
|
|
message=f"Unauthorized access to merchant with ID '{merchant_id}'",
|
|
error_code="UNAUTHORIZED_MERCHANT_ACCESS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class InvalidMerchantDataException(ValidationException): # noqa: MOD-025
|
|
"""Raised when merchant data is invalid or incomplete."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid merchant data",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_MERCHANT_DATA"
|
|
|
|
|
|
class MerchantValidationException(ValidationException): # noqa: MOD-025
|
|
"""Raised when merchant validation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Merchant validation failed",
|
|
field: str | None = None,
|
|
validation_errors: dict[str, str] | None = None,
|
|
):
|
|
details = {}
|
|
if validation_errors:
|
|
details["validation_errors"] = validation_errors
|
|
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "MERCHANT_VALIDATION_FAILED"
|
|
|
|
|
|
class MerchantHasStoresException(BusinessLogicException):
|
|
"""Raised when trying to delete a merchant that still has active stores."""
|
|
|
|
def __init__(self, merchant_id: int, store_count: int):
|
|
super().__init__(
|
|
message=f"Cannot delete merchant with ID '{merchant_id}' because it has {store_count} associated store(s)",
|
|
error_code="MERCHANT_HAS_STORES",
|
|
details={"merchant_id": merchant_id, "store_count": store_count},
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Admin User Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class UserNotFoundException(ResourceNotFoundException):
|
|
"""Raised when user is not found in admin operations."""
|
|
|
|
def __init__(self, user_identifier: str, identifier_type: str = "ID"):
|
|
if identifier_type.lower() == "username":
|
|
message = f"User with username '{user_identifier}' not found"
|
|
elif identifier_type.lower() == "email":
|
|
message = f"User with email '{user_identifier}' not found"
|
|
else:
|
|
message = f"User with ID '{user_identifier}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="User",
|
|
identifier=user_identifier,
|
|
message=message,
|
|
error_code="USER_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class UserStatusChangeException(BusinessLogicException):
|
|
"""Raised when user status cannot be changed."""
|
|
|
|
def __init__(
|
|
self,
|
|
user_id: int,
|
|
current_status: str,
|
|
attempted_action: str,
|
|
reason: str | None = None,
|
|
):
|
|
message = f"Cannot {attempted_action} user {user_id} (current status: {current_status})"
|
|
if reason:
|
|
message += f": {reason}"
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="USER_STATUS_CHANGE_FAILED",
|
|
details={
|
|
"user_id": user_id,
|
|
"current_status": current_status,
|
|
"attempted_action": attempted_action,
|
|
"reason": reason,
|
|
},
|
|
)
|
|
|
|
|
|
class AdminOperationException(BusinessLogicException):
|
|
"""Raised when admin operation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
operation: str,
|
|
reason: str,
|
|
target_type: str | None = None,
|
|
target_id: str | None = None,
|
|
):
|
|
message = f"Admin operation '{operation}' failed: {reason}"
|
|
|
|
details = {
|
|
"operation": operation,
|
|
"reason": reason,
|
|
}
|
|
|
|
if target_type:
|
|
details["target_type"] = target_type
|
|
if target_id:
|
|
details["target_id"] = target_id
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="ADMIN_OPERATION_FAILED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class CannotModifySelfException(BusinessLogicException):
|
|
"""Raised when admin tries to modify their own status."""
|
|
|
|
def __init__(self, user_id: int, operation: str):
|
|
super().__init__(
|
|
message=f"Cannot perform '{operation}' on your own account",
|
|
error_code="CANNOT_MODIFY_SELF",
|
|
details={
|
|
"user_id": user_id,
|
|
"operation": operation,
|
|
},
|
|
)
|
|
|
|
|
|
class BulkOperationException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when bulk admin operation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
operation: str,
|
|
total_items: int,
|
|
failed_items: int,
|
|
errors: dict[str, Any] | None = None,
|
|
):
|
|
message = f"Bulk {operation} completed with errors: {failed_items}/{total_items} failed"
|
|
|
|
details = {
|
|
"operation": operation,
|
|
"total_items": total_items,
|
|
"failed_items": failed_items,
|
|
"success_items": total_items - failed_items,
|
|
}
|
|
|
|
if errors:
|
|
details["errors"] = errors
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="BULK_OPERATION_PARTIAL_FAILURE",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class ConfirmationRequiredException(BusinessLogicException):
|
|
"""Raised when a destructive operation requires explicit confirmation."""
|
|
|
|
def __init__(
|
|
self,
|
|
operation: str,
|
|
message: str | None = None,
|
|
confirmation_param: str = "confirm",
|
|
):
|
|
if not message:
|
|
message = f"Operation '{operation}' requires confirmation parameter: {confirmation_param}=true"
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="CONFIRMATION_REQUIRED",
|
|
details={
|
|
"operation": operation,
|
|
"confirmation_param": confirmation_param,
|
|
},
|
|
)
|
|
|
|
|
|
class StoreVerificationException(BusinessLogicException):
|
|
"""Raised when store verification fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
store_id: int,
|
|
reason: str,
|
|
current_verification_status: bool | None = None,
|
|
):
|
|
details = {
|
|
"store_id": store_id,
|
|
"reason": reason,
|
|
}
|
|
|
|
if current_verification_status is not None:
|
|
details["current_verification_status"] = current_verification_status
|
|
|
|
super().__init__(
|
|
message=f"Store verification failed for store {store_id}: {reason}",
|
|
error_code="STORE_VERIFICATION_FAILED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class UserCannotBeDeletedException(BusinessLogicException):
|
|
"""Raised when a user cannot be deleted due to ownership constraints."""
|
|
|
|
def __init__(self, user_id: int, reason: str, owned_count: int = 0):
|
|
details = {
|
|
"user_id": user_id,
|
|
"reason": reason,
|
|
}
|
|
if owned_count > 0:
|
|
details["owned_merchants_count"] = owned_count
|
|
|
|
super().__init__(
|
|
message=f"Cannot delete user {user_id}: {reason}",
|
|
error_code="USER_CANNOT_BE_DELETED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class UserRoleChangeException(BusinessLogicException):
|
|
"""Raised when user role cannot be changed."""
|
|
|
|
def __init__(self, user_id: int, current_role: str, target_role: str, reason: str):
|
|
super().__init__(
|
|
message=f"Cannot change user {user_id} role from {current_role} to {target_role}: {reason}",
|
|
error_code="USER_ROLE_CHANGE_FAILED",
|
|
details={
|
|
"user_id": user_id,
|
|
"current_role": current_role,
|
|
"target_role": target_role,
|
|
"reason": reason,
|
|
},
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Team Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class TeamMemberNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a team member is not found."""
|
|
|
|
def __init__(self, user_id: int, store_id: int | None = None):
|
|
details = {"user_id": user_id}
|
|
if store_id:
|
|
details["store_id"] = store_id
|
|
message = f"Team member with user ID '{user_id}' not found in store {store_id}"
|
|
else:
|
|
message = f"Team member with user ID '{user_id}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="TeamMember",
|
|
identifier=str(user_id),
|
|
message=message,
|
|
error_code="TEAM_MEMBER_NOT_FOUND",
|
|
)
|
|
self.details.update(details)
|
|
|
|
|
|
class TeamMemberAlreadyExistsException(ConflictException):
|
|
"""Raised when trying to add a user who is already a team member."""
|
|
|
|
def __init__(self, user_id: int, store_id: int):
|
|
super().__init__(
|
|
message=f"User {user_id} is already a team member of store {store_id}",
|
|
error_code="TEAM_MEMBER_ALREADY_EXISTS",
|
|
details={
|
|
"user_id": user_id,
|
|
"store_id": store_id,
|
|
},
|
|
)
|
|
|
|
|
|
class TeamInvitationNotFoundException(ResourceNotFoundException): # noqa: MOD-025
|
|
"""Raised when a team invitation is not found."""
|
|
|
|
def __init__(self, invitation_token: str):
|
|
super().__init__(
|
|
resource_type="TeamInvitation",
|
|
identifier=invitation_token,
|
|
message=f"Team invitation with token '{invitation_token}' not found or expired",
|
|
error_code="TEAM_INVITATION_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class TeamInvitationExpiredException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when trying to accept an expired invitation."""
|
|
|
|
def __init__(self, invitation_token: str):
|
|
super().__init__(
|
|
message="Team invitation has expired",
|
|
error_code="TEAM_INVITATION_EXPIRED",
|
|
details={"invitation_token": invitation_token},
|
|
)
|
|
|
|
|
|
class TeamInvitationAlreadyAcceptedException(ConflictException):
|
|
"""Raised when trying to accept an already accepted invitation."""
|
|
|
|
def __init__(self, invitation_token: str):
|
|
super().__init__(
|
|
message="Team invitation has already been accepted",
|
|
error_code="TEAM_INVITATION_ALREADY_ACCEPTED",
|
|
details={"invitation_token": invitation_token},
|
|
)
|
|
|
|
|
|
class UnauthorizedTeamActionException(AuthorizationException): # noqa: MOD-025
|
|
"""Raised when user tries to perform team action without permission."""
|
|
|
|
def __init__(
|
|
self,
|
|
action: str,
|
|
user_id: int | None = None,
|
|
required_permission: str | None = None,
|
|
):
|
|
details = {"action": action}
|
|
if user_id:
|
|
details["user_id"] = user_id
|
|
if required_permission:
|
|
details["required_permission"] = required_permission
|
|
|
|
super().__init__(
|
|
message=f"Unauthorized to perform action: {action}",
|
|
error_code="UNAUTHORIZED_TEAM_ACTION",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class CannotRemoveOwnerException(BusinessLogicException):
|
|
"""Raised when trying to remove the store owner from team."""
|
|
|
|
def __init__(self, user_id: int, store_id: int):
|
|
super().__init__(
|
|
message="Cannot remove store owner from team",
|
|
error_code="CANNOT_REMOVE_OWNER",
|
|
details={
|
|
"user_id": user_id,
|
|
"store_id": store_id,
|
|
},
|
|
)
|
|
|
|
|
|
class CannotModifyOwnRoleException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when user tries to modify their own role."""
|
|
|
|
def __init__(self, user_id: int):
|
|
super().__init__(
|
|
message="Cannot modify your own role",
|
|
error_code="CANNOT_MODIFY_OWN_ROLE",
|
|
details={"user_id": user_id},
|
|
)
|
|
|
|
|
|
class RoleNotFoundException(ResourceNotFoundException): # noqa: MOD-025
|
|
"""Raised when a role is not found."""
|
|
|
|
def __init__(self, role_id: int, store_id: int | None = None):
|
|
details = {"role_id": role_id}
|
|
if store_id:
|
|
details["store_id"] = store_id
|
|
message = f"Role with ID '{role_id}' not found in store {store_id}"
|
|
else:
|
|
message = f"Role with ID '{role_id}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="Role",
|
|
identifier=str(role_id),
|
|
message=message,
|
|
error_code="ROLE_NOT_FOUND",
|
|
)
|
|
self.details.update(details)
|
|
|
|
|
|
class InvalidRoleException(ValidationException): # noqa: MOD-025
|
|
"""Raised when role data is invalid."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid role data",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_ROLE_DATA"
|
|
|
|
|
|
class InsufficientTeamPermissionsException(AuthorizationException): # noqa: MOD-025
|
|
"""Raised when user lacks required team permissions for an action."""
|
|
|
|
def __init__(
|
|
self,
|
|
required_permission: str,
|
|
user_id: int | None = None,
|
|
action: str | None = None,
|
|
):
|
|
details = {"required_permission": required_permission}
|
|
if user_id:
|
|
details["user_id"] = user_id
|
|
if action:
|
|
details["action"] = action
|
|
|
|
message = f"Insufficient team permissions. Required: {required_permission}"
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="INSUFFICIENT_TEAM_PERMISSIONS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class MaxTeamMembersReachedException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when store has reached maximum team members limit."""
|
|
|
|
def __init__(self, max_members: int, store_id: int):
|
|
super().__init__(
|
|
message=f"Maximum number of team members reached ({max_members})",
|
|
error_code="MAX_TEAM_MEMBERS_REACHED",
|
|
details={
|
|
"max_members": max_members,
|
|
"store_id": store_id,
|
|
},
|
|
)
|
|
|
|
|
|
class TeamValidationException(ValidationException):
|
|
"""Raised when team operation validation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Team operation validation failed",
|
|
field: str | None = None,
|
|
validation_errors: dict[str, str] | None = None,
|
|
):
|
|
details = {}
|
|
if validation_errors:
|
|
details["validation_errors"] = validation_errors
|
|
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "TEAM_VALIDATION_FAILED"
|
|
|
|
|
|
class InvalidInvitationDataException(ValidationException): # noqa: MOD-025
|
|
"""Raised when team invitation data is invalid."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid invitation data",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_INVITATION_DATA"
|
|
|
|
|
|
class InvalidInvitationTokenException(ValidationException):
|
|
"""Raised when invitation token is invalid, expired, or already used."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid or expired invitation token",
|
|
invitation_token: str | None = None,
|
|
):
|
|
details = {}
|
|
if invitation_token:
|
|
details["invitation_token"] = invitation_token
|
|
|
|
super().__init__(
|
|
message=message,
|
|
field="invitation_token",
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_INVITATION_TOKEN"
|
|
|
|
|
|
# =============================================================================
|
|
# Store Domain Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
# =============================================================================
|
|
# Merchant Domain Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class MerchantDomainNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a merchant domain is not found."""
|
|
|
|
def __init__(self, domain_identifier: str, identifier_type: str = "ID"):
|
|
if identifier_type.lower() == "domain":
|
|
message = f"Merchant domain '{domain_identifier}' not found"
|
|
else:
|
|
message = f"Merchant domain with ID '{domain_identifier}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="MerchantDomain",
|
|
identifier=domain_identifier,
|
|
message=message,
|
|
error_code="MERCHANT_DOMAIN_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class MerchantDomainAlreadyExistsException(ConflictException):
|
|
"""Raised when trying to add a domain that already exists."""
|
|
|
|
def __init__(self, domain: str, existing_merchant_id: int | None = None):
|
|
details = {"domain": domain}
|
|
if existing_merchant_id:
|
|
details["existing_merchant_id"] = existing_merchant_id
|
|
|
|
super().__init__(
|
|
message=f"Domain '{domain}' is already registered",
|
|
error_code="MERCHANT_DOMAIN_ALREADY_EXISTS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class StoreDomainNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a store domain is not found."""
|
|
|
|
def __init__(self, domain_identifier: str, identifier_type: str = "ID"):
|
|
if identifier_type.lower() == "domain":
|
|
message = f"Domain '{domain_identifier}' not found"
|
|
else:
|
|
message = f"Domain with ID '{domain_identifier}' not found"
|
|
|
|
super().__init__(
|
|
resource_type="StoreDomain",
|
|
identifier=domain_identifier,
|
|
message=message,
|
|
error_code="STORE_DOMAIN_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class StoreDomainAlreadyExistsException(ConflictException):
|
|
"""Raised when trying to add a domain that already exists."""
|
|
|
|
def __init__(self, domain: str, existing_store_id: int | None = None):
|
|
details = {"domain": domain}
|
|
if existing_store_id:
|
|
details["existing_store_id"] = existing_store_id
|
|
|
|
super().__init__(
|
|
message=f"Domain '{domain}' is already registered",
|
|
error_code="STORE_DOMAIN_ALREADY_EXISTS",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class InvalidDomainFormatException(ValidationException): # noqa: MOD-025
|
|
"""Raised when domain format is invalid."""
|
|
|
|
def __init__(self, domain: str, reason: str = "Invalid domain format"):
|
|
super().__init__(
|
|
message=f"{reason}: {domain}",
|
|
field="domain",
|
|
details={"domain": domain, "reason": reason},
|
|
)
|
|
self.error_code = "INVALID_DOMAIN_FORMAT"
|
|
|
|
|
|
class ReservedDomainException(ValidationException):
|
|
"""Raised when trying to use a reserved domain."""
|
|
|
|
def __init__(self, domain: str, reserved_part: str):
|
|
super().__init__(
|
|
message=f"Domain cannot use reserved subdomain: {reserved_part}",
|
|
field="domain",
|
|
details={"domain": domain, "reserved_part": reserved_part},
|
|
)
|
|
self.error_code = "RESERVED_DOMAIN"
|
|
|
|
|
|
class DomainNotVerifiedException(BusinessLogicException):
|
|
"""Raised when trying to activate an unverified domain."""
|
|
|
|
def __init__(self, domain_id: int, domain: str):
|
|
super().__init__(
|
|
message=f"Domain '{domain}' must be verified before activation",
|
|
error_code="DOMAIN_NOT_VERIFIED",
|
|
details={"domain_id": domain_id, "domain": domain},
|
|
)
|
|
|
|
|
|
class DomainVerificationFailedException(BusinessLogicException):
|
|
"""Raised when domain verification fails."""
|
|
|
|
def __init__(self, domain: str, reason: str):
|
|
super().__init__(
|
|
message=f"Domain verification failed for '{domain}': {reason}",
|
|
error_code="DOMAIN_VERIFICATION_FAILED",
|
|
details={"domain": domain, "reason": reason},
|
|
)
|
|
|
|
|
|
class DomainAlreadyVerifiedException(BusinessLogicException):
|
|
"""Raised when trying to verify an already verified domain."""
|
|
|
|
def __init__(self, domain_id: int, domain: str):
|
|
super().__init__(
|
|
message=f"Domain '{domain}' is already verified",
|
|
error_code="DOMAIN_ALREADY_VERIFIED",
|
|
details={"domain_id": domain_id, "domain": domain},
|
|
)
|
|
|
|
|
|
class MultiplePrimaryDomainsException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when trying to set multiple primary domains."""
|
|
|
|
def __init__(self, store_id: int):
|
|
super().__init__(
|
|
message="Store can only have one primary domain",
|
|
error_code="MULTIPLE_PRIMARY_DOMAINS",
|
|
details={"store_id": store_id},
|
|
)
|
|
|
|
|
|
class DNSVerificationException(ExternalServiceException):
|
|
"""Raised when DNS verification service fails."""
|
|
|
|
def __init__(self, domain: str, reason: str):
|
|
super().__init__(
|
|
service_name="DNS",
|
|
message=f"DNS verification failed for '{domain}': {reason}",
|
|
error_code="DNS_VERIFICATION_ERROR",
|
|
details={"domain": domain, "reason": reason},
|
|
)
|
|
|
|
|
|
class MaxDomainsReachedException(BusinessLogicException):
|
|
"""Raised when store tries to add more domains than allowed."""
|
|
|
|
def __init__(self, store_id: int, max_domains: int):
|
|
super().__init__(
|
|
message=f"Maximum number of domains reached ({max_domains})",
|
|
error_code="MAX_DOMAINS_REACHED",
|
|
details={"store_id": store_id, "max_domains": max_domains},
|
|
)
|
|
|
|
|
|
class UnauthorizedDomainAccessException(BusinessLogicException): # noqa: MOD-025
|
|
"""Raised when trying to access domain that doesn't belong to store."""
|
|
|
|
def __init__(self, domain_id: int, store_id: int):
|
|
super().__init__(
|
|
message=f"Unauthorized access to domain {domain_id}",
|
|
error_code="UNAUTHORIZED_DOMAIN_ACCESS",
|
|
details={"domain_id": domain_id, "store_id": store_id},
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
# Auth
|
|
"InvalidCredentialsException",
|
|
"TokenExpiredException",
|
|
"InvalidTokenException",
|
|
"InsufficientPermissionsException",
|
|
"UserNotActiveException",
|
|
"AdminRequiredException",
|
|
"UserAlreadyExistsException",
|
|
# Platform
|
|
"PlatformNotFoundException",
|
|
"PlatformInactiveException",
|
|
"PlatformUpdateException",
|
|
# Store
|
|
"StoreNotFoundException",
|
|
"StoreAlreadyExistsException",
|
|
"StoreNotActiveException",
|
|
"StoreNotVerifiedException",
|
|
"UnauthorizedStoreAccessException",
|
|
"InvalidStoreDataException",
|
|
"StoreValidationException",
|
|
"MaxStoresReachedException",
|
|
"StoreAccessDeniedException",
|
|
"StoreOwnerOnlyException",
|
|
"InsufficientStorePermissionsException",
|
|
# Merchant
|
|
"MerchantNotFoundException",
|
|
"MerchantAlreadyExistsException",
|
|
"MerchantNotActiveException",
|
|
"MerchantNotVerifiedException",
|
|
"UnauthorizedMerchantAccessException",
|
|
"InvalidMerchantDataException",
|
|
"MerchantValidationException",
|
|
"MerchantHasStoresException",
|
|
# Admin User
|
|
"UserNotFoundException",
|
|
"UserStatusChangeException",
|
|
"AdminOperationException",
|
|
"CannotModifySelfException",
|
|
"BulkOperationException",
|
|
"ConfirmationRequiredException",
|
|
"StoreVerificationException",
|
|
"UserCannotBeDeletedException",
|
|
"UserRoleChangeException",
|
|
# Team
|
|
"TeamMemberNotFoundException",
|
|
"TeamMemberAlreadyExistsException",
|
|
"TeamInvitationNotFoundException",
|
|
"TeamInvitationExpiredException",
|
|
"TeamInvitationAlreadyAcceptedException",
|
|
"UnauthorizedTeamActionException",
|
|
"CannotRemoveOwnerException",
|
|
"CannotModifyOwnRoleException",
|
|
"RoleNotFoundException",
|
|
"InvalidRoleException",
|
|
"InsufficientTeamPermissionsException",
|
|
"MaxTeamMembersReachedException",
|
|
"TeamValidationException",
|
|
"InvalidInvitationDataException",
|
|
"InvalidInvitationTokenException",
|
|
# Merchant Domain
|
|
"MerchantDomainNotFoundException",
|
|
"MerchantDomainAlreadyExistsException",
|
|
# Store Domain
|
|
"StoreDomainNotFoundException",
|
|
"StoreDomainAlreadyExistsException",
|
|
"InvalidDomainFormatException",
|
|
"ReservedDomainException",
|
|
"DomainNotVerifiedException",
|
|
"DomainVerificationFailedException",
|
|
"DomainAlreadyVerifiedException",
|
|
"MultiplePrimaryDomainsException",
|
|
"DNSVerificationException",
|
|
"MaxDomainsReachedException",
|
|
"UnauthorizedDomainAccessException",
|
|
]
|