# Handlers — Base Handler Framework

## Overview

Handlers are the core extension point for tenant adapters. Each handler processes documents for a specific `adapter_identifier` (e.g., `purchase_order`, `invoice`). The package provides two abstract base classes: `BaseHandler` for document processing and `RetrievalHandler` for pulling documents from external sources.

## Concepts

### Singleton Convention

**Handlers are instantiated once at startup and shared across all concurrent requests.**

This means:
- Use `self` **only** for shared resources: settings, API clients, configuration
- Use **local variables** in `process()` for all per-request data
- **Never** store request-specific state on `self`

If a developer writes `self.current_document = payload.data`, concurrent requests will overwrite each other's state. This is a subtle bug that only manifests under load and can cause data corruption (e.g., sending one customer's invoice to another customer's ERP).

### Error Handling Contract

- **Raise exceptions** for unexpected errors — the framework catches them
- **Return `AdapterResponse` explicitly** for expected business outcomes (success, failure, review_required)

The framework wraps all unhandled exceptions into `AdapterResponse(status="failed")` with a safe error message. No stack traces leak to Core. Flight recorder logs emitted before the crash are still attached to the response.

### Retrieval Handlers

Retrieval handlers are optional. They enable adapters to pull documents from external sources (email, FTP, APIs) on behalf of ENTR Core.

- Implement `RetrievalHandler` as a separate class
- Inject it into `BaseHandler` via the `retrieval_handler` constructor parameter
- Core's scheduler calls `list_documents()` periodically, then `get_document()` for each new document

## Patterns

### Recommended Processing Flow

1. **Validate** — Check the input payload for required fields and business rules
2. **Transform** — Convert extracted data into the target system format
3. **Send** — Deliver transformed data to the target system (ERP, CRM, etc.)
4. **Respond** — Return an `AdapterResponse` with the appropriate status

### Correct Pattern

```python
class MyHandler(BaseHandler):
    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        # Local variables for per-request data
        document_data = payload.data
        transformed = self._transform(document_data)
        result = await self._send_to_erp(transformed)
        return AdapterResponse(document_id=payload.document_id, status=StatusEnum.SUCCESS)
```

### Anti-Pattern: Stateful Handler

```python
class MyHandler(BaseHandler):
    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        # WRONG: storing request data on self — concurrent requests will collide
        self.current_data = payload.data
        self.transformed = self._transform(self.current_data)
        ...
```

### Anti-Patterns Table

| Anti-Pattern | Why It's Wrong | Do Instead |
|-------------|---------------|------------|
| `print()` | Bypasses the flight recorder | Use `logging.getLogger(__name__)` |
| `raise HTTPException` | Framework handles HTTP responses | Return `AdapterResponse(status="failed")` |
| `try/except` around entire `process()` | Hides errors from flight recorder | Let unexpected exceptions propagate |
| Custom flight recorder | Duplicates framework behavior | Use standard logging — flight recorder captures automatically |
| Copy-pasting connectors | Creates maintenance burden | Use package connectors or promote to package |
| `self.request_data = ...` | State leakage across concurrent requests | Use local variables |

### Registration

Handlers are registered at app startup via the handlers dict:

```python
app = create_adapter_app(
    handlers={
        "purchase_order": PurchaseOrderHandler(settings=my_settings),
        "invoice": InvoiceHandler(
            settings=my_settings,
            retrieval_handler=GmailRetrieval(gmail_client),
        ),
    }
)
```

## Guidance

### Handler Checklist

Before deploying a handler, verify:

- [ ] Validate input payload before processing
- [ ] Handle errors gracefully — return meaningful `AdapterResponse` messages
- [ ] Log key decision points during processing
- [ ] Follow the recommended flow: validate → transform → send → respond
- [ ] Never store request-specific data on `self`
- [ ] Reference implementation reviewed in `tests/mock_handler.py`

### Reference Implementation

The test suite includes `tests/mock_handler.py` — a complete reference implementation:

- **`MockProcessHandler`**: Primary reference for new handlers. Demonstrates validate → transform → send → respond.
- **`MockReviewHandler`**: Demonstrates `review_required` flow with `ReviewSuggestion` and `ProposedValue`.
- **`MockRetrievalHandler`**: Demonstrates `list_documents()` and `get_document()` with static data.
- **`MockFailingHandler`**: Shows flight recorder behavior on unhandled exceptions.
- **`MockStatefulHandler`**: Deliberately bad pattern proving state leakage under concurrent requests. Do NOT use as reference.

## Examples

**Minimal handler:**
```python
from entr_adapter_core.handlers import BaseHandler
from entr_adapter_core.schemas import DocumentPayload, AdapterResponse, StatusEnum

class PurchaseOrderHandler(BaseHandler):
    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        if "order_number" not in payload.data:
            return AdapterResponse(
                document_id=payload.document_id,
                status=StatusEnum.FAILED,
                failure_type="validation_error",
                failure_reason="Missing order_number field",
            )

        order_data = {"po_number": payload.data["order_number"]}
        result = await self.settings.erp_client.create_order(order_data)

        return AdapterResponse(
            document_id=payload.document_id,
            status=StatusEnum.SUCCESS,
        )
```

**Handler with retrieval:**
```python
app = create_adapter_app(
    handlers={
        "invoice": InvoiceHandler(
            settings=my_settings,
            retrieval_handler=EntrMailboxRetrievalHandler(settings=my_settings),
        ),
    }
)
```

## Cross-References

- **API reference**: See docstrings in `base.py` for `BaseHandler` and `RetrievalHandler` constructor parameters, method signatures, and return types
- **App factory**: See `app.md` for how handlers are registered and how requests are dispatched
- **Flight recorder**: See `logging.md` for how handler logs are captured and attached to responses
- **Schemas**: See `schemas.md` for `DocumentPayload` and `AdapterResponse` contract details
- **Connectors**: See `connectors.md` for pre-built retrieval handlers (Gmail, Exchange, SFTP, FTP)
- **Mock handlers**: See `tests/mock_handler.py` for reference implementations
