/** * Small helpers shared between the document-signing flows and the * Documenso webhook handlers. Kept in their own file so the two * heavyweight callers (`documents.service.ts` + the webhook handler * routes) don't have to depend on each other for a 30-line utility. */ import type { DocumentLabel } from '@/lib/services/document-signing-emails.service'; import type { DocumentSigner } from '@/lib/db/schema/documents'; /** * Pull the per-recipient signing token out of a Documenso signing URL. * Both v1.13 and v2 emit URLs of the form * `https://signatures.example.com/sign/` * so the token is the last non-empty path segment. Returns null when * the URL is empty / unparseable / doesn't carry a token-shaped tail. * * Used at document-send time to populate `document_signers.signing_token` * so subsequent webhook deliveries can match recipients by token * (more robust than email match when one address serves multiple roles). */ export function extractSigningToken(url: string | null | undefined): string | null { if (!url) return null; let parsed: URL; try { parsed = new URL(url); } catch { return null; } const segments = parsed.pathname.split('/').filter(Boolean); const last = segments[segments.length - 1]; if (!last) return null; // A token must be at least 8 chars and contain only URL-safe chars — // discriminates real tokens from generic words like "sign" or "embed" // that some Documenso 2.x deployments append. if (last.length < 8) return null; if (!/^[A-Za-z0-9_-]+$/.test(last)) return null; return last; } /** Map of internal documentType to the customer-facing label used in * email subjects + body copy. Duplicated in send-invitation/route.ts * but consolidated here so the webhook cascade picks up the same * label without re-defining the map. */ export const DOC_TYPE_LABEL: Record = { eoi: 'Expression of Interest', contract: 'Sales Contract', reservation_agreement: 'Reservation Agreement', }; /** Pick the next pending signer in signing order. Returns null when * every signer in the list has signed (or declined). */ export function nextPendingSigner>( signers: T[], ): T | null { // signers list is assumed pre-sorted asc by signingOrder; we // defensively pick the lowest-order pending row regardless. const pending = signers.filter((s) => s.status === 'pending'); if (pending.length === 0) return null; return pending.reduce((lo, s) => (s.signingOrder < lo.signingOrder ? s : lo)); }