# Adapter Migration Guide

Migrate an existing adapter from the old template-fork model to using `entr-adapter-core` as a package dependency. Follow the steps in order — each step is independent, so you can pause and resume at any point.

> **Before you start:** Your adapter should be on a clean git branch. Every step below is reversible with `git checkout .`

---

## Step 1: Add the package dependency

**Goal:** Replace copied framework code with the shared package.

- [ ] Open your adapter's `pyproject.toml`
- [ ] Add `entr-adapter-core` to the `dependencies` list:
  ```toml
  dependencies = [
      "entr-adapter-core>=1.0,<2",
      # ... your other dependencies
  ]
  ```
- [ ] Run `uv sync` to install the package
- [ ] Verify the import works:
  ```bash
  uv run python -c "from entr_adapter_core import create_adapter_app; print('OK')"
  ```

**How to verify:** `uv pip list | grep entr-adapter-core` shows the installed version.

---

## Step 2: Replace schema imports

**Goal:** Stop using locally copy-pasted schemas and import from the package instead.

- [ ] Open `app/schemas.py` (your local copy of the schemas)
- [ ] Compare each class against the package schemas — identify any tenant-specific additions
- [ ] In every file that imports from `app.schemas`, replace with package imports:

  **Before:**
  ```python
  from app.schemas import DocumentPayload, AdapterResponse, StatusEnum
  ```

  **After:**
  ```python
  from entr_adapter_core.schemas import DocumentPayload, AdapterResponse, StatusEnum
  ```

- [ ] The following schemas are available from the package:
  - `DocumentPayload` — incoming document data from Core
  - `AdapterResponse` — response back to Core (success/failed/review_required)
  - `StatusEnum` — success, failed, review_required
  - `AppliedMapping` — field mapping applied by Core
  - `RetrievalPayload` — incoming retrieval request
  - `RetrievalResponse` — retrieval response with document list
  - `DocumentIdentifier` — document reference (id + filename)
  - `DocumentDownloadResponse` — downloaded document content (base64)
  - `ReviewSuggestion`, `ProposedValue` — review feedback models

- [ ] If you have **custom fields** added to any schema, keep those in a local file and extend the package class:
  ```python
  from entr_adapter_core.schemas import AdapterResponse

  class MyAdapterResponse(AdapterResponse):
      custom_field: str = ""
  ```

- [ ] Run your tests / start the server to verify nothing broke
- [ ] Once all imports point to the package, delete `app/schemas.py`

**How to verify:** `grep -r "from app.schemas" app/` returns no results.

---

## Step 3: Migrate handlers to BaseHandler

**Goal:** Your handler classes should extend `BaseHandler` from the package.

- [ ] Open your handler file(s) in `app/handlers/`
- [ ] Import the base class:
  ```python
  from entr_adapter_core.handlers import BaseHandler
  ```
- [ ] Make your handler extend `BaseHandler`:

  **Before (typical old pattern):**
  ```python
  class MyHandler:
      def __init__(self, settings):
          self.settings = settings

      async def process_document(self, payload):
          # business logic
          return {"status": "success", "message": "done"}
  ```

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

  class MyHandler(BaseHandler):
      async def process(self, payload: DocumentPayload) -> AdapterResponse:
          # business logic — same as before, but:
          # - method name is process() not process_document()
          # - accepts DocumentPayload, returns AdapterResponse
          # - self.settings is available (set by BaseHandler.__init__)
          return AdapterResponse(status=StatusEnum.SUCCESS, message="done")
  ```

- [ ] Key differences to watch for:
  - Method name: `process()` not `process_document()` or `handle()`
  - Input: `DocumentPayload` (not a raw dict)
  - Output: `AdapterResponse` (not a raw dict)
  - Settings: access via `self.settings` (passed to constructor)
  - Retrieval: if your handler does document retrieval, accept `retrieval_handler` in the constructor
  - **Don't wrap `process()` in a broad try/except** — the framework's flight recorder handles unhandled exceptions automatically

- [ ] If you have a retrieval handler, extend `RetrievalHandler` instead:
  ```python
  from entr_adapter_core.handlers import RetrievalHandler

  class MyRetrievalHandler(RetrievalHandler):
      async def list_documents(self, payload: RetrievalPayload) -> RetrievalResponse:
          ...
      async def get_document(self, document_identifier: str) -> DocumentDownloadResponse:
          ...
  ```

- [ ] Remove `app/processing_engine/` and `app/retrieval_engine/` directories once handlers are migrated

**How to verify:** Start the adapter and process a test document through Core.

---

## Step 4: Switch to the app factory

**Goal:** Replace direct `FastAPI()` instantiation with the package's `create_adapter_app()`.

- [ ] Open `app/main.py`
- [ ] Replace the old startup pattern with the factory:

  **Before (old `app/main.py` — ~150 lines of boilerplate):**
  ```python
  from fastapi import FastAPI
  app = FastAPI(title="ENTR Adapter")

  @app.on_event("startup")
  async def startup_event():
      register_tenant_handlers()

  @app.post("/process")
  async def process(payload: DocumentPayload):
      handler = get_handler_instance(payload.adapter_identifier)
      return await handler.process(payload)

  # ... more boilerplate endpoints
  ```

  **After (new `src/main.py` — ~20 lines):**
  ```python
  from entr_adapter_core import create_adapter_app
  from src.config import get_settings
  from src.handlers.my_handler import MyHandler

  settings = get_settings()

  handlers = {
      "my-identifier": MyHandler(settings=settings),
  }

  app = create_adapter_app(
      handlers=handlers,
      title=f"ENTR Adapter — {settings.TENANT_NAME}",
      settings=settings,
  )
  ```

- [ ] The factory provides these endpoints automatically:
  - `POST /process` — document processing (dispatches to correct handler)
  - `POST /list_documents` — document retrieval
  - `POST /download_document` — document download
  - `GET /health` — health check

- [ ] If you have **custom routes** beyond the standard ones, use `create_adapter_router()` instead:
  ```python
  from fastapi import FastAPI
  from entr_adapter_core import create_adapter_router

  app = FastAPI()
  router = create_adapter_router(handlers=handlers)
  app.include_router(router)

  @app.get("/my-custom-endpoint")
  async def custom():
      return {"status": "ok"}
  ```

- [ ] Remove these files (now provided by the package):
  - `app/processing_engine/registry.py`
  - `app/registration.py`
  - Any boilerplate endpoint code in `main.py`

- [ ] Move your source from `app/` to `src/` to match the new template structure (optional but recommended)

**How to verify:** `curl http://localhost:8096/health` returns `{"status": "healthy"}`.

---

## Step 5: Migrate deployment files

**Goal:** Use the package's default deployment files, override only what's customized.

- [ ] Compare your deployment files against the package defaults. The package provides:
  - `Dockerfile`
  - `nginx.conf`
  - `docker-compose.yml`
  - `docker-compose.dev.yml`
  - `docker-compose.prod.yml`
  - `fluent-bit.conf`

- [ ] For each file, decide:
  - **Identical to default** → Delete it. The package default will be used automatically.
  - **Customized** → Move it to `deploy/` override directory.

  ```bash
  # Create the overrides directory
  mkdir -p deploy

  # Move customized files (example — only move files you've actually changed)
  mv Dockerfile deploy/Dockerfile
  mv nginx.conf deploy/nginx.conf

  # Delete files identical to defaults
  rm docker-compose.yml docker-compose.dev.yml
  ```

- [ ] The override resolution order is:
  1. `deploy/<file>` (your override) — used if present
  2. Package default — used if no override exists

- [ ] Common customizations that warrant an override:
  - `Dockerfile` — extra system packages, custom build steps
  - `nginx.conf` — custom routing, SSL, rate limiting
  - `docker-compose.prod.yml` — extra services, volume mounts
  - `fluent-bit.conf` — custom log group names

**How to verify:** `docker compose build` still succeeds.

---

## Step 6: Remove remaining boilerplate

**Goal:** Delete files that the package now provides.

- [ ] Check for and remove these commonly duplicated files:
  - `app/schemas.py` (replaced in Step 2)
  - `app/processing_engine/` directory (replaced in Step 3)
  - `app/retrieval_engine/base.py` (replaced in Step 3)
  - `app/registration.py` (replaced in Step 4)
  - `app/utils/flight_recorder.py` (the package handles this automatically)
  - `app/utils/sandbox.py` (if identical to template)

- [ ] Keep files that are genuinely tenant-specific:
  - `app/handlers/` — your business logic
  - `app/connectors/` — tenant-specific integrations (unless using shared connectors)
  - `app/matching/` — unless migrated to `entr_adapter_core.utils.matching`
  - `app/config.py` / `app/tenant_config.py` — tenant settings
  - `app/services/` — tenant-specific service logic

- [ ] Run a final import check:
  ```bash
  uv run python -c "from src.main import app; print('OK')"
  ```

- [ ] Run your tests to confirm nothing broke

**How to verify:** The adapter starts, processes a document, and all endpoints work.

---

## Quick Reference: Before & After

| Aspect | Before (template-fork) | After (package-dependency) |
|--------|----------------------|---------------------------|
| Schemas | `app/schemas.py` (local copy) | `from entr_adapter_core.schemas import ...` |
| Handlers | Custom base class or none | Extends `BaseHandler` / `RetrievalHandler` |
| App setup | `FastAPI()` + manual routes | `create_adapter_app(handlers={...})` |
| Deployment | Files in repo root | Package defaults + `deploy/` overrides |
| Processing engine | `app/processing_engine/` | Built into package dispatcher |
| Flight recorder | `app/utils/flight_recorder.py` | Automatic — handled by framework |
| Source dir | `app/` | `src/` (recommended) |
| Dependency | Template code copied | `entr-adapter-core>=1.0,<2` in pyproject.toml |

---

## Troubleshooting

**Import errors after Step 2:**
Your code may import schemas via intermediate modules (e.g., `from app.utils import DocumentPayload`). Search for all import paths: `grep -rn "DocumentPayload\|AdapterResponse\|StatusEnum" app/`

**Handler not found after Step 4:**
The `adapter_identifier` in your `handlers` dict must match what Core sends in `DocumentPayload.adapter_identifier`. Check your Core Process settings.

**Deployment files not resolving after Step 5:**
The package resolves files at import time. Ensure `entr-adapter-core` is installed in the Docker image. Check with `docker compose exec adapter python -c "from entr_adapter_core.defaults.resolve import resolve_all_files; print(resolve_all_files())"`.

**Flight recorder logs missing:**
If you removed your custom flight recorder but don't see execution logs, ensure your handler extends `BaseHandler` (not a plain class). The dispatcher only wraps `BaseHandler` subclasses.
