- API-004: Add noqa for factory-pattern auth in user_account routes and payments admin - MDL-003: Add from_attributes to MerchantStoreDetailResponse schema - EXC-003: Suppress broad except in merchant_store_service and admin_subscription_service (intentional fallbacks for optional billing module) - NAM-002: Rename onboarding files to *_service.py suffix and update all imports - JS-001: Add file-level noqa for dev-toolbar.js (console interceptor by design) - JS-005: Add init guards to dashboard.js and customer-detail.js - IMPORT-004: Break circular deps by removing orders from inventory requires and marketplace from orders requires; add IMPORT-002 suppression for lazy cross-imports - MOD-025: Remove unused OnboardingAlreadyCompletedException Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
584 lines
18 KiB
Python
584 lines
18 KiB
Python
# app/modules/marketplace/exceptions.py
|
|
"""
|
|
Marketplace module exceptions.
|
|
|
|
This module provides exception classes for marketplace operations including:
|
|
- Letzshop integration (client, authentication, credentials)
|
|
- Product import/export
|
|
- Sync operations
|
|
- Onboarding wizard
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from app.exceptions.base import (
|
|
AuthorizationException,
|
|
BusinessLogicException,
|
|
ConflictException,
|
|
ExternalServiceException,
|
|
ResourceNotFoundException,
|
|
ValidationException,
|
|
)
|
|
|
|
__all__ = [
|
|
# Base exception
|
|
"MarketplaceException",
|
|
# Letzshop client exceptions
|
|
"LetzshopClientError",
|
|
"LetzshopAuthenticationError",
|
|
"LetzshopCredentialsNotFoundException",
|
|
"LetzshopConnectionFailedException",
|
|
# Import job exceptions
|
|
"ImportJobNotFoundException",
|
|
"HistoricalImportJobNotFoundException",
|
|
"ImportJobNotOwnedException",
|
|
"ImportJobCannotBeCancelledException",
|
|
"ImportJobCannotBeDeletedException",
|
|
"ImportJobAlreadyProcessingException",
|
|
"ImportValidationError",
|
|
"InvalidImportDataException",
|
|
"ImportRateLimitException",
|
|
# Marketplace exceptions
|
|
"MarketplaceImportException",
|
|
"MarketplaceConnectionException",
|
|
"MarketplaceDataParsingException",
|
|
"InvalidMarketplaceException",
|
|
# Product exceptions
|
|
"StoreNotFoundException",
|
|
"ProductNotFoundException",
|
|
"MarketplaceProductNotFoundException",
|
|
"MarketplaceProductAlreadyExistsException",
|
|
"InvalidMarketplaceProductDataException",
|
|
"MarketplaceProductValidationException",
|
|
"InvalidGTINException",
|
|
"MarketplaceProductCSVImportException",
|
|
# Export/Sync exceptions
|
|
"ExportError",
|
|
"SyncError",
|
|
# Onboarding exceptions
|
|
"OnboardingNotFoundException",
|
|
"OnboardingStepOrderException",
|
|
"OnboardingCsvUrlRequiredException",
|
|
"OnboardingSyncJobNotFoundException",
|
|
"OnboardingSyncNotCompleteException",
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# Base Marketplace Exception
|
|
# =============================================================================
|
|
|
|
|
|
class MarketplaceException(BusinessLogicException):
|
|
"""Base exception for marketplace module errors."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
error_code: str = "MARKETPLACE_ERROR",
|
|
details: dict | None = None,
|
|
):
|
|
super().__init__(message=message, error_code=error_code, details=details)
|
|
|
|
|
|
# =============================================================================
|
|
# Letzshop Client Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class LetzshopClientError(MarketplaceException):
|
|
"""Raised when Letzshop API call fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
status_code: int | None = None,
|
|
response: str | None = None,
|
|
):
|
|
details = {}
|
|
if status_code:
|
|
details["http_status_code"] = status_code
|
|
if response:
|
|
details["response"] = response
|
|
super().__init__(
|
|
message=message,
|
|
error_code="LETZSHOP_CLIENT_ERROR",
|
|
details=details if details else None,
|
|
)
|
|
self.status_code = status_code
|
|
self.response = response
|
|
|
|
|
|
class LetzshopAuthenticationError(LetzshopClientError): # noqa: MOD025
|
|
"""Raised when Letzshop authentication fails."""
|
|
|
|
def __init__(self, message: str = "Letzshop authentication failed"):
|
|
super().__init__(message, status_code=401)
|
|
self.error_code = "LETZSHOP_AUTHENTICATION_FAILED"
|
|
|
|
|
|
class LetzshopCredentialsNotFoundException(ResourceNotFoundException): # noqa: MOD025
|
|
"""Raised when Letzshop credentials not found for store."""
|
|
|
|
def __init__(self, store_id: int):
|
|
super().__init__(
|
|
resource_type="LetzshopCredentials",
|
|
identifier=str(store_id),
|
|
error_code="LETZSHOP_CREDENTIALS_NOT_FOUND",
|
|
)
|
|
self.store_id = store_id
|
|
|
|
|
|
class LetzshopConnectionFailedException(BusinessLogicException): # noqa: MOD025
|
|
"""Raised when Letzshop API connection test fails."""
|
|
|
|
def __init__(self, error_message: str):
|
|
super().__init__(
|
|
message=f"Letzshop connection failed: {error_message}",
|
|
error_code="LETZSHOP_CONNECTION_FAILED",
|
|
details={"error": error_message},
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Import Job Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class ImportJobNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a marketplace import job is not found."""
|
|
|
|
def __init__(self, job_id: int):
|
|
super().__init__(
|
|
resource_type="MarketplaceImportJob",
|
|
identifier=str(job_id),
|
|
message=f"Import job with ID '{job_id}' not found",
|
|
error_code="IMPORT_JOB_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class HistoricalImportJobNotFoundException(ResourceNotFoundException): # noqa: MOD025
|
|
"""Raised when a historical import job is not found."""
|
|
|
|
def __init__(self, job_id: int):
|
|
super().__init__(
|
|
resource_type="LetzshopHistoricalImportJob",
|
|
identifier=str(job_id),
|
|
error_code="HISTORICAL_IMPORT_JOB_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class ImportJobNotOwnedException(AuthorizationException):
|
|
"""Raised when user tries to access import job they don't own."""
|
|
|
|
def __init__(self, job_id: int, user_id: int | None = None):
|
|
details = {"job_id": job_id}
|
|
if user_id:
|
|
details["user_id"] = user_id
|
|
|
|
super().__init__(
|
|
message=f"Unauthorized access to import job '{job_id}'",
|
|
error_code="IMPORT_JOB_NOT_OWNED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class ImportJobCannotBeCancelledException(BusinessLogicException): # noqa: MOD025
|
|
"""Raised when trying to cancel job that cannot be cancelled."""
|
|
|
|
def __init__(self, job_id: int, current_status: str):
|
|
super().__init__(
|
|
message=f"Import job '{job_id}' cannot be cancelled (current status: {current_status})",
|
|
error_code="IMPORT_JOB_CANNOT_BE_CANCELLED",
|
|
details={
|
|
"job_id": job_id,
|
|
"current_status": current_status,
|
|
},
|
|
)
|
|
|
|
|
|
class ImportJobCannotBeDeletedException(BusinessLogicException): # noqa: MOD025
|
|
"""Raised when trying to delete job that cannot be deleted."""
|
|
|
|
def __init__(self, job_id: int, current_status: str):
|
|
super().__init__(
|
|
message=f"Import job '{job_id}' cannot be deleted (current status: {current_status})",
|
|
error_code="IMPORT_JOB_CANNOT_BE_DELETED",
|
|
details={
|
|
"job_id": job_id,
|
|
"current_status": current_status,
|
|
},
|
|
)
|
|
|
|
|
|
class ImportJobAlreadyProcessingException(BusinessLogicException): # noqa: MOD025
|
|
"""Raised when trying to start import while another is already processing."""
|
|
|
|
def __init__(self, store_code: str, existing_job_id: int):
|
|
super().__init__(
|
|
message=f"Import already in progress for store '{store_code}'",
|
|
error_code="IMPORT_JOB_ALREADY_PROCESSING",
|
|
details={
|
|
"store_code": store_code,
|
|
"existing_job_id": existing_job_id,
|
|
},
|
|
)
|
|
|
|
|
|
class ImportValidationError(MarketplaceException):
|
|
"""Raised when import data validation fails."""
|
|
|
|
def __init__(self, message: str, errors: list[dict] | None = None):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="IMPORT_VALIDATION_ERROR",
|
|
details={"errors": errors} if errors else None,
|
|
)
|
|
self.errors = errors or []
|
|
|
|
|
|
class InvalidImportDataException(ValidationException): # noqa: MOD025
|
|
"""Raised when import data is invalid."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid import data",
|
|
field: str | None = None,
|
|
row_number: int | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
if not details:
|
|
details = {}
|
|
|
|
if row_number:
|
|
details["row_number"] = row_number
|
|
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_IMPORT_DATA"
|
|
|
|
|
|
class ImportRateLimitException(BusinessLogicException): # noqa: MOD025
|
|
"""Raised when import rate limit is exceeded."""
|
|
|
|
def __init__(
|
|
self,
|
|
max_imports: int,
|
|
time_window: str,
|
|
retry_after: int | None = None,
|
|
):
|
|
details = {
|
|
"max_imports": max_imports,
|
|
"time_window": time_window,
|
|
}
|
|
|
|
if retry_after:
|
|
details["retry_after"] = retry_after
|
|
|
|
super().__init__(
|
|
message=f"Import rate limit exceeded: {max_imports} imports per {time_window}",
|
|
error_code="IMPORT_RATE_LIMIT_EXCEEDED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Marketplace Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class MarketplaceImportException(BusinessLogicException): # noqa: MOD025
|
|
"""Base exception for marketplace import operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
error_code: str = "MARKETPLACE_IMPORT_ERROR",
|
|
marketplace: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
if not details:
|
|
details = {}
|
|
|
|
if marketplace:
|
|
details["marketplace"] = marketplace
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code=error_code,
|
|
details=details,
|
|
)
|
|
|
|
|
|
class MarketplaceConnectionException(ExternalServiceException): # noqa: MOD025
|
|
"""Raised when marketplace connection fails."""
|
|
|
|
def __init__(
|
|
self, marketplace: str, message: str = "Failed to connect to marketplace"
|
|
):
|
|
super().__init__(
|
|
service_name=marketplace,
|
|
message=f"{message}: {marketplace}",
|
|
error_code="MARKETPLACE_CONNECTION_FAILED",
|
|
)
|
|
|
|
|
|
class MarketplaceDataParsingException(ValidationException): # noqa: MOD025
|
|
"""Raised when marketplace data cannot be parsed."""
|
|
|
|
def __init__(
|
|
self,
|
|
marketplace: str,
|
|
message: str = "Failed to parse marketplace data",
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
if not details:
|
|
details = {}
|
|
details["marketplace"] = marketplace
|
|
|
|
super().__init__(
|
|
message=f"{message} from {marketplace}",
|
|
details=details,
|
|
)
|
|
self.error_code = "MARKETPLACE_DATA_PARSING_FAILED"
|
|
|
|
|
|
class InvalidMarketplaceException(ValidationException): # noqa: MOD025
|
|
"""Raised when marketplace is not supported."""
|
|
|
|
def __init__(self, marketplace: str, supported_marketplaces: list | None = None):
|
|
details = {"marketplace": marketplace}
|
|
if supported_marketplaces:
|
|
details["supported_marketplaces"] = supported_marketplaces
|
|
|
|
super().__init__(
|
|
message=f"Unsupported marketplace: {marketplace}",
|
|
field="marketplace",
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_MARKETPLACE"
|
|
|
|
|
|
# =============================================================================
|
|
# Product Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class StoreNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a store is not found."""
|
|
|
|
def __init__(self, store_id: int):
|
|
super().__init__(
|
|
resource_type="Store",
|
|
identifier=str(store_id),
|
|
error_code="STORE_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class ProductNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a marketplace product is not found."""
|
|
|
|
def __init__(self, product_id: str | int):
|
|
super().__init__(
|
|
resource_type="MarketplaceProduct",
|
|
identifier=str(product_id),
|
|
error_code="MARKETPLACE_PRODUCT_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class MarketplaceProductNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a product is not found."""
|
|
|
|
def __init__(self, marketplace_product_id: str):
|
|
super().__init__(
|
|
resource_type="MarketplaceProduct",
|
|
identifier=marketplace_product_id,
|
|
message=f"MarketplaceProduct with ID '{marketplace_product_id}' not found",
|
|
error_code="PRODUCT_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class MarketplaceProductAlreadyExistsException(ConflictException):
|
|
"""Raised when trying to create a product that already exists."""
|
|
|
|
def __init__(self, marketplace_product_id: str):
|
|
super().__init__(
|
|
message=f"MarketplaceProduct with ID '{marketplace_product_id}' already exists",
|
|
error_code="PRODUCT_ALREADY_EXISTS",
|
|
details={"marketplace_product_id": marketplace_product_id},
|
|
)
|
|
|
|
|
|
class InvalidMarketplaceProductDataException(ValidationException):
|
|
"""Raised when product data is invalid."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid product data",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_PRODUCT_DATA"
|
|
|
|
|
|
class MarketplaceProductValidationException(ValidationException):
|
|
"""Raised when product validation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
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 = "PRODUCT_VALIDATION_FAILED"
|
|
|
|
|
|
class InvalidGTINException(ValidationException): # noqa: MOD025
|
|
"""Raised when GTIN format is invalid."""
|
|
|
|
def __init__(self, gtin: str, message: str = "Invalid GTIN format"):
|
|
super().__init__(
|
|
message=f"{message}: {gtin}",
|
|
field="gtin",
|
|
details={"gtin": gtin},
|
|
)
|
|
self.error_code = "INVALID_GTIN"
|
|
|
|
|
|
class MarketplaceProductCSVImportException(BusinessLogicException): # noqa: MOD025
|
|
"""Raised when product CSV import fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "MarketplaceProduct CSV import failed",
|
|
row_number: int | None = None,
|
|
errors: dict[str, Any] | None = None,
|
|
):
|
|
details = {}
|
|
if row_number:
|
|
details["row_number"] = row_number
|
|
if errors:
|
|
details["errors"] = errors
|
|
|
|
super().__init__(
|
|
message=message,
|
|
error_code="PRODUCT_CSV_IMPORT_FAILED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Export/Sync Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class ExportError(MarketplaceException): # noqa: MOD025
|
|
"""Raised when product export fails."""
|
|
|
|
def __init__(self, message: str, language: str | None = None):
|
|
details = {}
|
|
if language:
|
|
details["language"] = language
|
|
super().__init__(
|
|
message=message,
|
|
error_code="EXPORT_ERROR",
|
|
details=details if details else None,
|
|
)
|
|
self.language = language
|
|
|
|
|
|
class SyncError(MarketplaceException):
|
|
"""Raised when store directory sync fails."""
|
|
|
|
def __init__(self, message: str, store_code: str | None = None):
|
|
details = {}
|
|
if store_code:
|
|
details["store_code"] = store_code
|
|
super().__init__(
|
|
message=message,
|
|
error_code="SYNC_ERROR",
|
|
details=details if details else None,
|
|
)
|
|
self.store_code = store_code
|
|
|
|
|
|
# =============================================================================
|
|
# Onboarding Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class OnboardingNotFoundException(ResourceNotFoundException):
|
|
"""Raised when onboarding record is not found for a store."""
|
|
|
|
def __init__(self, store_id: int):
|
|
super().__init__(
|
|
resource_type="StoreOnboarding",
|
|
identifier=str(store_id),
|
|
error_code="ONBOARDING_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class OnboardingStepOrderException(ValidationException):
|
|
"""Raised when trying to access a step out of order."""
|
|
|
|
def __init__(self, current_step: str, required_step: str):
|
|
super().__init__(
|
|
message=f"Please complete the {required_step} step first",
|
|
field="step",
|
|
details={
|
|
"current_step": current_step,
|
|
"required_step": required_step,
|
|
},
|
|
)
|
|
self.error_code = "ONBOARDING_STEP_ORDER_ERROR"
|
|
|
|
|
|
class OnboardingCsvUrlRequiredException(ValidationException):
|
|
"""Raised when no CSV URL is provided in product import step."""
|
|
|
|
def __init__(self):
|
|
super().__init__(
|
|
message="At least one CSV URL must be provided",
|
|
field="csv_url",
|
|
)
|
|
self.error_code = "ONBOARDING_CSV_URL_REQUIRED"
|
|
|
|
|
|
class OnboardingSyncJobNotFoundException(ResourceNotFoundException):
|
|
"""Raised when sync job is not found."""
|
|
|
|
def __init__(self, job_id: int):
|
|
super().__init__(
|
|
resource_type="LetzshopHistoricalImportJob",
|
|
identifier=str(job_id),
|
|
error_code="ONBOARDING_SYNC_JOB_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class OnboardingSyncNotCompleteException(BusinessLogicException):
|
|
"""Raised when trying to complete onboarding before sync is done."""
|
|
|
|
def __init__(self, job_status: str):
|
|
super().__init__(
|
|
message=f"Import job is still {job_status}, please wait",
|
|
error_code="SYNC_NOT_COMPLETE",
|
|
details={"job_status": job_status},
|
|
)
|