154 lines
4.9 KiB
Python
154 lines
4.9 KiB
Python
"""
|
|
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.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()
|
|
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,
|
|
)
|
|
|
|
return {"voided": True, "envelope_id": envelope_id}
|