# Exception Handling Guide

## Overview

The Orion API uses a unified custom exception system to provide consistent, meaningful error responses across all endpoints. This system was redesigned to eliminate competing exception handlers and provide a single source of truth for error handling.

## Architecture

### Exception Hierarchy

```
OrionException (Base)
├── ValidationException (422)
├── AuthenticationException (401)
├── AuthorizationException (403)
├── ResourceNotFoundException (404)
├── ConflictException (409)
├── BusinessLogicException (400)
├── RateLimitException (429)
└── ExternalServiceException (502)
```

### File Structure

```
app/exceptions/
├── __init__.py           # All exception exports
├── base.py              # Base exception classes
├── handler.py           # Unified FastAPI exception handlers
├── auth.py              # Authentication/authorization exceptions
├── admin.py             # Admin operation exceptions
├── marketplace.py       # Import/marketplace exceptions
├── product.py           # MarketplaceProduct management exceptions
├── shop.py              # Shop management exceptions
└── inventory.py             # Inventory management exceptions
```

## Core Concepts

### Unified Exception Handling

The system uses **only FastAPI's built-in exception handlers** - no middleware or competing systems. All exceptions are processed through a single, consistent pipeline in `handler.py`.

### Exception Response Format

All custom exceptions return a consistent JSON structure:

```json
{
  "error_code": "PRODUCT_NOT_FOUND",
  "message": "MarketplaceProduct with ID 'ABC123' not found",
  "status_code": 404,
  "details": {
    "resource_type": "MarketplaceProduct",
    "identifier": "ABC123"
  }
}
```

### HTTP Status Codes

The system uses appropriate HTTP status codes:

- **400**: Business logic violations (cannot modify own admin account)
- **401**: Authentication failures (invalid credentials, expired tokens)
- **403**: Authorization failures (admin privileges required)
- **404**: Resource not found (user, shop, product not found)
- **409**: Resource conflicts (duplicate shop codes, existing products)
- **422**: Validation errors (Pydantic validation failures)
- **429**: Rate limiting (too many requests)
- **500**: Internal server errors (unexpected exceptions)

## Implementation Details

### Exception Handler Setup

The main application registers all exception handlers in a single function:

```python
# main.py
from app.exceptions.handler import setup_exception_handlers

app = FastAPI(...)
setup_exception_handlers(app)  # Single registration point
```

### Handler Types

The system handles four categories of exceptions:

```python
# app/exceptions/handler.py

@app.exception_handler(OrionException)
async def custom_exception_handler(request, exc):
    """Handle all custom business exceptions"""
    return JSONResponse(
        status_code=exc.status_code,
        content=exc.to_dict()
    )

@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    """Handle FastAPI HTTP exceptions"""
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error_code": f"HTTP_{exc.status_code}",
            "message": exc.detail,
            "status_code": exc.status_code,
        }
    )

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    """Handle Pydantic validation errors"""
    return JSONResponse(
        status_code=422,
        content={
            "error_code": "VALIDATION_ERROR",
            "message": "Request validation failed",
            "status_code": 422,
            "details": {"validation_errors": exc.errors()}
        }
    )

@app.exception_handler(Exception)
async def generic_exception_handler(request, exc):
    """Handle unexpected exceptions"""
    return JSONResponse(
        status_code=500,
        content={
            "error_code": "INTERNAL_SERVER_ERROR",
            "message": "Internal server error",
            "status_code": 500,
        }
    )
```

## Working with Exceptions

### Service Layer Pattern

Services raise custom exceptions with specific error codes:

```python
# app/services/admin_service.py
from app.exceptions.admin import UserNotFoundException, CannotModifySelfException

def toggle_user_status(self, db: Session, user_id: int, current_admin_id: int):
    user = self._get_user_by_id_or_raise(db, user_id)

    # Prevent self-modification
    if user.id == current_admin_id:
        raise CannotModifySelfException(user_id, "deactivate account")

    # Prevent admin-to-admin modification
    if user.role == "admin" and user.id != current_admin_id:
        raise UserStatusChangeException(
            user_id=user_id,
            current_status="admin",
            attempted_action="toggle status",
            reason="Cannot modify another admin user"
        )
```

### Authentication Integration

Authentication dependencies use custom exceptions instead of HTTPException:

```python
# middleware/auth.py
from app.exceptions.auth import AdminRequiredException, TokenExpiredException

def require_admin(self, current_user: User):
    if current_user.role != "admin":
        raise AdminRequiredException()  # Returns error_code: "ADMIN_REQUIRED"
    return current_user

def verify_token(self, token: str):
    try:
        payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
        # ... token verification logic
    except jwt.ExpiredSignatureError:
        raise TokenExpiredException()  # Returns error_code: "TOKEN_EXPIRED"
```

### Endpoint Implementation

Endpoints rely on service layer exceptions and don't handle HTTPException:

```python
# app/api/v1/admin.py
@router.put("/admin/users/{user_id}/status")
def toggle_user_status(
    user_id: int,
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),  # May raise AdminRequiredException
):
    # Service raises UserNotFoundException, CannotModifySelfException, etc.
    user, message = admin_service.toggle_user_status(db, user_id, current_admin.id)
    return {"message": message}
```

## Creating New Exceptions

### Step 1: Define Domain-Specific Exceptions

Create exceptions in the appropriate module:

```python
# app/exceptions/payment.py
from .base import BusinessLogicException, ResourceNotFoundException

class PaymentNotFoundException(ResourceNotFoundException):
    """Raised when payment record is not found."""

    def __init__(self, payment_id: str):
        super().__init__(
            resource_type="Payment",
            identifier=payment_id,
            message=f"Payment with ID '{payment_id}' not found",
            error_code="PAYMENT_NOT_FOUND",
        )

class InsufficientFundsException(BusinessLogicException):
    """Raised when account has insufficient funds."""

    def __init__(self, required: float, available: float, account_id: str):
        super().__init__(
            message=f"Insufficient funds. Required: {required}, Available: {available}",
            error_code="INSUFFICIENT_FUNDS",
            details={
                "required_amount": required,
                "available_amount": available,
                "account_id": account_id,
            },
        )
```

### Step 2: Export in __init__.py

```python
# app/exceptions/__init__.py
from .payment import PaymentNotFoundException, InsufficientFundsException

__all__ = [
    # ... existing exports
    "PaymentNotFoundException",
    "InsufficientFundsException",
]
```

### Step 3: Use in Services

```python
# app/services/payment_service.py
from app.exceptions import PaymentNotFoundException, InsufficientFundsException

def process_payment(self, db: Session, payment_id: str, amount: float):
    payment = self._get_payment_by_id_or_raise(db, payment_id)

    if payment.account_balance < amount:
        raise InsufficientFundsException(amount, payment.account_balance, payment.account_id)

    # Process payment...
    return payment
```

## Testing Exception Handling

### Unit Tests

Test exception properties and inheritance:

```python
# tests/unit/exceptions/test_admin_exceptions.py
import pytest
from app.exceptions.admin import UserNotFoundException, CannotModifySelfException

def test_user_not_found_exception():
    exc = UserNotFoundException("123")

    assert exc.error_code == "USER_NOT_FOUND"
    assert exc.status_code == 404
    assert "123" in exc.message
    assert exc.details["identifier"] == "123"

def test_cannot_modify_self_exception():
    exc = CannotModifySelfException(456, "deactivate account")

    assert exc.error_code == "CANNOT_MODIFY_SELF"
    assert exc.status_code == 400
    assert "deactivate account" in exc.message
```

### Integration Tests

Test complete error handling flow:

```python
# tests/integration/api/v1/test_admin_endpoints.py
def test_toggle_user_status_cannot_modify_self(client, admin_headers, test_admin):
    """Test that admin cannot modify their own account"""
    response = client.put(
        f"/api/v1/admin/users/{test_admin.id}/status",
        headers=admin_headers
    )

    assert response.status_code == 400
    data = response.json()
    assert data["error_code"] == "CANNOT_MODIFY_SELF"
    assert "Cannot perform 'deactivate account' on your own account" in data["message"]

def test_get_all_users_non_admin(client, auth_headers):
    """Test non-admin trying to access admin endpoint"""
    response = client.get("/api/v1/admin/users", headers=auth_headers)

    assert response.status_code == 403
    data = response.json()
    assert data["error_code"] == "ADMIN_REQUIRED"  # Custom exception, not HTTP_403
    assert "Admin privileges required" in data["message"]
```

## Migration from Legacy System

### What Was Changed

1. **Removed competing handlers**: Eliminated `ExceptionHandlerMiddleware` and duplicate exception handling
2. **Unified response format**: All exceptions now return consistent JSON structure
3. **Enhanced authentication**: Replaced `HTTPException` in auth layer with custom exceptions
4. **Improved logging**: Added structured logging with request context

### Breaking Changes

- Authentication failures now return `ADMIN_REQUIRED` instead of `HTTP_403`
- Rate limiting returns structured error with `RATE_LIMIT_EXCEEDED` code
- All custom exceptions use 400 for business logic errors (not 422)

### Migration Steps

1. **Update exception imports**: Replace `HTTPException` imports with custom exceptions
2. **Fix test expectations**: Update tests to expect specific error codes (e.g., `ADMIN_REQUIRED`)
3. **Remove legacy handlers**: Delete any remaining middleware or duplicate handlers
4. **Update frontend**: Map new error codes to user-friendly messages

## Error Code Reference

### Authentication (401)
- `INVALID_CREDENTIALS`: Invalid username or password
- `TOKEN_EXPIRED`: JWT token has expired
- `INVALID_TOKEN`: Malformed or invalid token
- `USER_NOT_ACTIVE`: User account is deactivated

### Authorization (403)
- `ADMIN_REQUIRED`: Admin privileges required for operation
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions
- `UNAUTHORIZED_SHOP_ACCESS`: Cannot access shop (not owner)

### Resource Not Found (404)
- `USER_NOT_FOUND`: User with specified ID not found
- `SHOP_NOT_FOUND`: Shop with specified code/ID not found
- `PRODUCT_NOT_FOUND`: MarketplaceProduct with specified ID not found

### Business Logic (400)
- `CANNOT_MODIFY_SELF`: Admin cannot modify own account
- `USER_STATUS_CHANGE_FAILED`: Cannot change user status
- `SHOP_VERIFICATION_FAILED`: Shop verification operation failed
- `ADMIN_OPERATION_FAILED`: Generic admin operation failure

### Validation (422)
- `VALIDATION_ERROR`: Pydantic request validation failed
- `INVALID_PRODUCT_DATA`: MarketplaceProduct data validation failed
- `INVALID_SHOP_DATA`: Shop data validation failed

### Rate Limiting (429)
- `RATE_LIMIT_EXCEEDED`: API rate limit exceeded

### Service Errors (500/502)
- `INTERNAL_SERVER_ERROR`: Unexpected server error
- `EXTERNAL_SERVICE_ERROR`: Third-party service failure

## Best Practices

### Exception Design
- Use specific error codes for programmatic handling
- Include relevant context in exception details
- Inherit from appropriate base exception class
- Provide clear, actionable error messages

### Service Layer
- Raise custom exceptions, never HTTPException
- Include identifiers and context in exception details
- Use private helper methods with `_or_raise` suffix
- Log errors before raising exceptions

### Testing
- Test both exception raising and response format
- Verify error codes, status codes, and message content
- Test edge cases and error conditions
- Use custom exceptions in test assertions

### Frontend Integration
- Map error codes to user-friendly messages
- Handle field-specific validation errors
- Implement retry logic for transient errors
- Log errors for debugging and monitoring

This unified exception system provides consistent error handling across your entire application while maintaining clean separation of concerns and excellent developer experience.
