Enterprise UI redesign — Phases 14–22 (Docusign-branded migration console) #1
|
|
@ -0,0 +1,133 @@
|
||||||
|
"""
|
||||||
|
tests/test_api_verify.py
|
||||||
|
------------------------
|
||||||
|
Tests for /api/verify/* endpoints (send test envelope, status, void).
|
||||||
|
All DocuSign API calls are mocked with respx.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
import httpx
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from web.app import app
|
||||||
|
from web.session import _serializer, _COOKIE_NAME
|
||||||
|
|
||||||
|
client = TestClient(app, raise_server_exceptions=True)
|
||||||
|
|
||||||
|
DS_BASE = "https://demo.docusign.net/restapi"
|
||||||
|
DS_ACCOUNT = "verify-account-id"
|
||||||
|
TEMPLATE_ID = "tpl-verify-001"
|
||||||
|
ENVELOPE_ID = "env-abc-123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def patch_settings(monkeypatch):
|
||||||
|
import web.config as cfg
|
||||||
|
monkeypatch.setattr(cfg.settings, "docusign_account_id", DS_ACCOUNT)
|
||||||
|
monkeypatch.setattr(cfg.settings, "docusign_base_url", DS_BASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _full_session():
|
||||||
|
return _serializer.dumps({
|
||||||
|
"adobe_access_token": "adobe-tok",
|
||||||
|
"docusign_access_token": "ds-tok",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _ds_session():
|
||||||
|
return _serializer.dumps({"docusign_access_token": "ds-tok"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerifySend:
|
||||||
|
def test_send_requires_auth(self):
|
||||||
|
"""No session → 401."""
|
||||||
|
resp = client.post(
|
||||||
|
"/api/verify/send",
|
||||||
|
json={"template_id": TEMPLATE_ID, "recipient_name": "Alice", "recipient_email": "alice@example.com"},
|
||||||
|
cookies={},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_send_returns_envelope_id(self):
|
||||||
|
"""Authenticated + valid template → envelope_id returned."""
|
||||||
|
respx.post(
|
||||||
|
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes"
|
||||||
|
).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID}))
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/api/verify/send",
|
||||||
|
json={
|
||||||
|
"template_id": TEMPLATE_ID,
|
||||||
|
"recipient_name": "Alice Test",
|
||||||
|
"recipient_email": "alice@example.com",
|
||||||
|
},
|
||||||
|
cookies={_COOKIE_NAME: _ds_session()},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["envelope_id"] == ENVELOPE_ID
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_send_propagates_docusign_error(self):
|
||||||
|
"""DocuSign 400 → 502 with error detail."""
|
||||||
|
respx.post(
|
||||||
|
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes"
|
||||||
|
).mock(return_value=httpx.Response(400, json={"message": "Invalid templateId"}))
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/api/verify/send",
|
||||||
|
json={"template_id": "bad-id", "recipient_name": "X", "recipient_email": "x@x.com"},
|
||||||
|
cookies={_COOKIE_NAME: _ds_session()},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 502
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerifyStatus:
|
||||||
|
def test_status_requires_auth(self):
|
||||||
|
resp = client.get(f"/api/verify/status/{ENVELOPE_ID}", cookies={})
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_status_returns_envelope_state(self):
|
||||||
|
"""Authenticated → status and sent_at returned."""
|
||||||
|
respx.get(
|
||||||
|
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes/{ENVELOPE_ID}"
|
||||||
|
).mock(return_value=httpx.Response(200, json={
|
||||||
|
"envelopeId": ENVELOPE_ID,
|
||||||
|
"status": "sent",
|
||||||
|
"sentDateTime": "2026-04-21T12:00:00Z",
|
||||||
|
"completedDateTime": None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
f"/api/verify/status/{ENVELOPE_ID}",
|
||||||
|
cookies={_COOKIE_NAME: _ds_session()},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["status"] == "sent"
|
||||||
|
assert data["envelope_id"] == ENVELOPE_ID
|
||||||
|
assert data["sent_at"] == "2026-04-21T12:00:00Z"
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerifyVoid:
|
||||||
|
def test_void_requires_auth(self):
|
||||||
|
resp = client.post(f"/api/verify/void/{ENVELOPE_ID}", json={"reason": "test"}, cookies={})
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_void_calls_docusign(self):
|
||||||
|
"""Authenticated → PUT envelope status to voided → voided: true."""
|
||||||
|
respx.put(
|
||||||
|
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes/{ENVELOPE_ID}"
|
||||||
|
).mock(return_value=httpx.Response(200, json={}))
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/api/verify/void/{ENVELOPE_ID}",
|
||||||
|
json={"reason": "Verification complete"},
|
||||||
|
cookies={_COOKIE_NAME: _ds_session()},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["voided"] is True
|
||||||
|
assert resp.json()["envelope_id"] == ENVELOPE_ID
|
||||||
|
|
@ -15,7 +15,7 @@ from fastapi.responses import FileResponse
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from web.config import settings
|
from web.config import settings
|
||||||
from web.routers import auth, templates, migrate
|
from web.routers import auth, templates, migrate, verify
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Adobe Sign → DocuSign Migrator",
|
title="Adobe Sign → DocuSign Migrator",
|
||||||
|
|
@ -24,9 +24,10 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Routers
|
# Routers
|
||||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||||
app.include_router(templates.router, prefix="/api/templates", tags=["templates"])
|
app.include_router(templates.router, prefix="/api/templates", tags=["templates"])
|
||||||
app.include_router(migrate.router, prefix="/api/migrate", tags=["migrate"])
|
app.include_router(migrate.router, prefix="/api/migrate", tags=["migrate"])
|
||||||
|
app.include_router(verify.router, prefix="/api/verify", tags=["verify"])
|
||||||
|
|
||||||
# Static files (frontend)
|
# Static files (frontend)
|
||||||
_static_dir = os.path.join(os.path.dirname(__file__), "static")
|
_static_dir = os.path.join(os.path.dirname(__file__), "static")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"templateId": body.template_id,
|
||||||
|
"status": "sent",
|
||||||
|
"templateRoles": [
|
||||||
|
{
|
||||||
|
"email": body.recipient_email,
|
||||||
|
"name": body.recipient_name,
|
||||||
|
"roleName": "Signer",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emailSubject": f"[Verification Test] Please sign this document",
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{settings.docusign_base_url}/v2.1/accounts/{settings.docusign_account_id}/envelopes",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {session['docusign_access_token']}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
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")}
|
||||||
|
|
||||||
|
|
||||||
|
@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}
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
// Verification view — send test envelopes to confirm migrated templates work
|
||||||
|
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { state } from './state.js';
|
||||||
|
import { escHtml, formatDateTime } from './utils.js';
|
||||||
|
|
||||||
|
const POLL_MS = 5000;
|
||||||
|
const _envelopes = {}; // { adobeId: { envelopeId, status, sentAt, completedAt, polling } }
|
||||||
|
|
||||||
|
function getSettings() {
|
||||||
|
try { return JSON.parse(localStorage.getItem('migrator_settings')) || {}; }
|
||||||
|
catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderVerification(preloadedIds = null) {
|
||||||
|
const outlet = document.getElementById('router-outlet');
|
||||||
|
const ids = preloadedIds || state.verifyIds || null;
|
||||||
|
|
||||||
|
// Candidate templates: recently migrated (from state) or all migrated
|
||||||
|
const candidates = (state.templates || []).filter(t =>
|
||||||
|
t.status === 'migrated' || t.status === 'needs_update' || t.docusign_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!state.auth.docusign) {
|
||||||
|
outlet.innerHTML = `
|
||||||
|
<div class="page-header"><div><div class="page-title">Verification</div></div></div>
|
||||||
|
<div class="callout info"><span class="callout-icon">ℹ️</span>Connect DocuSign to send verification envelopes.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidates.length) {
|
||||||
|
outlet.innerHTML = `
|
||||||
|
<div class="page-header"><div><div class="page-title">Verification</div></div></div>
|
||||||
|
<div class="callout info"><span class="callout-icon">ℹ️</span>
|
||||||
|
No migrated templates yet. Run a migration first, then return here to verify.
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderVerifyView(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderVerifyView(candidates) {
|
||||||
|
const outlet = document.getElementById('router-outlet');
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
outlet.innerHTML = `
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<div class="page-title">Verification</div>
|
||||||
|
<div class="page-subtitle">Send test envelopes to confirm templates work end-to-end</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout info" style="margin-bottom:20px">
|
||||||
|
<span class="callout-icon">ℹ️</span>
|
||||||
|
Verification sends a real DocuSign envelope using each template. Test envelopes should be voided after use.
|
||||||
|
Configure default recipient in <a href="#/settings" style="color:var(--cobalt)">Settings</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">Migrated Templates</span>
|
||||||
|
<span style="font-size:12px;color:var(--text-muted)">${candidates.length} templates</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="verify-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Template</th>
|
||||||
|
<th>DocuSign ID</th>
|
||||||
|
<th>Verification Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${candidates.map(t => _verifyRow(t, settings)).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Wire Send Test buttons
|
||||||
|
document.querySelectorAll('.btn-send-test').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const adobeId = btn.dataset.id;
|
||||||
|
const t = candidates.find(t => t.adobe_id === adobeId);
|
||||||
|
if (t) _showSendDialog(t, settings);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wire Void buttons
|
||||||
|
document.querySelectorAll('.btn-void-envelope').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
_voidEnvelope(btn.dataset.id, btn.dataset.envelopeid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _verifyRow(t, settings) {
|
||||||
|
const env = _envelopes[t.adobe_id];
|
||||||
|
let statusCell = '<span class="badge badge-gray">Not Tested</span>';
|
||||||
|
let actionsCell = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(t.adobe_id)}">Send Test</button>`;
|
||||||
|
|
||||||
|
if (env) {
|
||||||
|
if (env.status === 'completed') {
|
||||||
|
statusCell = '<span class="badge badge-green">✓ Verified</span>';
|
||||||
|
actionsCell = `<button class="btn btn-secondary btn-xs btn-void-envelope"
|
||||||
|
data-id="${escHtml(t.adobe_id)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
|
||||||
|
} else if (env.status === 'sent' || env.status === 'delivered') {
|
||||||
|
statusCell = `<span class="badge badge-blue"><span class="spinner spinner-sm"></span> ${env.status}</span>`;
|
||||||
|
actionsCell = `<button class="btn btn-secondary btn-xs btn-void-envelope"
|
||||||
|
data-id="${escHtml(t.adobe_id)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
|
||||||
|
} else if (env.status === 'voided') {
|
||||||
|
statusCell = '<span class="badge badge-gray">Voided</span>';
|
||||||
|
actionsCell = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(t.adobe_id)}">Send Again</button>`;
|
||||||
|
} else {
|
||||||
|
statusCell = `<span class="badge badge-amber">${escHtml(env.status || 'pending')}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr id="verify-row-${escHtml(t.adobe_id)}">
|
||||||
|
<td>
|
||||||
|
<div class="table-name">${escHtml(t.name)}</div>
|
||||||
|
<div class="table-sub">${escHtml(t.adobe_id)}</div>
|
||||||
|
</td>
|
||||||
|
<td class="mono" style="font-size:11px">${t.docusign_id ? escHtml(t.docusign_id.slice(0,12)) + '…' : '—'}</td>
|
||||||
|
<td id="verify-status-${escHtml(t.adobe_id)}">${statusCell}</td>
|
||||||
|
<td id="verify-actions-${escHtml(t.adobe_id)}">${actionsCell}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showSendDialog(t, settings) {
|
||||||
|
const existing = document.getElementById('send-dialog');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.id = 'send-dialog';
|
||||||
|
wrapper.innerHTML = `
|
||||||
|
<div class="modal-backdrop"></div>
|
||||||
|
<div class="modal-box modal-sm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">Send Test Envelope</span>
|
||||||
|
<button class="modal-close" id="sd-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div style="font-size:13px;color:var(--text-muted);margin-bottom:14px">
|
||||||
|
Template: <strong>${escHtml(t.name)}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="sd-name">Recipient Name</label>
|
||||||
|
<input type="text" class="form-input" id="sd-name"
|
||||||
|
value="${escHtml(settings.testRecipientName || '')}"
|
||||||
|
placeholder="Test Recipient" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="sd-email">Recipient Email</label>
|
||||||
|
<input type="email" class="form-input" id="sd-email"
|
||||||
|
value="${escHtml(settings.testRecipientEmail || '')}"
|
||||||
|
placeholder="test@example.com" />
|
||||||
|
<div class="form-error" id="sd-error"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" id="sd-cancel">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="sd-send">Send Test →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(wrapper);
|
||||||
|
|
||||||
|
document.getElementById('sd-close').onclick = () => wrapper.remove();
|
||||||
|
document.getElementById('sd-cancel').onclick = () => wrapper.remove();
|
||||||
|
document.getElementById('sd-send').onclick = () => _sendEnvelope(t, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _sendEnvelope(t, wrapper) {
|
||||||
|
const name = document.getElementById('sd-name')?.value.trim();
|
||||||
|
const email = document.getElementById('sd-email')?.value.trim();
|
||||||
|
const errorEl = document.getElementById('sd-error');
|
||||||
|
|
||||||
|
if (!name || !email) {
|
||||||
|
if (errorEl) errorEl.textContent = 'Recipient name and email are required.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorEl) errorEl.textContent = '';
|
||||||
|
|
||||||
|
const sendBtn = document.getElementById('sd-send');
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.textContent = 'Sending…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await api.verify.send(t.docusign_id || t.adobe_id, name, email);
|
||||||
|
wrapper.remove();
|
||||||
|
|
||||||
|
_envelopes[t.adobe_id] = { envelopeId: data.envelope_id, status: 'sent', sentAt: new Date().toISOString() };
|
||||||
|
_updateVerifyRow(t.adobe_id);
|
||||||
|
_startPolling(t.adobe_id, data.envelope_id);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (errorEl) errorEl.textContent = 'Send failed: ' + e.message;
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.textContent = 'Send Test →';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _startPolling(adobeId, envelopeId) {
|
||||||
|
const env = _envelopes[adobeId];
|
||||||
|
if (!env || env.polling) return;
|
||||||
|
env.polling = true;
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.verify.status(envelopeId);
|
||||||
|
if (_envelopes[adobeId]) {
|
||||||
|
_envelopes[adobeId].status = data.status;
|
||||||
|
_envelopes[adobeId].completedAt = data.completed_at;
|
||||||
|
_updateVerifyRow(adobeId);
|
||||||
|
if (data.status !== 'completed' && data.status !== 'voided') {
|
||||||
|
setTimeout(poll, POLL_MS);
|
||||||
|
} else {
|
||||||
|
_envelopes[adobeId].polling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Polling error:', e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(poll, POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateVerifyRow(adobeId) {
|
||||||
|
const t = (state.templates || []).find(t => t.adobe_id === adobeId);
|
||||||
|
const env = _envelopes[adobeId];
|
||||||
|
if (!t || !env) return;
|
||||||
|
|
||||||
|
const statusEl = document.getElementById(`verify-status-${adobeId}`);
|
||||||
|
const actionsEl = document.getElementById(`verify-actions-${adobeId}`);
|
||||||
|
if (!statusEl) return;
|
||||||
|
|
||||||
|
if (env.status === 'completed') {
|
||||||
|
statusEl.innerHTML = '<span class="badge badge-green">✓ Verified</span>';
|
||||||
|
actionsEl.innerHTML = `<button class="btn btn-secondary btn-xs btn-void-envelope"
|
||||||
|
data-id="${escHtml(adobeId)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
|
||||||
|
} else if (env.status === 'sent' || env.status === 'delivered') {
|
||||||
|
statusEl.innerHTML = `<span class="badge badge-blue"><span class="spinner spinner-sm"></span> ${env.status}</span>`;
|
||||||
|
actionsEl.innerHTML = `<button class="btn btn-secondary btn-xs btn-void-envelope"
|
||||||
|
data-id="${escHtml(adobeId)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
|
||||||
|
} else if (env.status === 'voided') {
|
||||||
|
statusEl.innerHTML = '<span class="badge badge-gray">Voided</span>';
|
||||||
|
actionsEl.innerHTML = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(adobeId)}">Send Again</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-wire newly injected buttons
|
||||||
|
actionsEl.querySelectorAll('.btn-void-envelope').forEach(btn => {
|
||||||
|
btn.onclick = () => _voidEnvelope(btn.dataset.id, btn.dataset.envelopeid);
|
||||||
|
});
|
||||||
|
actionsEl.querySelectorAll('.btn-send-test').forEach(btn => {
|
||||||
|
btn.onclick = () => {
|
||||||
|
const settings = getSettings();
|
||||||
|
_showSendDialog(t, settings);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _voidEnvelope(adobeId, envelopeId) {
|
||||||
|
if (!confirm('Void this test envelope?')) return;
|
||||||
|
try {
|
||||||
|
await api.verify.void(envelopeId);
|
||||||
|
if (_envelopes[adobeId]) {
|
||||||
|
_envelopes[adobeId].status = 'voided';
|
||||||
|
_envelopes[adobeId].polling = false;
|
||||||
|
}
|
||||||
|
_updateVerifyRow(adobeId);
|
||||||
|
} catch (e) {
|
||||||
|
alert('Failed to void envelope: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue