""" 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.audit import log_event from web.docusign_context import DocusignContextError, current_account 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) try: current_account(session) except DocusignContextError as e: return JSONResponse({"error": str(e), "code": e.code}, status_code=e.status_code) 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 account = current_account(session) headers = { "Authorization": f"Bearer {session['docusign_access_token']}", "Content-Type": "application/json", } base = f"{account['base_url']}/v2.1/accounts/{account['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() log_event( request, session, "verification_sent", { "template_id": body.template_id, "recipient_email": body.recipient_email, "recipient_name": body.recipient_name, "envelope_id": data.get("envelopeId"), }, ) 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 account = current_account(session) async with httpx.AsyncClient() as client: resp = await client.get( f"{account['base_url']}/v2.1/accounts/{account['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 account = current_account(session) async with httpx.AsyncClient() as client: resp = await client.put( f"{account['base_url']}/v2.1/accounts/{account['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, ) log_event( request, session, "verification_voided", {"envelope_id": envelope_id, "reason": body.reason}, ) return {"voided": True, "envelope_id": envelope_id}