diff --git a/app/api/v1/admin/vendor_themes.py b/app/api/v1/admin/vendor_themes.py index 9346e0b1..52dd1997 100644 --- a/app/api/v1/admin/vendor_themes.py +++ b/app/api/v1/admin/vendor_themes.py @@ -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 diff --git a/app/api/v1/admin/vendors.py b/app/api/v1/admin/vendors.py index 6c0b01ce..3000aaa5 100644 --- a/app/api/v1/admin/vendors.py +++ b/app/api/v1/admin/vendors.py @@ -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} \ No newline at end of file + return {"message": message} diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py index 03825953..bfa4a3b1 100644 --- a/app/exceptions/__init__.py +++ b/app/exceptions/__init__.py @@ -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", ] \ No newline at end of file diff --git a/app/exceptions/admin.py b/app/exceptions/admin.py index 2f9ab138..8df4d62e 100644 --- a/app/exceptions/admin.py +++ b/app/exceptions/admin.py @@ -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, + ) diff --git a/app/exceptions/base.py b/app/exceptions/base.py index 8f350364..79fc589d 100644 --- a/app/exceptions/base.py +++ b/app/exceptions/base.py @@ -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"): diff --git a/app/exceptions/handler.py b/app/exceptions/handler.py index 99cb7705..f208ae01 100644 --- a/app/exceptions/handler.py +++ b/app/exceptions/handler.py @@ -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) diff --git a/docs/__temp/BACKEND/ADMIN_FEATURE_INTEGRATION_GUIDE.md b/docs/__temp/BACKEND/ADMIN_FEATURE_INTEGRATION_GUIDE.md index 77900948..1b4f8831 100644 --- a/docs/__temp/BACKEND/ADMIN_FEATURE_INTEGRATION_GUIDE.md +++ b/docs/__temp/BACKEND/ADMIN_FEATURE_INTEGRATION_GUIDE.md @@ -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 diff --git a/docs/__temp/FRONT_TO_BACK_MULTITENANT/VENDOR_DOMAIN/VENDOR_DOMAIN_ARCHITECTURE_DIAGRAMS.md b/docs/__temp/FRONT_TO_BACK_MULTITENANT/VENDOR_DOMAIN/VENDOR_DOMAIN_ARCHITECTURE_DIAGRAMS.md index 0f383cb3..656ae717 100644 --- a/docs/__temp/FRONT_TO_BACK_MULTITENANT/VENDOR_DOMAIN/VENDOR_DOMAIN_ARCHITECTURE_DIAGRAMS.md +++ b/docs/__temp/FRONT_TO_BACK_MULTITENANT/VENDOR_DOMAIN/VENDOR_DOMAIN_ARCHITECTURE_DIAGRAMS.md @@ -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, │ diff --git a/docs/development/exception-handling.md b/docs/development/exception-handling.md index 1ad08e5f..bad56856 100644 --- a/docs/development/exception-handling.md +++ b/docs/development/exception-handling.md @@ -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( diff --git a/static/admin/js/vendor-theme.js b/static/admin/js/vendor-theme.js index 06b18572..036f3c1e 100644 --- a/static/admin/js/vendor-theme.js +++ b/static/admin/js/vendor-theme.js @@ -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; diff --git a/tests/integration/api/v1/test_inventory_endpoints.py b/tests/integration/api/v1/test_inventory_endpoints.py index 3c107d6a..bf121bba 100644 --- a/tests/integration/api/v1/test_inventory_endpoints.py +++ b/tests/integration/api/v1/test_inventory_endpoints.py @@ -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}" diff --git a/tests/integration/api/v1/test_marketplace_products_endpoints.py b/tests/integration/api/v1/test_marketplace_products_endpoints.py index 5d4ea3cb..3e01c0e8 100644 --- a/tests/integration/api/v1/test_marketplace_products_endpoints.py +++ b/tests/integration/api/v1/test_marketplace_products_endpoints.py @@ -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}" diff --git a/tests/integration/api/v1/test_pagination.py b/tests/integration/api/v1/test_pagination.py index 924c96fa..c2440c18 100644 --- a/tests/integration/api/v1/test_pagination.py +++ b/tests/integration/api/v1/test_pagination.py @@ -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}" diff --git a/tests/integration/api/v1/test_vendor_endpoints.py b/tests/integration/api/v1/test_vendor_endpoints.py index 44f6c990..663cca06 100644 --- a/tests/integration/api/v1/test_vendor_endpoints.py +++ b/tests/integration/api/v1/test_vendor_endpoints.py @@ -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}" diff --git a/tests/unit/services/test_inventory_service.py b/tests/unit/services/test_inventory_service.py index 044ef903..78942474 100644 --- a/tests/unit/services/test_inventory_service.py +++ b/tests/unit/services/test_inventory_service.py @@ -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')