# Implementation Plan — Adobe → DocuSign Migrator v2 *Created: 2026-04-17* --- ## Objective Extend the CLI migration pipeline with: 1. **Idempotent upload** — update the most recently modified DocuSign template with the same name instead of always creating a new one. 2. **Web UI** — browser-based app that lets users authenticate to both platforms, browse templates side-by-side, and run migrations with live feedback. --- ## Architecture Overview ``` adobe-to-docusign-migrator/ ├── src/ # Core pipeline (existing) │ ├── compose_docusign_template.py │ ├── upload_docusign_template.py ← Phase 1: upsert logic added │ ├── adobe_api.py │ ├── docusign_auth.py │ └── ... ├── web/ # New — FastAPI web app │ ├── app.py # FastAPI entrypoint │ ├── config.py # Env/settings │ ├── session.py # Session middleware │ ├── routers/ │ │ ├── auth.py # Adobe + DocuSign OAuth │ │ ├── templates.py # Listing + status API │ │ └── migrate.py # Migration trigger + history │ └── static/ │ ├── index.html # Main SPA page │ ├── app.js # Vanilla JS app │ └── style.css ├── tests/ │ ├── test_mapping.py # Existing field-mapping unit tests │ ├── test_upload_upsert.py # Phase 1: upsert logic │ ├── test_api_health.py # Phase 2: health endpoint │ ├── test_api_auth.py # Phase 3: auth endpoints │ ├── test_api_templates.py # Phase 4: template listing │ ├── test_api_migrate.py # Phase 5: migration API │ └── test_e2e.py # Phase 7: full pipeline e2e └── docs/ ├── IMPLEMENTATION-PLAN.md ← this file └── agent-harness/ └── EXECUTION-BOARD.md ``` --- ## Tech Stack | Layer | Choice | Reason | |---|---|---| | Backend | FastAPI | Matches existing Python; async; auto-generates OpenAPI docs | | Sessions | `starlette-sessions` + `itsdangerous` | Lightweight, no DB needed | | Frontend | Vanilla HTML/CSS/JS | No build tooling; straightforward for this scope | | Testing | `pytest` + `httpx` + `respx` | FastAPI's recommended test stack; respx for mocking HTTP | | HTTP mocking | `respx` | Intercepts `httpx` calls for Adobe/DocuSign API mocks | --- ## Phase 1 — Idempotent Upload **Goal:** `upload_docusign_template.py` should update the most recently modified DocuSign template with the same name, rather than always creating a new one. **Logic:** 1. After loading the template JSON, extract its `name`. 2. Call `GET /v2.1/accounts/{accountId}/templates?search_text={name}` to list matches. 3. Filter to exact name matches; sort by `lastModified` descending; take the first. 4. If found: `PUT /v2.1/accounts/{accountId}/templates/{templateId}` (update). 5. If not found: `POST /v2.1/accounts/{accountId}/templates` (create). 6. Print `Updated template {id}` or `Created template {id}`. 7. Add `--force-create` flag to bypass upsert and always create. **Tests (`tests/test_upload_upsert.py`):** - `test_creates_when_no_match` — no existing templates; POST called. - `test_updates_most_recent_when_match` — two existing templates with same name; PUT called on newer one. - `test_force_create_bypasses_upsert` — `--force-create` always POSTs. - `test_partial_name_match_ignored` — template name contains search term but isn't exact; still creates. --- ## Phase 2 — FastAPI Backend Foundation **Goal:** Runnable FastAPI app with health endpoint, env config, and session middleware. **Endpoints:** - `GET /health` → `{"status": "ok", "version": "2.0"}` **Tests (`tests/test_api_health.py`):** - `test_health_returns_200` - `test_health_response_shape` --- ## Phase 3 — Auth Endpoints **Goal:** Users can connect to Adobe Sign and DocuSign from the browser; tokens stored in server-side session. **Adobe Sign (OAuth 2.0 Authorization Code):** - `GET /api/auth/adobe/start` → redirect to Adobe Sign OAuth URL - `GET /api/auth/adobe/callback?code=...` → exchange code for tokens; store in session - `GET /api/auth/adobe/disconnect` → clear Adobe tokens from session **DocuSign (OAuth 2.0 Authorization Code — demo sandbox):** - `GET /api/auth/docusign/start` → redirect to DocuSign OAuth URL - `GET /api/auth/docusign/callback?code=...` → exchange code for tokens; store in session - `GET /api/auth/docusign/disconnect` → clear DocuSign tokens from session **Status:** - `GET /api/auth/status` → `{"adobe": true/false, "docusign": true/false}` **Tests (`tests/test_api_auth.py`):** - `test_status_unauthenticated` — both false on fresh session. - `test_adobe_callback_stores_token` — mock token exchange; session updated. - `test_docusign_callback_stores_token` — same for DocuSign. - `test_disconnect_clears_token` — after disconnect, status shows false. --- ## Phase 4 — Template Listing API **Goal:** Expose Adobe and DocuSign template lists; compute per-template migration status. **Endpoints:** - `GET /api/templates/adobe` → list of Adobe Sign library documents - `GET /api/templates/docusign` → list of DocuSign templates - `GET /api/templates/status` → merged view: each Adobe template tagged as: - `not_migrated` — no DocuSign template with same name - `migrated` — at least one DocuSign template with exact name match - `needs_update` — Adobe template modified after the matched DocuSign template **Tests (`tests/test_api_templates.py`):** - `test_adobe_list_requires_auth` — 401 if not authenticated. - `test_adobe_list_returns_templates` — mock Adobe API; correct shape. - `test_docusign_list_returns_templates` — mock DocuSign API. - `test_status_not_migrated` — Adobe template with no DS match → `not_migrated`. - `test_status_migrated` — name match exists → `migrated`. - `test_status_needs_update` — Adobe modified after DS template → `needs_update`. --- ## Phase 5 — Migration API **Goal:** Trigger migration of selected Adobe templates and retrieve history. **Endpoints:** - `POST /api/migrate` — body: `{"adobe_template_ids": ["id1", "id2"]}` - Downloads each template, runs `compose_docusign_template.py`, uploads via upsert - Returns `{"results": [{"adobe_id": "...", "docusign_id": "...", "status": "created|updated|failed", "error": null}]}` - `GET /api/migrate/history` — reads `migration-output/.history.json`; returns past runs **History record schema:** ```json { "timestamp": "2026-04-17T10:30:00Z", "adobe_template_name": "NDA", "adobe_template_id": "CBJ...", "docusign_template_id": "7dfd...", "action": "created|updated", "status": "success|failed", "error": null } ``` **Tests (`tests/test_api_migrate.py`):** - `test_migrate_requires_auth` — 401 if not authenticated. - `test_migrate_single_template_creates` — mock Adobe download + DS upload (no existing); returns created. - `test_migrate_single_template_updates` — mock with existing DS template; returns updated. - `test_migrate_records_history` — after run, history file updated. - `test_history_returns_past_runs` — GET history returns written records. - `test_migrate_handles_partial_failure` — one template fails; others succeed; partial results returned. --- ## Phase 6 — Frontend **Goal:** Single-page app served at `/`; no build step. **Layout:** ``` ┌─────────────────────────────────────────────────────┐ │ Adobe Sign → DocuSign Migrator [auth]│ ├───────────────────────┬─────────────────────────────┤ │ Adobe Sign Templates │ DocuSign Templates │ │ [connect] │ [connect] │ │ ───────────────── │ ───────────────────────── │ │ ● NDA [●] │ ● NDA │ │ ○ Sales Agmt [○] │ ● David Tag Demo │ │ ○ Rob Test [○] │ │ │ │ │ │ [Migrate Selected] │ │ └───────────────────────┴─────────────────────────────┘ ``` **Status badges:** - Green dot = Migrated - Yellow dot = Needs Update - Red dot = Not Migrated **Migrate flow:** 1. User checks templates to migrate. 2. Clicks "Migrate Selected." 3. Progress shown inline per template (spinner → check/error). 4. History section at bottom shows past runs. --- ## Phase 7 — End-to-End & Regression Tests **Goal:** Ensure the full pipeline works together and existing behaviour doesn't regress. **`tests/test_e2e.py`:** - `test_full_migration_flow` — using TestClient + respx mocks: 1. Connect Adobe (mock OAuth callback) 2. Connect DocuSign (mock OAuth callback) 3. GET /api/templates/status → at least one `not_migrated` 4. POST /api/migrate → status `created` 5. GET /api/templates/status → now `migrated` 6. POST /api/migrate again → status `updated` 7. GET /api/migrate/history → two entries **`tests/test_regression.py`:** - Runs `compose_docusign_template.py` on all fixtures in `sample-templates/` - Validates output against expected JSON snapshots in `tests/fixtures/expected/` - Any field type regression fails the test - Run via `pytest tests/test_regression.py` — no live API calls needed --- ## Running Tests ```bash # All tests pytest tests/ -v # Unit only (no live API) pytest tests/ -v -m "not integration" # Regression only pytest tests/test_regression.py -v # With coverage pytest tests/ --cov=src --cov=web --cov-report=term-missing ``` --- ## Dependencies to Add ``` fastapi uvicorn[standard] starlette-sessions itsdangerous httpx respx pytest-asyncio pytest-cov ``` --- *Last updated: 2026-04-17*