From ba128646e16fc0f99d770f812bfc7d867930ed2e Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 28 Jun 2026 18:48:20 +0200 Subject: [PATCH] fix(documenso): skip legacy v1 document IDs in the v2 status poller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The signature-poll worker fetched every sent/partially_signed document via GET /api/v2/envelope/{id}. Pre-cutover documents carry a bare numeric documenso_id (e.g. "46"), which the v2 envelope endpoint rejects with 400 "Invalid envelope ID" — ~36 errors/hour on prod from 3 abandoned June-3 EOIs. Skip documents whose documenso_id isn't an `envelope_xxx` string when the port is on the v2 API (v1-API ports keep polling numeric ids). New v2 signing is unaffected. Pure isPollableDocumensoId() helper, unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01L2qc3xZTfif7N4Wq3QDa8X --- src/jobs/processors/documenso-poll.ts | 40 +++++++++++++++++++++++++++ tests/unit/documenso-poll.test.ts | 21 ++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/unit/documenso-poll.test.ts diff --git a/src/jobs/processors/documenso-poll.ts b/src/jobs/processors/documenso-poll.ts index 73fd1e91..e7df28ce 100644 --- a/src/jobs/processors/documenso-poll.ts +++ b/src/jobs/processors/documenso-poll.ts @@ -9,8 +9,25 @@ import { handleDocumentExpired, handleDocumentRejected, } from '@/lib/services/documents.service'; +import { getPortDocumensoConfig, type DocumensoApiVersion } from '@/lib/services/port-config'; import { logger } from '@/lib/logger'; +/** + * Whether a document's `documensoId` can be fetched under the port's current + * Documenso API version. A v2 envelope id is the public `envelope_xxx` string; + * a legacy v1 id is a bare numeric (e.g. "46"). The v2 `/api/v2/envelope/{id}` + * endpoint rejects numerics with a 400 "Invalid envelope ID", so a port that + * has cut over to v2 must SKIP its old v1 documents in the poll instead of + * erroring on every cycle. Ports still on the v1 API keep polling numeric ids. + */ +export function isPollableDocumensoId( + documensoId: string, + apiVersion: DocumensoApiVersion, +): boolean { + if (apiVersion === 'v2' && !documensoId.startsWith('envelope_')) return false; + return true; +} + export async function processDocumensoPoll(): Promise { // Find all documents that are in-progress signing and have a documensoId const pendingDocs = await db.query.documents.findMany({ @@ -24,9 +41,25 @@ export async function processDocumensoPoll(): Promise { logger.info({ count: pendingDocs.length }, 'Polling Documenso for document statuses'); + // Per-port API version, resolved once per port (cheap memo for the run). + const apiVersionByPort = new Map(); + let skippedLegacy = 0; + for (const doc of pendingDocs) { if (!doc.documensoId) continue; + let apiVersion = apiVersionByPort.get(doc.portId); + if (!apiVersion) { + apiVersion = (await getPortDocumensoConfig(doc.portId)).apiVersion; + apiVersionByPort.set(doc.portId, apiVersion); + } + // Skip legacy v1 documents the port's v2 API can't resolve — otherwise each + // poll cycle 400s ("Invalid envelope ID") on abandoned pre-cutover docs. + if (!isPollableDocumensoId(doc.documensoId, apiVersion)) { + skippedLegacy++; + continue; + } + try { // Pass the doc's portId so the client uses per-port credentials // (admin-set Documenso URL/key/version), not the global env fallback. @@ -112,4 +145,11 @@ export async function processDocumensoPoll(): Promise { ); } } + + if (skippedLegacy > 0) { + logger.info( + { skippedLegacy }, + 'Documenso poll: skipped legacy v1 documents not resolvable on the v2 API', + ); + } } diff --git a/tests/unit/documenso-poll.test.ts b/tests/unit/documenso-poll.test.ts new file mode 100644 index 00000000..fc47a75e --- /dev/null +++ b/tests/unit/documenso-poll.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; + +import { isPollableDocumensoId } from '@/jobs/processors/documenso-poll'; + +describe('isPollableDocumensoId', () => { + it('skips legacy v1 numeric ids when the port is on the v2 API', () => { + // Real prod offenders: abandoned June-3 v1 EOIs whose documenso_id is "46"/"85"/"88". + // The v2 /api/v2/envelope/{id} endpoint rejects bare numerics ("Invalid envelope ID"). + expect(isPollableDocumensoId('46', 'v2')).toBe(false); + expect(isPollableDocumensoId('125', 'v2')).toBe(false); + }); + + it('polls real v2 envelope ids on the v2 API', () => { + expect(isPollableDocumensoId('envelope_ydshkombscbhfnfd', 'v2')).toBe(true); + }); + + it('still polls numeric ids for a port that is on the v1 API', () => { + expect(isPollableDocumensoId('46', 'v1')).toBe(true); + expect(isPollableDocumensoId('envelope_abc', 'v1')).toBe(true); + }); +});