Main exception renamed to WizamartException

This commit is contained in:
2025-10-27 21:55:05 +01:00
parent 1e0cbf5927
commit 5c80ba17c5
15 changed files with 126 additions and 179 deletions

View File

@@ -9,30 +9,20 @@ These endpoints allow admins to:
- Reset themes to default
All operations use the service layer for business logic.
All exceptions are handled by the global exception handler.
"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Path, status
from fastapi import APIRouter, Depends, Path
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user, get_db
from app.services.vendor_theme_service import vendor_theme_service
from app.exceptions.vendor import VendorNotFoundException
from app.exceptions.vendor_theme import (
VendorThemeNotFoundException,
ThemePresetNotFoundException,
ThemeValidationException,
ThemeOperationException,
InvalidColorFormatException,
InvalidFontFamilyException
)
from models.database.user import User
from models.schema.vendor_theme import (
VendorThemeResponse,
VendorThemeUpdate,
ThemePresetResponse,
ThemePresetListResponse
)
@@ -61,16 +51,8 @@ async def get_theme_presets(
"""
logger.info("Getting theme presets")
try:
presets = vendor_theme_service.get_available_presets()
return {"presets": presets}
except Exception as e:
logger.error(f"Failed to get theme presets: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve theme presets"
)
presets = vendor_theme_service.get_available_presets()
return {"presets": presets}
# ============================================================================
@@ -97,27 +79,14 @@ async def get_vendor_theme(
- Complete theme configuration including colors, fonts, layout, and branding
**Errors:**
- `404`: Vendor not found
- `500`: Internal server error
- `404`: Vendor not found (VendorNotFoundException)
"""
logger.info(f"Getting theme for vendor: {vendor_code}")
try:
theme = vendor_theme_service.get_theme(db, vendor_code)
return theme
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except Exception as e:
logger.error(f"Failed to get theme for vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve theme"
)
# Service raises VendorNotFoundException if vendor not found
# Global exception handler converts it to HTTP 404
theme = vendor_theme_service.get_theme(db, vendor_code)
return theme
# ============================================================================
@@ -155,40 +124,16 @@ async def update_vendor_theme(
- Updated theme configuration
**Errors:**
- `404`: Vendor not found
- `422`: Validation error (invalid colors, fonts, or layout values)
- `500`: Internal server error
- `404`: Vendor not found (VendorNotFoundException)
- `422`: Validation error (ThemeValidationException, InvalidColorFormatException, etc.)
- `500`: Operation failed (ThemeOperationException)
"""
logger.info(f"Updating theme for vendor: {vendor_code}")
try:
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
return theme.to_dict()
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except (ThemeValidationException, InvalidColorFormatException, InvalidFontFamilyException) as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=e.message
)
except ThemeOperationException as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e.message
)
except Exception as e:
logger.error(f"Failed to update theme for vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update theme"
)
# Service handles all validation and raises appropriate exceptions
# Global exception handler converts them to proper HTTP responses
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
return theme.to_dict()
# ============================================================================
@@ -227,43 +172,20 @@ async def apply_theme_preset(
- Success message and applied theme configuration
**Errors:**
- `404`: Vendor or preset not found
- `500`: Internal server error
- `404`: Vendor not found (VendorNotFoundException) or preset not found (ThemePresetNotFoundException)
- `500`: Operation failed (ThemeOperationException)
"""
logger.info(f"Applying preset '{preset_name}' to vendor {vendor_code}")
try:
theme = vendor_theme_service.apply_theme_preset(db, vendor_code, preset_name)
# Service validates preset name and applies it
# Raises ThemePresetNotFoundException if preset doesn't exist
# Global exception handler converts to HTTP 404
theme = vendor_theme_service.apply_theme_preset(db, vendor_code, preset_name)
return {
"message": f"Applied {preset_name} preset successfully",
"theme": theme.to_dict()
}
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except ThemePresetNotFoundException as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message
)
except ThemeOperationException as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e.message
)
except Exception as e:
logger.error(f"Failed to apply preset to vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to apply preset"
)
return {
"message": f"Applied {preset_name} preset successfully",
"theme": theme.to_dict()
}
# ============================================================================
@@ -291,36 +213,12 @@ async def delete_vendor_theme(
- Success message
**Errors:**
- `404`: Vendor not found or vendor has no custom theme
- `500`: Internal server error
- `404`: Vendor not found (VendorNotFoundException) or no custom theme (VendorThemeNotFoundException)
- `500`: Operation failed (ThemeOperationException)
"""
logger.info(f"Deleting theme for vendor: {vendor_code}")
try:
result = vendor_theme_service.delete_theme(db, vendor_code)
return result
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except VendorThemeNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' has no custom theme"
)
except ThemeOperationException as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e.message
)
except Exception as e:
logger.error(f"Failed to delete theme for vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete theme"
)
# Service handles deletion and raises exceptions if needed
# Global exception handler converts them to proper HTTP responses
result = vendor_theme_service.delete_theme(db, vendor_code)
return result

View File

@@ -6,14 +6,14 @@ Vendor management endpoints for admin.
import logging
from typing import Optional
from fastapi import APIRouter, Depends, Query, Path, HTTPException, Body
from fastapi import APIRouter, Depends, Query, Path, Body
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user
from app.core.database import get_db
from app.services.admin_service import admin_service
from app.services.stats_service import stats_service
from app.exceptions import VendorNotFoundException
from app.exceptions import VendorNotFoundException, ConfirmationRequiredException
from models.schema.stats import VendorStatsResponse
from models.schema.vendor import (
VendorListResponse,
@@ -432,12 +432,13 @@ def delete_vendor(
Requires confirmation parameter: `confirm=true`
"""
# Raise custom exception instead of HTTPException
if not confirm:
raise HTTPException(
status_code=400,
detail="Deletion requires confirmation parameter: confirm=true"
raise ConfirmationRequiredException(
operation="delete_vendor",
message="Deletion requires confirmation parameter: confirm=true"
)
vendor = _get_vendor_by_identifier(db, vendor_identifier)
message = admin_service.delete_vendor(db, vendor.id)
return {"message": message}
return {"message": message}

View File

@@ -8,7 +8,7 @@ messages, and HTTP status mappings.
# Base exceptions
from .base import (
LetzShopException,
WizamartException,
ValidationException,
AuthenticationException,
AuthorizationException,
@@ -41,6 +41,7 @@ from .admin import (
CannotModifySelfException,
InvalidAdminActionException,
BulkOperationException,
ConfirmationRequiredException,
)
# Marketplace import job exceptions
@@ -170,7 +171,7 @@ from .order import (
__all__ = [
# Base exceptions
"LetzShopException",
"WizamartException",
"ValidationException",
"AuthenticationException",
"AuthorizationException",
@@ -304,4 +305,5 @@ __all__ = [
"CannotModifySelfException",
"InvalidAdminActionException",
"BulkOperationException",
"ConfirmationRequiredException",
]

View File

@@ -57,17 +57,17 @@ class UserStatusChangeException(BusinessLogicException):
)
class VendorVerificationException(BusinessLogicException):
"""Raised when vendor verification fails."""
class ShopVerificationException(BusinessLogicException):
"""Raised when shop verification fails."""
def __init__(
self,
vendor_id: int,
shop_id: int,
reason: str,
current_verification_status: Optional[bool] = None,
):
details = {
"vendor_id": vendor_id,
"shop_id": shop_id,
"reason": reason,
}
@@ -75,8 +75,8 @@ class VendorVerificationException(BusinessLogicException):
details["current_verification_status"] = current_verification_status
super().__init__(
message=f"Vendor verification failed for vendor {vendor_id}: {reason}",
error_code="VENDOR_VERIFICATION_FAILED",
message=f"Shop verification failed for shop {shop_id}: {reason}",
error_code="SHOP_VERIFICATION_FAILED",
details=details,
)
@@ -189,3 +189,49 @@ class BulkOperationException(BusinessLogicException):
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: Optional[str] = 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 VendorVerificationException(BusinessLogicException):
"""Raised when vendor verification fails."""
def __init__(
self,
vendor_id: int,
reason: str,
current_verification_status: Optional[bool] = None,
):
details = {
"vendor_id": vendor_id,
"reason": reason,
}
if current_verification_status is not None:
details["current_verification_status"] = current_verification_status
super().__init__(
message=f"Vendor verification failed for vendor {vendor_id}: {reason}",
error_code="VENDOR_VERIFICATION_FAILED",
details=details,
)

View File

@@ -11,7 +11,7 @@ This module provides classes and functions for:
from typing import Any, Dict, Optional
class LetzShopException(Exception):
class WizamartException(Exception):
"""Base exception class for all custom exceptions."""
def __init__(
@@ -41,7 +41,7 @@ class LetzShopException(Exception):
class ValidationException(LetzShopException):
class ValidationException(WizamartException):
"""Raised when request validation fails."""
def __init__(
@@ -64,7 +64,7 @@ class ValidationException(LetzShopException):
class AuthenticationException(LetzShopException):
class AuthenticationException(WizamartException):
"""Raised when authentication fails."""
def __init__(
@@ -81,7 +81,7 @@ class AuthenticationException(LetzShopException):
)
class AuthorizationException(LetzShopException):
class AuthorizationException(WizamartException):
"""Raised when user lacks permission for an operation."""
def __init__(
@@ -97,7 +97,7 @@ class AuthorizationException(LetzShopException):
details=details,
)
class ResourceNotFoundException(LetzShopException):
class ResourceNotFoundException(WizamartException):
"""Raised when a requested resource is not found."""
def __init__(
@@ -122,7 +122,7 @@ class ResourceNotFoundException(LetzShopException):
},
)
class ConflictException(LetzShopException):
class ConflictException(WizamartException):
"""Raised when a resource conflict occurs."""
def __init__(
@@ -138,7 +138,7 @@ class ConflictException(LetzShopException):
details=details,
)
class BusinessLogicException(LetzShopException):
class BusinessLogicException(WizamartException):
"""Raised when business logic rules are violated."""
def __init__(
@@ -155,7 +155,7 @@ class BusinessLogicException(LetzShopException):
)
class ExternalServiceException(LetzShopException):
class ExternalServiceException(WizamartException):
"""Raised when an external service fails."""
def __init__(
@@ -176,7 +176,7 @@ class ExternalServiceException(LetzShopException):
)
class RateLimitException(LetzShopException):
class RateLimitException(WizamartException):
"""Raised when rate limit is exceeded."""
def __init__(
@@ -196,7 +196,7 @@ class RateLimitException(LetzShopException):
details=rate_limit_details,
)
class ServiceUnavailableException(LetzShopException):
class ServiceUnavailableException(WizamartException):
"""Raised when service is unavailable."""
def __init__(self, message: str = "Service temporarily unavailable"):

View File

@@ -16,7 +16,7 @@ from fastapi import Request, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, RedirectResponse
from .base import LetzShopException
from .base import WizamartException
logger = logging.getLogger(__name__)
@@ -24,8 +24,8 @@ logger = logging.getLogger(__name__)
def setup_exception_handlers(app):
"""Setup exception handlers for the FastAPI app."""
@app.exception_handler(LetzShopException)
async def custom_exception_handler(request: Request, exc: LetzShopException):
@app.exception_handler(WizamartException)
async def custom_exception_handler(request: Request, exc: WizamartException):
"""Handle custom exceptions."""
# Special handling for 401 on HTML page requests (redirect to login)

View File

@@ -535,7 +535,7 @@ class DNSVerificationException(ExternalServiceException):
| 409 | `ConflictException` | Resource conflicts |
| 422 | `ValidationException` | Input validation errors |
| 429 | `RateLimitException` | Rate limiting |
| 500 | `LetzShopException` | Generic errors |
| 500 | `WizamartException` | Generic errors |
| 502 | `ExternalServiceException` | Third-party failures |
### Step 2: Update Exception Exports

View File

@@ -237,7 +237,7 @@
│ Exception Handler │
│ app/exceptions/handler.py │
│ │
│ @app.exception_handler(LetzShopException) │
│ @app.exception_handler(WizamartException) │
│ async def custom_exception_handler(...): │
│ return JSONResponse( │
│ status_code=exc.status_code, │

View File

@@ -9,7 +9,7 @@ The LetzShop API uses a unified custom exception system to provide consistent, m
### Exception Hierarchy
```
LetzShopException (Base)
WizamartException (Base)
├── ValidationException (422)
├── AuthenticationException (401)
├── AuthorizationException (403)
@@ -91,7 +91,7 @@ The system handles four categories of exceptions:
```python
# app/exceptions/handler.py
@app.exception_handler(LetzShopException)
@app.exception_handler(WizamartException)
async def custom_exception_handler(request, exc):
"""Handle all custom business exceptions"""
return JSONResponse(

View File

@@ -119,7 +119,7 @@ function adminVendorTheme() {
try {
const startTime = Date.now();
const response = await apiClient.get(`/api/v1/admin/vendors/${this.vendorCode}`);
const response = await apiClient.get(`/admin/vendors/${this.vendorCode}`);
const duration = Date.now() - startTime;
this.vendor = response;
@@ -139,7 +139,7 @@ function adminVendorTheme() {
try {
const startTime = Date.now();
const response = await apiClient.get(`/api/v1/admin/vendor-themes/${this.vendorCode}`);
const response = await apiClient.get(`/admin/vendor-themes/${this.vendorCode}`);
const duration = Date.now() - startTime;
if (response) {
@@ -184,7 +184,7 @@ function adminVendorTheme() {
try {
const startTime = Date.now();
const response = await apiClient.get('/api/v1/admin/vendor-themes/presets');
const response = await apiClient.get('/admin/vendor-themes/presets');
const duration = Date.now() - startTime;
this.presets = response.presets || [];
@@ -207,7 +207,7 @@ function adminVendorTheme() {
try {
const startTime = Date.now();
const response = await apiClient.post(
`/api/v1/admin/vendor-themes/${this.vendorCode}/preset/${presetName}`
`/admin/vendor-themes/${this.vendorCode}/preset/${presetName}`
);
const duration = Date.now() - startTime;
@@ -257,7 +257,7 @@ function adminVendorTheme() {
try {
const startTime = Date.now();
const response = await apiClient.put(
`/api/v1/admin/vendor-themes/${this.vendorCode}`,
`/admin/vendor-themes/${this.vendorCode}`,
this.themeData
);
const duration = Date.now() - startTime;

View File

@@ -434,14 +434,14 @@ class TestInventoryAPI:
assert data["error_code"] == "VALIDATION_ERROR"
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all inventory exceptions follow the consistent LetzShopException structure"""
"""Test that all inventory exceptions follow the consistent WizamartException structure"""
# Test with a known error case
response = client.get("/api/v1/inventory/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
# Verify exception structure matches WizamartException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"

View File

@@ -329,14 +329,14 @@ class TestMarketplaceProductsAPI:
assert data["status_code"] == 401
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all exceptions follow the consistent LetzShopException structure"""
"""Test that all exceptions follow the consistent WizamartException structure"""
# Test with a known error case
response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
# Verify exception structure matches WizamartException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"

View File

@@ -299,7 +299,7 @@ class TestPagination:
assert response.status_code == 422
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
# Verify exception structure matches WizamartException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"

View File

@@ -368,14 +368,14 @@ class TestVendorsAPI:
assert data["error_code"] == "VALIDATION_ERROR"
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all vendor exceptions follow the consistent LetzShopException structure"""
"""Test that all vendor exceptions follow the consistent WizamartException structure"""
# Test with a known error case
response = client.get("/api/v1/vendor/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
# Verify exception structure matches WizamartException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"

View File

@@ -469,14 +469,14 @@ class TestInventoryService:
self.service.add_inventory(db, inventory_data_add)
def test_exception_structure_consistency(self, db):
"""Test that all exceptions follow the consistent LetzShopException structure."""
"""Test that all exceptions follow the consistent WizamartException structure."""
# Test with a known error case
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.get_inventory_by_gtin(db, "9999999999999")
exception = exc_info.value
# Verify exception structure matches LetzShopException.to_dict()
# Verify exception structure matches WizamartException.to_dict()
assert hasattr(exception, 'error_code')
assert hasattr(exception, 'message')
assert hasattr(exception, 'status_code')