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:
2026-05-11 14:24:40 +02:00
parent 1f41f8a8a0
commit ad312df8a4
4 changed files with 152 additions and 10 deletions

View File

@@ -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 ports 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&apos;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 &amp; 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&apos;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&apos;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."

View File

@@ -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');
}