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>
This commit is contained in:
@@ -2,23 +2,110 @@
|
||||
"""
|
||||
Marketplace module exceptions.
|
||||
|
||||
Custom exceptions for Letzshop integration, product import/export, and sync operations.
|
||||
This module provides exception classes for marketplace operations including:
|
||||
- Letzshop integration (client, authentication, credentials)
|
||||
- Product import/export
|
||||
- Sync operations
|
||||
- Onboarding wizard
|
||||
"""
|
||||
|
||||
from app.exceptions import BusinessLogicException, ResourceNotFoundException
|
||||
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."""
|
||||
|
||||
pass
|
||||
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):
|
||||
super().__init__(message)
|
||||
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
|
||||
|
||||
@@ -28,57 +115,393 @@ class LetzshopAuthenticationError(LetzshopClientError):
|
||||
|
||||
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__("LetzshopCredentials", str(vendor_id))
|
||||
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__("MarketplaceImportJob", str(job_id))
|
||||
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__("LetzshopHistoricalImportJob", str(job_id))
|
||||
super().__init__(
|
||||
resource_type="LetzshopHistoricalImportJob",
|
||||
identifier=str(job_id),
|
||||
error_code="HISTORICAL_IMPORT_JOB_NOT_FOUND",
|
||||
)
|
||||
|
||||
|
||||
class VendorNotFoundException(ResourceNotFoundException):
|
||||
"""Raised when a vendor is not found."""
|
||||
class ImportJobNotOwnedException(AuthorizationException):
|
||||
"""Raised when user tries to access import job they don't own."""
|
||||
|
||||
def __init__(self, vendor_id: int):
|
||||
super().__init__("Vendor", str(vendor_id))
|
||||
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 ProductNotFoundException(ResourceNotFoundException):
|
||||
"""Raised when a marketplace product is not found."""
|
||||
class ImportJobCannotBeCancelledException(BusinessLogicException):
|
||||
"""Raised when trying to cancel job that cannot be cancelled."""
|
||||
|
||||
def __init__(self, product_id: str | int):
|
||||
super().__init__("MarketplaceProduct", str(product_id))
|
||||
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)
|
||||
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):
|
||||
super().__init__(message)
|
||||
details = {}
|
||||
if language:
|
||||
details["language"] = language
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="EXPORT_ERROR",
|
||||
details=details if details else None,
|
||||
)
|
||||
self.language = language
|
||||
|
||||
|
||||
@@ -86,20 +509,87 @@ class SyncError(MarketplaceException):
|
||||
"""Raised when vendor directory sync fails."""
|
||||
|
||||
def __init__(self, message: str, vendor_code: str | None = None):
|
||||
super().__init__(message)
|
||||
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
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MarketplaceException",
|
||||
"LetzshopClientError",
|
||||
"LetzshopAuthenticationError",
|
||||
"LetzshopCredentialsNotFoundException",
|
||||
"ImportJobNotFoundException",
|
||||
"HistoricalImportJobNotFoundException",
|
||||
"VendorNotFoundException",
|
||||
"ProductNotFoundException",
|
||||
"ImportValidationError",
|
||||
"ExportError",
|
||||
"SyncError",
|
||||
]
|
||||
# =============================================================================
|
||||
# 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},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user