Files
orion/app/modules/marketplace/exceptions.py
Samir Boulahtit 4e28d91a78 refactor: migrate templates and static files to self-contained modules
Templates Migration:
- Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.)
- Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.)
- Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms)
- Migrate public templates to modules (billing, marketplace, cms)
- Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/)
- Migrate letzshop partials to marketplace module

Static Files Migration:
- Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file)
- Migrate vendor JS to modules: tenancy (4 files), core (2 files)
- Migrate shared JS: vendor-selector.js to core, media-picker.js to cms
- Migrate storefront JS: storefront-layout.js to core
- Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/)
- Update all template references to use module_static paths

Naming Consistency:
- Rename static/platform/ to static/public/
- Rename app/templates/platform/ to app/templates/public/
- Update all extends and static references

Documentation:
- Update module-system.md with shared templates documentation
- Update frontend-structure.md with new module JS organization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 14:34:16 +01:00

596 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
"VendorNotFoundException",
"ProductNotFoundException",
"MarketplaceProductNotFoundException",
"MarketplaceProductAlreadyExistsException",
"InvalidMarketplaceProductDataException",
"MarketplaceProductValidationException",
"InvalidGTINException",
"MarketplaceProductCSVImportException",
# Export/Sync exceptions
"ExportError",
"SyncError",
# Onboarding exceptions
"OnboardingNotFoundException",
"OnboardingStepOrderException",
"OnboardingAlreadyCompletedException",
"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):
"""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):
"""Raised when Letzshop credentials not found for vendor."""
def __init__(self, vendor_id: int):
super().__init__(
resource_type="LetzshopCredentials",
identifier=str(vendor_id),
error_code="LETZSHOP_CREDENTIALS_NOT_FOUND",
)
self.vendor_id = vendor_id
class LetzshopConnectionFailedException(BusinessLogicException):
"""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):
"""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):
"""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):
"""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):
"""Raised when trying to start import while another is already processing."""
def __init__(self, vendor_code: str, existing_job_id: int):
super().__init__(
message=f"Import already in progress for vendor '{vendor_code}'",
error_code="IMPORT_JOB_ALREADY_PROCESSING",
details={
"vendor_code": vendor_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):
"""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):
"""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):
"""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):
"""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):
"""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):
"""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 VendorNotFoundException(ResourceNotFoundException):
"""Raised when a vendor is not found."""
def __init__(self, vendor_id: int):
super().__init__(
resource_type="Vendor",
identifier=str(vendor_id),
error_code="VENDOR_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):
"""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):
"""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):
"""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 vendor directory sync fails."""
def __init__(self, message: str, vendor_code: str | None = None):
details = {}
if vendor_code:
details["vendor_code"] = vendor_code
super().__init__(
message=message,
error_code="SYNC_ERROR",
details=details if details else None,
)
self.vendor_code = vendor_code
# =============================================================================
# Onboarding Exceptions
# =============================================================================
class OnboardingNotFoundException(ResourceNotFoundException):
"""Raised when onboarding record is not found for a vendor."""
def __init__(self, vendor_id: int):
super().__init__(
resource_type="VendorOnboarding",
identifier=str(vendor_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 OnboardingAlreadyCompletedException(BusinessLogicException):
"""Raised when trying to modify a completed onboarding."""
def __init__(self, vendor_id: int):
super().__init__(
message="Onboarding has already been completed",
error_code="ONBOARDING_ALREADY_COMPLETED",
details={"vendor_id": vendor_id},
)
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},
)