# Existing Adapter Migration Guide

**Package version:** `entr-adapter-core >= 1.1.0`
**Reference migration:** MessageBird-Nest (completed 2026-03-17)

This guide walks through migrating an existing adapter from the template-fork model (`app/`, `deploy-files/`, copy-pasted schemas) to the package-dependency model (`src/`, `entr-adapter-core`, `create_adapter_app()`).

**Key principle:** Each step is independently deployable. You can deploy after any step and the adapter continues working. Steps 1-5 are reversible. Step 6 (remove boilerplate) is not.

---

## Pre-Migration Checklist

Before starting, verify:

- [ ] `entr-adapter-core >= 1.1.0` is available on PyPI
- [ ] You have the adapter repo cloned locally
- [ ] The adapter is currently working in production
- [ ] You've tagged the current state for rollback: `git tag pre-migration`

Document the current structure:

```
MessageBird-TenantName/
├── app/
│   ├── main.py                     # Custom FastAPI app
│   ├── schemas.py                  # Copy-pasted DocumentPayload, AdapterResponse
│   ├── config.py                   # CoreSettings
│   ├── tenant_config.py            # TenantSettings
│   ├── registration.py             # Handler registration
│   ├── processing_engine/
│   │   ├── base_handler.py         # Copy-pasted ProcessHandler
│   │   ├── mapping_service.py      # Copy-pasted MappingService
│   │   └── registry.py             # Handler registry
│   ├── retrieval_engine/
│   │   └── base.py                 # Copy-pasted RetrievalHandler
│   ├── handlers/
│   │   └── tenant_handler.py       # Tenant-specific logic
│   └── utils/
│       ├── logger.py               # Copy-pasted logger
│       └── flight_recorder.py      # Copy-pasted flight recorder
├── deploy-files/
│   ├── docker-compose.dev.yml
│   ├── docker-compose.prod.yml
│   ├── nginx.conf
│   └── fluent-bit/
├── Dockerfile
├── pyproject.toml
└── .env
```

Identify tenant-specific code that must be preserved:
- `app/handlers/` — your handler implementations
- `app/tenant_config.py` — your environment variables
- `app/registration.py` — your handler registrations
- Any custom clients, connectors, or utilities

---

## Step 1: Add `entr-adapter-core` as dependency

**Goal:** Install the package alongside existing code. No code changes.

Update `pyproject.toml`:

```toml
[project]
dependencies = [
    "entr-adapter-core>=1.0,<2",
    # ... keep existing dependencies
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]
```

```bash
uv sync
uv run python -c "from entr_adapter_core import __version__; print(__version__)"
```

**Deploy and verify:** Adapter functions identically — the package is installed but not used yet.

---

## Step 2: Replace schemas with package imports

**Goal:** Stop using copy-pasted schemas. Import from the package instead.

The package schemas have been cleaned up compared to the old template versions:

| Old (template) | New (package) | Notes |
|----------------|---------------|-------|
| `DocumentPayload.mappings` | Removed | Core applies mappings before sending |
| `DocumentPayload.mapping_rules` | Removed | Core applies mappings before sending |
| `DocumentPayload.meta` | Removed | Metadata moved to other fields |
| `DocumentPayload.context` | Removed | No longer needed |
| `AdapterResponse.generated_mappings` | Removed | Adapter doesn't generate mappings |
| `AdapterResponse.created_mappings` | Removed | Adapter doesn't create mappings |
| `CreatedMapping` | Removed | No longer needed |
| `ListedDocument` | `DocumentIdentifier` | Renamed |

**Find and replace imports:**

```python
# Before
from app.schemas import DocumentPayload, AdapterResponse, StatusEnum
from app.schemas import RetrievalPayload, RetrievalResponse, ListedDocument, DocumentDownloadResponse

# After
from entr_adapter_core import DocumentPayload, AdapterResponse, StatusEnum
from entr_adapter_core import RetrievalPayload, RetrievalResponse, DocumentIdentifier, DocumentDownloadResponse
```

**Remove MappingService usage from handlers:**

```python
# Before — adapter applied mappings
transformed_data = payload.data
if payload.mappings:
    transformed_data = MappingService.apply_mappings(
        payload.data, payload.mappings, payload.mapping_rules
    )

# After — Core already applied mappings, payload.data is clean
data = payload.data  # Mappings already applied by Core
```

**Remove mapping generation from responses:**

```python
# Before
return AdapterResponse(
    document_id=payload.document_id,
    status=StatusEnum.SUCCESS,
    generated_mappings=generated_mappings,  # Remove this
)

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

**Deploy and verify:** All endpoints return the same responses (minus removed mapping fields).

---

## Step 3: Extract handler into BaseHandler subclass

**Goal:** Replace `ProcessHandler` with `BaseHandler` from the package.

Key differences:

| Old (`ProcessHandler`) | New (`BaseHandler`) |
|------------------------|---------------------|
| `handle_document(payload)` | `process(payload)` |
| `postprocess_data()` + `interact_with_target_system()` | Single `process()` method |
| MappingService integration | Removed (Core handles) |
| FlightRecorder manual setup | Automatic (framework wraps `process()`) |

**Migration pattern:**

```python
# Before
from app.processing_engine.base_handler import ProcessHandler
from app.schemas import DocumentPayload, AdapterResponse

class TenantHandler(ProcessHandler):
    def __init__(self, settings, retrieval_handler=None):
        super().__init__(settings, retrieval_handler)

    async def postprocess_data(self, payload):
        # Transform data
        return processed_data

    async def interact_with_target_system(self, processed_data, document_id, original_data=None):
        # Send to ERP
        return {"status": "success", "details": {...}}

    async def handle_document(self, payload):
        processed = await self.postprocess_data(payload)
        result = await self.interact_with_target_system(processed, payload.document_id)
        return AdapterResponse(document_id=payload.document_id, status=result["status"], ...)

# After
from entr_adapter_core import BaseHandler, DocumentPayload, AdapterResponse, StatusEnum

class TenantHandler(BaseHandler):
    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        # 1. Validate
        data = payload.data

        # 2. Transform (was postprocess_data)
        processed = self._transform(data)

        # 3. Send (was interact_with_target_system)
        result = await self._send_to_erp(processed)

        # 4. Respond
        return AdapterResponse(
            document_id=payload.document_id,
            status=StatusEnum.SUCCESS,
            details=result,
        )
```

**Important:** Handlers are singletons. Never store per-request state on `self`.

**Logging:** Replace `from app.utils.logger import get_logger` with standard Python logging:

```python
import logging
logger = logging.getLogger(__name__)
```

The flight recorder captures all log output automatically — no manual setup needed.

**Deploy and verify:** Processing works identically.

---

## Step 4: Swap FastAPI app for factory

**Goal:** Replace custom `app/main.py` with `create_adapter_app()`.

**Create `src/main.py`:**

```python
from entr_adapter_core import create_adapter_app
from src.config import get_settings
from src.handlers.tenant_handler import TenantHandler

settings = get_settings()

handlers = {
    "your_adapter_identifier": TenantHandler(settings=settings),
}

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

**Create `src/config.py`:**

```python
from functools import lru_cache
from entr_adapter_core import CoreSettings

class TenantSettings(CoreSettings):
    # Move fields from app/tenant_config.py here
    ERP_API_URL: str = ""
    ERP_API_KEY: str = ""

@lru_cache()
def get_settings() -> TenantSettings:
    return TenantSettings()
```

**Move handlers to `src/handlers/`:**

```bash
mkdir -p src/handlers
cp app/handlers/tenant_handler.py src/handlers/
touch src/__init__.py src/handlers/__init__.py
```

Update imports in the handler files from `app.*` to `entr_adapter_core.*`.

The factory provides these endpoints automatically:
- `POST /process` — document processing
- `POST /list_documents` — document retrieval
- `POST /download_document` — document download
- `GET /health` — health check
- `GET /adapters` — list registered identifiers
- `GET /version` — version info
- `GET /debug/handlers` — handler details (local env only)

**Deploy and verify:** All endpoints return the same responses.

---

## Step 5: Move deployment files to package defaults

**Goal:** Remove `deploy-files/` and use package defaults with optional overrides.

**Compare your files with package defaults:**

| Your file | Package default | Action |
|-----------|----------------|--------|
| `Dockerfile` | Same | Delete yours |
| `deploy-files/docker-compose.prod.yml` | Same | Delete yours |
| `deploy-files/docker-compose.dev.yml` | Same | Delete yours |
| `deploy-files/nginx.conf` | Same | Delete yours |
| `deploy-files/fluent-bit/*` | Same | Delete yours |
| Custom `Dockerfile` with extra deps | Different | Move to `deploy/Dockerfile` |
| Custom `nginx.conf` with higher limits | Different | Move to `deploy/nginx.conf` |

```bash
# Create override directory
mkdir -p deploy

# Move only files that DIFFER from package defaults
# If your Dockerfile adds custom system deps:
cp deploy-files/Dockerfile deploy/Dockerfile

# If your nginx.conf has custom settings:
cp deploy-files/nginx.conf deploy/nginx.conf

# Delete deploy-files/
rm -rf deploy-files/
rm -f Dockerfile  # Root Dockerfile (now in package or deploy/)
```

Add `deploy/README.md` explaining the override convention (copy from the template).

**Deploy and verify:** Deployment behavior unchanged.

---

## Step 6: Remove duplicated boilerplate

**Warning:** This step is NOT reversible (except via git). Only proceed after verifying Steps 1-5.

**Delete these directories/files:**

```bash
# Copy-pasted framework code (now in package)
rm -rf app/processing_engine/
rm -rf app/retrieval_engine/
rm -f app/schemas.py
rm -f app/config.py          # Replaced by src/config.py
rm -f app/registration.py    # Replaced by src/main.py handler manifest
rm -f app/tenant_config.py   # Replaced by src/config.py

# Old app structure (replaced by src/)
rm -rf app/utils/
rm -f app/__init__.py
rm -f app/main.py

# If app/handlers/ was moved to src/handlers/:
rm -rf app/handlers/

# If app/ is now empty:
rm -rf app/

# Old deployment files
rm -rf deploy-files/

# Template merge artifacts
rm -f TEMPLATE-CHANGELOG.md
rm -f .gitattributes
rm -f .pylintrc
```

**Final structure:**

```
MessageBird-TenantName/
├── src/
│   ├── __init__.py
│   ├── main.py                     # create_adapter_app() with handler manifest
│   ├── config.py                   # TenantSettings extending CoreSettings
│   └── handlers/
│       ├── __init__.py
│       └── tenant_handler.py       # Only tenant-specific logic
├── deploy/                         # Only files that differ from defaults
│   └── README.md
├── tests/
├── .github/workflows/
│   ├── build.yml
│   ├── deploy.yml
│   └── auto-update.yml
├── pyproject.toml                  # entr-adapter-core>=1.0,<2
├── adapter-version-policy.yml
├── core-version.txt
├── CLAUDE.md
├── TENANT-CHANGELOG.md
└── .gitignore
```

**Deploy and verify:** Adapter functions identically to pre-migration version.

---

## Common Pitfalls

1. **`MappingService` removal**: Core now applies mappings before sending `payload.data`. If your handler calls `MappingService.apply_mappings()` or `MappingService.generate_mappings()`, remove those calls. `payload.data` is already mapped.

2. **`handle_document()` → `process()`**: The method name changed. `BaseHandler` has no `postprocess_data()` or `interact_with_target_system()` — consolidate into `process()`.

3. **Schema field removals**: `payload.mappings`, `payload.mapping_rules`, `payload.meta`, `payload.context` no longer exist. Remove any code that references them.

4. **Docker empty directories**: If Docker created empty directories for missing volume mounts (e.g., `nginx.conf/` as a directory), delete them before `entr-cli dev start`.

5. **Singleton handlers**: `BaseHandler` instances are singletons shared across concurrent requests. Never store per-request state on `self`. Use local variables in `process()`.

6. **`ListedDocument` → `DocumentIdentifier`**: The retrieval schema was renamed. Update imports if your handler implements document retrieval.

7. **Logging**: Replace `from app.utils.logger import get_logger` with `import logging; logger = logging.getLogger(__name__)`. The flight recorder captures logs automatically.

---

## Migration Checklist

Use this checklist for each adapter migration:

- [ ] Tag pre-migration state: `git tag pre-migration`
- [ ] **Step 1**: Add `entr-adapter-core>=1.0,<2` to `pyproject.toml` → deploy → verify
- [ ] **Step 2**: Replace schema imports → remove MappingService calls → deploy → verify
- [ ] **Step 3**: Extract handlers to `BaseHandler.process()` → deploy → verify
- [ ] **Step 4**: Create `src/main.py` with `create_adapter_app()` → deploy → verify
- [ ] **Step 5**: Move deployment overrides to `deploy/`, delete `deploy-files/` → deploy → verify
- [ ] **Step 6**: Remove all boilerplate (`app/`, old schemas, old handlers) → deploy → verify
- [ ] Final verification: all endpoints match pre-migration responses
- [ ] Update CLAUDE.md for the new structure
- [ ] Update CI/CD workflows (build.yml, deploy.yml) from template
