10 KiB
Implementation Plan — Adobe → DocuSign Migrator v2
Created: 2026-04-17
Objective
Extend the CLI migration pipeline with:
- Idempotent upload — update the most recently modified DocuSign template with the same name instead of always creating a new one.
- 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:
- After loading the template JSON, extract its
name. - Call
GET /v2.1/accounts/{accountId}/templates?search_text={name}to list matches. - Filter to exact name matches; sort by
lastModifieddescending; take the first. - If found:
PUT /v2.1/accounts/{accountId}/templates/{templateId}(update). - If not found:
POST /v2.1/accounts/{accountId}/templates(create). - Print
Updated template {id}orCreated template {id}. - Add
--force-createflag 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-createalways 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_200test_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 URLGET /api/auth/adobe/callback?code=...→ exchange code for tokens; store in sessionGET /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 URLGET /api/auth/docusign/callback?code=...→ exchange code for tokens; store in sessionGET /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 documentsGET /api/templates/docusign→ list of DocuSign templatesGET /api/templates/status→ merged view: each Adobe template tagged as:not_migrated— no DocuSign template with same namemigrated— at least one DocuSign template with exact name matchneeds_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}]}
- Downloads each template, runs
GET /api/migrate/history— readsmigration-output/.history.json; returns past runs
History record schema:
{
"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:
- User checks templates to migrate.
- Clicks "Migrate Selected."
- Progress shown inline per template (spinner → check/error).
- 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:- Connect Adobe (mock OAuth callback)
- Connect DocuSign (mock OAuth callback)
- GET /api/templates/status → at least one
not_migrated - POST /api/migrate → status
created - GET /api/templates/status → now
migrated - POST /api/migrate again → status
updated - GET /api/migrate/history → two entries
tests/test_regression.py:
- Runs
compose_docusign_template.pyon all fixtures insample-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
# 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