From 0b91d33bfb2921a480daababe0973efc26b76315 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 7 Nov 2025 10:35:59 +0200 Subject: [PATCH] chore: enhance recipient handling and streamline placeholder processing in PDF fields --- .../lib/server-only/pdf/auto-place-fields.ts | 94 ++++++------------- packages/lib/server-only/pdf/helpers.ts | 80 +++++++++++++++- 2 files changed, 102 insertions(+), 72 deletions(-) diff --git a/packages/lib/server-only/pdf/auto-place-fields.ts b/packages/lib/server-only/pdf/auto-place-fields.ts index 75d3f9413..9b16dd953 100644 --- a/packages/lib/server-only/pdf/auto-place-fields.ts +++ b/packages/lib/server-only/pdf/auto-place-fields.ts @@ -12,12 +12,18 @@ import { prisma } from '@documenso/prisma'; import { getPageSize } from './get-page-size'; import { - createRecipientsFromPlaceholders, + determineRecipientsForPlaceholders, extractRecipientPlaceholder, + findRecipientByPlaceholder, parseFieldMetaFromPlaceholder, parseFieldTypeFromPlaceholder, } from './helpers'; +const PLACEHOLDER_REGEX = /{{([^}]+)}}/g; +const DEFAULT_FIELD_HEIGHT_PERCENT = 2; +const WIDTH_ADJUSTMENT_FACTOR = 0.1; +const MIN_HEIGHT_THRESHOLD = 0.01; + type TextPosition = { text: string; x: number; @@ -52,16 +58,6 @@ type FieldToCreate = TFieldAndMeta & { height: number; }; -/* - Questions for later: - - Does it handle multi-page PDFs? ✅ YES! ✅ - - Does it handle multiple recipients on the same page? ✅ YES! ✅ - - Does it handle multiple recipients on multiple pages? ✅ YES! ✅ - - What happens with incorrect placeholders? E.g. those containing non-accepted properties. - - The placeholder data is dynamic. How to handle this parsing? Perhaps we need to do it similar to the fieldMeta parsing. ✅ - - Need to handle envelopes with multiple items. ✅ -*/ - export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise => { return new Promise((resolve, reject) => { const parser = new PDFParser(null, true); @@ -114,7 +110,7 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise[]; - - if (recipients && recipients.length > 0) { - createdRecipients = recipients; - } else { - createdRecipients = await createRecipientsFromPlaceholders( - recipientPlaceholders, - envelope, - envelopeId, - userId, - teamId, - requestMetadata, - ); - } + const createdRecipients = await determineRecipientsForPlaceholders( + recipients, + recipientPlaceholders, + envelope, + userId, + teamId, + requestMetadata, + ); const fieldsToCreate: FieldToCreate[] = []; @@ -327,46 +312,21 @@ export const insertFieldsFromPlaceholdersInPDF = async ( const widthPercent = (placeholder.width / placeholder.pageWidth) * 100; const heightPercent = (placeholder.height / placeholder.pageHeight) * 100; - let recipient: Pick | undefined; - - if (recipients && recipients.length > 0) { - /* - Map placeholder by index: r1 -> recipients[0], r2 -> recipients[1], etc. - recipientIndex is 1-based, so we subtract 1 to get the array index. - */ - const { recipientIndex } = extractRecipientPlaceholder(placeholder.recipient); - const recipientArrayIndex = recipientIndex - 1; - - if (recipientArrayIndex < 0 || recipientArrayIndex >= recipients.length) { - throw new AppError(AppErrorCode.INVALID_BODY, { - message: `Recipient placeholder ${placeholder.recipient} (index ${recipientIndex}) is out of range. Provided ${recipients.length} recipient(s).`, - }); - } - - recipient = recipients[recipientArrayIndex]; - } else { - /* - Use email-based matching for placeholder recipients. - */ - const { email } = extractRecipientPlaceholder(placeholder.recipient); - recipient = createdRecipients.find((r) => r.email === email); - } - - if (!recipient) { - throw new AppError(AppErrorCode.INVALID_BODY, { - message: `Could not find recipient ID for placeholder: ${placeholder.placeholder}`, - }); - } - - const recipientId = recipient.id; + const recipient = findRecipientByPlaceholder( + placeholder.recipient, + placeholder.placeholder, + recipients, + createdRecipients, + ); // Default height percentage if too small (use 2% as a reasonable default) - const finalHeightPercent = heightPercent > 0.01 ? heightPercent : 2; + const finalHeightPercent = + heightPercent > MIN_HEIGHT_THRESHOLD ? heightPercent : DEFAULT_FIELD_HEIGHT_PERCENT; fieldsToCreate.push({ ...placeholder.fieldAndMeta, envelopeItemId, - recipientId, + recipientId: recipient.id, pageNumber: placeholder.page, pageX: xPercent, pageY: yPercent, diff --git a/packages/lib/server-only/pdf/helpers.ts b/packages/lib/server-only/pdf/helpers.ts index d8fc414d9..d20730dc2 100644 --- a/packages/lib/server-only/pdf/helpers.ts +++ b/packages/lib/server-only/pdf/helpers.ts @@ -115,10 +115,78 @@ export const extractRecipientPlaceholder = (placeholder: string): RecipientPlace }; }; +/* + Finds a recipient based on a placeholder reference. + If recipients array is provided, uses index-based matching (r1 -> recipients[0], etc.). + Otherwise, uses email-based matching from createdRecipients. +*/ +export const findRecipientByPlaceholder = ( + recipientPlaceholder: string, + placeholder: string, + recipients: Pick[] | undefined, + createdRecipients: Pick[], +): Pick => { + if (recipients && recipients.length > 0) { + /* + Map placeholder by index: r1 -> recipients[0], r2 -> recipients[1], etc. + recipientIndex is 1-based, so we subtract 1 to get the array index. + */ + const { recipientIndex } = extractRecipientPlaceholder(recipientPlaceholder); + const recipientArrayIndex = recipientIndex - 1; + + if (recipientArrayIndex < 0 || recipientArrayIndex >= recipients.length) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: `Recipient placeholder ${recipientPlaceholder} (index ${recipientIndex}) is out of range. Provided ${recipients.length} recipient(s).`, + }); + } + + return recipients[recipientArrayIndex]; + } + + /* + Use email-based matching for placeholder recipients. + */ + const { email } = extractRecipientPlaceholder(recipientPlaceholder); + const recipient = createdRecipients.find((r) => r.email === email); + + if (!recipient) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: `Could not find recipient ID for placeholder: ${placeholder}`, + }); + } + + return recipient; +}; + +/* + Determines the recipients to use for field creation. + If recipients are provided, uses them directly. + Otherwise, creates recipients from placeholders. +*/ +export const determineRecipientsForPlaceholders = async ( + recipients: Pick[] | undefined, + recipientPlaceholders: Map, + envelope: Pick, + userId: number, + teamId: number, + requestMetadata: ApiRequestMetadata, +): Promise[]> => { + if (recipients && recipients.length > 0) { + return recipients; + } + + return createRecipientsFromPlaceholders( + recipientPlaceholders, + envelope, + userId, + teamId, + requestMetadata, + ); +}; + export const createRecipientsFromPlaceholders = async ( recipientPlaceholders: Map, envelope: Pick, - envelopeId: EnvelopeIdOptions, userId: number, teamId: number, requestMetadata: ApiRequestMetadata, @@ -156,6 +224,11 @@ export const createRecipientsFromPlaceholders = async ( const newRecipients = await match(envelope.type) .with(EnvelopeType.DOCUMENT, async () => { + const envelopeId: EnvelopeIdOptions = { + type: 'envelopeId', + id: envelope.id, + }; + const { recipients } = await createDocumentRecipients({ userId, teamId, @@ -167,10 +240,7 @@ export const createRecipientsFromPlaceholders = async ( return recipients; }) .with(EnvelopeType.TEMPLATE, async () => { - const templateId = - envelopeId.type === 'templateId' - ? envelopeId.id - : mapSecondaryIdToTemplateId(envelope.secondaryId ?? ''); + const templateId = mapSecondaryIdToTemplateId(envelope.secondaryId ?? ''); const { recipients } = await createTemplateRecipients({ userId,