feat(documenso): v2 coverage on getDocument/health + reminder webhook + admin UI benefits panel
- documenso-client.ts: getDocument now routes to /api/v2/envelope/{id} when port apiVersion=v2; checkDocumensoHealth surfaces resolved apiVersion for the admin Test button
- webhook route: handle DOCUMENT_REMINDER_SENT (structured log only, no audit-table noise) + DOCUMENT_CREATED / DOCUMENT_SENT (informational log)
- Admin Documenso page: prominent v1-vs-v2 explainer card listing v2-only capabilities the CRM already exploits (bulk fields, percent coords, richer fieldMeta, v2 webhook aliases, envelope endpoints) + amber roadmap callout for sequential signing / redirectUrl / template/use / envelope/update / non-SIGNER roles
- CLAUDE.md: idempotency + v2 webhook event list, berth-rules engine section, DOCUMENSO_API_URL gotcha, storage backend listByPrefix + timeout
Still v1-only (call out in admin UI roadmap): createDocument, generateDocumentFromTemplate, sendDocument, sendReminder, downloadSignedPdf. Migrating template/use to v2 requires per-template field-ID mapping in template config; deferred to a follow-up plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import { CheckCircle2, Info } from 'lucide-react';
|
||||
|
||||
import {
|
||||
SettingsFormCard,
|
||||
type SettingFieldDef,
|
||||
} from '@/components/admin/shared/settings-form-card';
|
||||
import { DocumensoTestButton } from '@/components/admin/documenso/documenso-test-button';
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
const API_FIELDS: SettingFieldDef[] = [
|
||||
{
|
||||
key: 'documenso_api_url_override',
|
||||
label: 'API URL override',
|
||||
description: 'Optional. Falls back to DOCUMENSO_API_URL env when blank.',
|
||||
description:
|
||||
'Optional. Falls back to DOCUMENSO_API_URL env when blank. Bare host only — never include /api/v1; the client appends versioned paths based on the API version below.',
|
||||
type: 'string',
|
||||
placeholder: 'https://documenso.example.com',
|
||||
defaultValue: '',
|
||||
@@ -25,11 +29,11 @@ const API_FIELDS: SettingFieldDef[] = [
|
||||
key: 'documenso_api_version_override',
|
||||
label: 'API version',
|
||||
description:
|
||||
'Which Documenso REST API to call against this port. v1 supports Documenso 1.x (per-field PIXEL placement, /api/v1/templates and /api/v1/documents). v2 unlocks the envelope/embed endpoints introduced in Documenso 2.x. Use the test-connection button below after switching to confirm the chosen version actually works against this port’s instance.',
|
||||
"Which Documenso REST API this port targets. v1 = Documenso 1.13.x stable. v2 = Documenso 2.x with the envelope model and richer per-field metadata. Test the connection after switching. See the v2 benefits card above for what changes when you flip this — and note that template-based EOI generation still uses the v1 formValues shape regardless of this setting (v2 template/use migration is on the roadmap).",
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'v1', label: 'v1 — Documenso 1.x (legacy stable)' },
|
||||
{ value: 'v2', label: 'v2 — Documenso 2.x (envelope + embedded signing)' },
|
||||
{ value: 'v1', label: 'v1 — Documenso 1.13.x (default, stable)' },
|
||||
{ value: 'v2', label: 'v2 — Documenso 2.x (envelope, recommended for new ports)' },
|
||||
],
|
||||
defaultValue: 'v1',
|
||||
},
|
||||
@@ -148,6 +152,108 @@ export default function DocumensoSettingsPage() {
|
||||
description="API credentials, signer identities, and document generation behaviour. Use the test-connection button to verify a saved configuration before relying on it."
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Info className="h-4 w-4" aria-hidden="true" />
|
||||
v1 vs v2 — what changes when you flip the API version
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 text-sm">
|
||||
<p className="text-muted-foreground">
|
||||
The CRM supports both Documenso 1.13.x (v1) and 2.x (v2). v1 is the default for
|
||||
backwards compatibility. v2 is recommended for new ports and unlocks the features
|
||||
below. Switching versions does <strong>not</strong> require any code changes —
|
||||
version-aware client methods pick the right endpoint per port. Switch, save, then run
|
||||
the test-connection button to confirm the chosen instance is actually on the matching
|
||||
Documenso version.
|
||||
</p>
|
||||
|
||||
<div className="rounded-md border border-border bg-muted/40 p-3">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
v2-only capabilities the CRM already uses when you pick v2
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600" aria-hidden="true" />
|
||||
<span>
|
||||
<strong>Bulk field placement.</strong> One API call per envelope vs. v1's
|
||||
per-field POST loop. Faster contract generation, fewer transient retries on
|
||||
multi-field uploaded contracts.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600" aria-hidden="true" />
|
||||
<span>
|
||||
<strong>Percent-based field coordinates.</strong> No page-dimension lookup
|
||||
needed — coordinates are portable across page sizes. v1 requires us to assume A4
|
||||
for auto-placed fields.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600" aria-hidden="true" />
|
||||
<span>
|
||||
<strong>Richer field metadata.</strong> TEXT labels & required flags, NUMBER
|
||||
min/max + format, CHECKBOX/DROPDOWN/RADIO option lists with defaults — all
|
||||
ignored by v1, surfaced by v2 in the signing UI.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600" aria-hidden="true" />
|
||||
<span>
|
||||
<strong>v2-flavoured webhook events.</strong> <code>RECIPIENT_VIEWED</code>,{' '}
|
||||
<code>RECIPIENT_SIGNED</code>, <code>DOCUMENT_RECIPIENT_COMPLETED</code>,{' '}
|
||||
<code>DOCUMENT_DECLINED</code>, <code>DOCUMENT_REMINDER_SENT</code> — all routed
|
||||
through the same dedup + audit pipeline as v1 events.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-600" aria-hidden="true" />
|
||||
<span>
|
||||
<strong>Envelope/embed endpoints.</strong> <code>GET</code> and{' '}
|
||||
<code>DELETE</code> go through <code>/api/v2/envelope/...</code> when v2 is
|
||||
selected. Future embedded-signing iframe work will plug in here.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50/60 p-3 dark:border-amber-900/40 dark:bg-amber-950/30">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
|
||||
v2 capabilities on the roadmap (not yet wired)
|
||||
</p>
|
||||
<ul className="space-y-1.5 text-muted-foreground">
|
||||
<li>
|
||||
<strong>Sequential signing</strong> (<code>signingOrder: SEQUENTIAL</code>) — would
|
||||
force client → developer → approver order on EOIs instead of all-at-once.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Post-signing redirect URL</strong> (<code>redirectUrl</code>) — would land
|
||||
signed clients back on the portal rather than Documenso's page.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Single-shot <code>/template/use</code></strong> (v2 prefillFields by ID
|
||||
replacing v1 formValues by name) — currently the EOI flow still uses the v1
|
||||
template path even when API version is v2. Needs per-template field-ID mapping in
|
||||
the template config before we can switch.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Update envelope metadata</strong> (<code>/envelope/update</code>) — change
|
||||
title / subject / redirectUrl after creation without re-generating.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Recipient roles beyond SIGNER</strong> (APPROVER / CC / VIEWER) — would let
|
||||
sales managers receive copies without a signature slot.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
These items have no admin setting yet because they need code changes first. They
|
||||
live here so you know what's in the pipeline.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SettingsFormCard
|
||||
title="Documenso API"
|
||||
description="Per-port API credentials. Leave blank to use the global env defaults."
|
||||
|
||||
@@ -235,6 +235,29 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
await handleDocumentExpired({ documentId: documensoId, ...portScope });
|
||||
break;
|
||||
|
||||
case 'DOCUMENT_REMINDER_SENT':
|
||||
// Documenso auto-reminded a recipient. We don't mutate state — the
|
||||
// reminder is informational. Structured log line is enough for
|
||||
// telemetry without polluting the audit_logs table on every
|
||||
// auto-reminder Documenso sends across all ports.
|
||||
logger.info(
|
||||
{
|
||||
documensoId,
|
||||
recipients: recipients.map((r) => r.email),
|
||||
...portScope,
|
||||
},
|
||||
'Documenso auto-reminder sent',
|
||||
);
|
||||
break;
|
||||
|
||||
case 'DOCUMENT_CREATED':
|
||||
case 'DOCUMENT_SENT':
|
||||
// Created + sent are informational — we initiated these from our
|
||||
// side so the state is already authoritative in our DB. Log for
|
||||
// forward-compat / out-of-band-creation telemetry.
|
||||
logger.info({ event, documensoId, ...portScope }, 'Documenso lifecycle event');
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.info({ event }, 'Unhandled Documenso webhook event type');
|
||||
}
|
||||
|
||||
@@ -229,7 +229,12 @@ export async function sendDocument(docId: string, portId?: string): Promise<Docu
|
||||
}
|
||||
|
||||
export async function getDocument(docId: string, portId?: string): Promise<DocumensoDocument> {
|
||||
return documensoFetch(`/api/v1/documents/${docId}`, undefined, portId).then(normalizeDocument);
|
||||
const { apiVersion } = await resolveCreds(portId);
|
||||
// v1: GET /api/v1/documents/{id}
|
||||
// v2: GET /api/v2/envelope/{id} — same response normalizer (id ↔ documentId,
|
||||
// recipientId ↔ id handled by normalizeDocument).
|
||||
const path = apiVersion === 'v2' ? `/api/v2/envelope/${docId}` : `/api/v1/documents/${docId}`;
|
||||
return documensoFetch(path, undefined, portId).then(normalizeDocument);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,13 +300,16 @@ export async function downloadSignedPdf(docId: string, portId?: string): Promise
|
||||
/** Convenience health-check used by the admin "Test connection" button. */
|
||||
export async function checkDocumensoHealth(
|
||||
portId?: string,
|
||||
): Promise<{ ok: boolean; status?: number; error?: string }> {
|
||||
): Promise<{ ok: boolean; status?: number; error?: string; apiVersion?: DocumensoApiVersion }> {
|
||||
try {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
const { baseUrl, apiKey, apiVersion } = await resolveCreds(portId);
|
||||
// Both v1 and v2 expose /api/v1/health (v2 keeps the v1 path for
|
||||
// backward compat). If a v2 deployment ever moves this we'll add a
|
||||
// v2 branch — but as of Documenso 2.x there isn't a v2 health path.
|
||||
const res = await fetchWithTimeout(`${baseUrl}/api/v1/health`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
return { ok: res.ok, status: res.status };
|
||||
return { ok: res.ok, status: res.status, apiVersion };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : 'Unknown error' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user