# Error Handling

Comprehensive error handling system for the FastAPI multi-tenant e-commerce platform.

## Overview

The application uses a structured exception hierarchy with custom exception classes and centralized error handlers. All exceptions are logged, formatted consistently, and return appropriate HTTP status codes.

## Exception Hierarchy

### Base Exceptions

All custom exceptions inherit from base exception classes defined in `app.exceptions`:

```python
from app.exceptions import (
    InvalidTokenException,
    TokenExpiredException,
    InvalidCredentialsException,
    UserNotActiveException,
    AdminRequiredException,
    InsufficientPermissionsException,
    RateLimitException
)
```

### Authentication Exceptions

| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `InvalidTokenException` | 401 | JWT token is invalid, malformed, or missing required claims |
| `TokenExpiredException` | 401 | JWT token has expired |
| `InvalidCredentialsException` | 401 | Username/password authentication failed |
| `UserNotActiveException` | 403 | User account is inactive or disabled |

### Authorization Exceptions

| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `AdminRequiredException` | 403 | Endpoint requires admin role |
| `InsufficientPermissionsException` | 403 | User lacks required permissions |

### Rate Limiting Exceptions

| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `RateLimitException` | 429 | Too many requests, rate limit exceeded |

### Shopping Cart Exceptions

| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `CartItemNotFoundException` | 404 | Cart item not found (product not in cart) |
| `InsufficientInventoryForCartException` | 400 | Product doesn't have enough inventory for cart operation |
| `InvalidCartQuantityException` | 422 | Cart quantity is invalid (e.g., less than min or greater than max) |
| `CartValidationException` | 422 | Cart data validation failed |
| `EmptyCartException` | 422 | Operation attempted on empty cart |
| `ProductNotAvailableForCartException` | 400 | Product is not available for adding to cart |

### Product Exceptions

| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `ProductNotFoundException` | 404 | Product not found in store catalog |
| `ProductNotActiveException` | 400 | Product is inactive and cannot be purchased |

### Inventory Exceptions

| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `InventoryNotFoundException` | 404 | Inventory record not found |
| `InsufficientInventoryException` | 400 | Not enough inventory for operation |

## Error Response Format

All custom exceptions (inheriting from `OrionException`) return a structured JSON format:

```json
{
  "error_code": "PRODUCT_NOT_FOUND",
  "message": "Product with ID '123' not found in store 1 catalog",
  "status_code": 404,
  "details": {
    "resource_type": "Product",
    "identifier": "123",
    "product_id": 123,
    "store_id": 1
  }
}
```

**Standard Fields:**

| Field | Type | Description |
|-------|------|-------------|
| `error_code` | string | Machine-readable error code (e.g., "CART_ITEM_NOT_FOUND") |
| `message` | string | Human-readable error message |
| `status_code` | integer | HTTP status code |
| `details` | object | Additional context-specific error details |

**Note:** Generic FastAPI/HTTP errors may still use the simpler format:

```json
{
  "detail": "Error message",
  "status_code": 401
}
```

### Rate Limit Error Response

Rate limit errors include additional information:

```json
{
  "detail": "Rate limit exceeded",
  "status_code": 429,
  "retry_after": 3600,
  "timestamp": "2024-11-16T13:00:00Z",
  "path": "/api/v1/resource"
}
```

## Usage Examples

### Raising Exceptions in Code

```python
from app.exceptions import InvalidCredentialsException, AdminRequiredException

# Authentication failure
if not user:
    raise InvalidCredentialsException("User not found")

# Authorization check
if user.role != "admin":
    raise AdminRequiredException()
```

### Catching Exceptions in Routes

```python
from fastapi import HTTPException
from app.exceptions import InvalidTokenException

@app.post("/api/v1/auth/protected")
async def protected_endpoint(token: str):
    try:
        user_data = auth_manager.verify_token(token)
        return {"user": user_data}
    except InvalidTokenException as e:
        # Exception will be caught by global handler
        raise
    except Exception as e:
        # Unexpected errors
        logger.error(f"Unexpected error: {e}")
        raise HTTPException(status_code=500, detail="Internal server error")
```

## Context-Aware Error Handling

The error handling system is context-aware and provides different error formats based on the request context:

### API Requests (`/api/*`)
Returns JSON error responses suitable for API clients.

### Admin/Store Dashboard (`/admin/*`, `/store/*`)
Returns JSON errors or redirects to error pages based on accept headers.

### Storefront Requests (`/storefront/*`)
Returns themed error pages matching the store's shop design.

## Logging

All errors are automatically logged with the following information:
- Error type and message
- Request path and method
- User information (if authenticated)
- Stack trace (for unexpected errors)
- Timestamp

Example log output:
```
2024-11-16 13:00:00 [ERROR] middleware.auth: Token verification error: Token missing user identifier
2024-11-16 13:00:00 [ERROR] app.main: Request failed: POST /api/v1/auth/login - 401
```

## Best Practices

### 1. Use Specific Exceptions
Always use the most specific exception class available:

```python
# Good
raise InvalidCredentialsException("Invalid email or password")

# Avoid
raise HTTPException(status_code=401, detail="Invalid credentials")
```

### 2. Provide Meaningful Messages
Include context in error messages:

```python
# Good
raise InvalidTokenException("Token missing user identifier")

# Avoid
raise InvalidTokenException("Invalid token")
```

### 3. Don't Expose Sensitive Information
Never include sensitive data in error messages:

```python
# Good
raise InvalidCredentialsException("Invalid email or password")

# Avoid - reveals which field is wrong
raise InvalidCredentialsException(f"User {email} not found")
```

### 4. Log Before Raising
Log errors before raising them for debugging:

```python
try:
    result = risky_operation()
except OperationFailed as e:
    logger.error(f"Operation failed: {e}", exc_info=True)
    raise InternalServerException("Operation failed")
```

## Testing Error Handling

### Unit Tests

```python
import pytest
from app.exceptions import InvalidTokenException

def test_invalid_token():
    auth_manager = AuthManager()

    with pytest.raises(InvalidTokenException) as exc_info:
        auth_manager.verify_token("invalid-token")

    assert "Could not validate credentials" in str(exc_info.value.message)
```

### Integration Tests

```python
def test_authentication_error_response(client):
    response = client.post(
        "/api/v1/auth/login",
        json={"username": "wrong", "password": "wrong"}
    )

    assert response.status_code == 401
    assert "detail" in response.json()
```

## Global Exception Handlers

The application registers global exception handlers in `main.py`:

```python
from fastapi import FastAPI
from app.exceptions import InvalidTokenException, RateLimitException

app = FastAPI()

@app.exception_handler(InvalidTokenException)
async def invalid_token_handler(request, exc):
    return JSONResponse(
        status_code=401,
        content={
            "detail": exc.message,
            "status_code": 401,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "path": str(request.url.path)
        }
    )
```

## Frontend Error Handling (apiClient)

When a request fails, the shared `apiClient` (`static/shared/js/api-client.js`) throws an `Error` with three structured properties populated from the JSON response, in addition to the `message`:

| Property | Source | Example |
|----------|--------|---------|
| `error.status` | HTTP status code | `429` |
| `error.errorCode` | Server `error_code` field | `"POINTS_COOLDOWN"` |
| `error.details` | Server `details` object (may be `null`) | `{ cooldown_minutes: 5, cooldown_ends: "..." }` |

This lets callers branch on a machine-readable code and localise the toast instead of rendering the raw English `message`. Pattern:

```js
try {
    await apiClient.post('/store/loyalty/transactions', payload);
} catch (error) {
    if (error.errorCode === 'POINTS_COOLDOWN' || error.errorCode === 'STAMP_COOLDOWN') {
        const minutes = error.details?.cooldown_minutes ?? '';
        Utils.showToast(I18n.t('loyalty.store.terminal.cooldown_wait_minutes', { minutes }), 'warning');
    } else {
        Utils.showToast(I18n.t('loyalty.store.terminal.transaction_failed', { message: error.message }), 'error');
    }
}
```

Generic FastAPI errors that don't carry `error_code` / `details` will leave those properties `undefined` / `null`, so a `?.` chain is the right guard.

## Related Documentation

- [Authentication](authentication.md) - Authentication-related exceptions
- [RBAC](rbac.md) - Authorization and permission exceptions
- [Rate Limiting](rate-limiting.md) - Rate limit error handling
- [Testing Guide](../testing/testing-guide.md) - Testing error scenarios
