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); + }); +});