""" web/routers/verify.py --------------------- Verification endpoints: send test envelopes, poll status, void. Uses DocuSign Envelopes API to confirm migrated templates work end-to-end. """ from typing import Optional import httpx from fastapi import APIRouter, Request from fastapi.responses import JSONResponse from pydantic import BaseModel from web.config import settings from web.session import get_session router = APIRouter() class SendRequest(BaseModel): template_id: str recipient_name: str recipient_email: str class VoidRequest(BaseModel): reason: str = "Test envelope — voided after verification" def _require_docusign(session: dict) -> Optional[JSONResponse]: if not session.get("docusign_access_token"): return JSONResponse({"error": "not authenticated to DocuSign"}, status_code=401) return None @router.post("/send") async def send_test_envelope(body: SendRequest, request: Request): """Send a test envelope using a migrated DocuSign template.""" session = get_session(request) err = _require_docusign(session) if err: return err headers = { "Authorization": f"Bearer {session['docusign_access_token']}", "Content-Type": "application/json", } base = f"{settings.docusign_base_url}/v2.1/accounts/{settings.docusign_account_id}" async with httpx.AsyncClient() as client: # Fetch template to discover actual role names tpl_resp = await client.get(f"{base}/templates/{body.template_id}", headers=headers) role_names = [] if tpl_resp.is_success: tpl = tpl_resp.json() recipients = tpl.get("recipients", {}) for group in recipients.values(): if isinstance(group, list): for r in group: rn = r.get("roleName") if rn and rn not in role_names: role_names.append(rn) # Fall back to generic role name if template fetch failed if not role_names: role_names = ["Signer"] template_roles = [ {"email": body.recipient_email, "name": body.recipient_name, "roleName": rn} for rn in role_names ] payload = { "templateId": body.template_id, "status": "sent", "templateRoles": template_roles, "emailSubject": "[Verification Test] Please sign this document", } resp = await client.post(f"{base}/envelopes", headers=headers, json=payload) if not resp.is_success: return JSONResponse( {"error": "DocuSign API error", "detail": resp.text}, status_code=502, ) data = resp.json() return {"envelope_id": data.get("envelopeId"), "roles": role_names} @router.get("/status/{envelope_id}") async def envelope_status(envelope_id: str, request: Request): """Get the current status of a test envelope.""" session = get_session(request) err = _require_docusign(session) if err: return err async with httpx.AsyncClient() as client: resp = await client.get( f"{settings.docusign_base_url}/v2.1/accounts/{settings.docusign_account_id}/envelopes/{envelope_id}", headers={"Authorization": f"Bearer {session['docusign_access_token']}"}, ) if not resp.is_success: return JSONResponse( {"error": "DocuSign API error", "detail": resp.text}, status_code=502, ) data = resp.json() return { "envelope_id": envelope_id, "status": data.get("status"), "completed_at": data.get("completedDateTime"), "sent_at": data.get("sentDateTime"), } @router.post("/void/{envelope_id}") async def void_envelope(envelope_id: str, body: VoidRequest, request: Request): """Void a test envelope after verification is complete.""" session = get_session(request) err = _require_docusign(session) if err: return err async with httpx.AsyncClient() as client: resp = await client.put( f"{settings.docusign_base_url}/v2.1/accounts/{settings.docusign_account_id}/envelopes/{envelope_id}", headers={ "Authorization": f"Bearer {session['docusign_access_token']}", "Content-Type": "application/json", }, json={"status": "voided", "voidedReason": body.reason}, ) if not resp.is_success: return JSONResponse( {"error": "DocuSign API error", "detail": resp.text}, status_code=502, ) return {"voided": True, "envelope_id": envelope_id}