From b37748654e7ff0779e27c43213d7143d805c0a11 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 3 Nov 2025 12:23:11 +0200 Subject: [PATCH] feat: enhance recipient placeholder extraction and management in PDF processing --- .../lib/server-only/pdf/auto-place-fields.ts | 110 +++++++++++------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/packages/lib/server-only/pdf/auto-place-fields.ts b/packages/lib/server-only/pdf/auto-place-fields.ts index 8998057a0..ecd714e04 100644 --- a/packages/lib/server-only/pdf/auto-place-fields.ts +++ b/packages/lib/server-only/pdf/auto-place-fields.ts @@ -13,7 +13,6 @@ import { type TFieldAndMeta, ZFieldAndMetaSchema } from '@documenso/lib/types/fi import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope'; import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; -import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates'; import { prisma } from '@documenso/prisma'; import { getPageSize } from './get-page-size'; @@ -43,6 +42,7 @@ type PlaceholderInfo = { }; type FieldToCreate = TFieldAndMeta & { + envelopeItemId?: string; recipientId: number; pageNumber: number; pageX: number; @@ -51,6 +51,12 @@ type FieldToCreate = TFieldAndMeta & { height: number; }; +type RecipientPlaceholderInfo = { + email: string; + name: string; + recipientIndex: number; +}; + /* Questions for later: - Does it handle multi-page PDFs? ✅ YES! ✅ @@ -316,9 +322,7 @@ export const replacePlaceholdersInPDF = async (pdf: Buffer): Promise => return Buffer.from(modifiedPdfBytes); }; -const extractRecipientPlaceholder = ( - placeholder: string, -): { email: string; recipientIndex: number } => { +const extractRecipientPlaceholder = (placeholder: string): RecipientPlaceholderInfo => { const indexMatch = placeholder.match(/^r(\d+)$/i); if (!indexMatch) { @@ -327,9 +331,12 @@ const extractRecipientPlaceholder = ( }); } + const recipientIndex = Number(indexMatch[1]); + return { - email: `recipient.${indexMatch[1]}@documenso.com`, - recipientIndex: Number(indexMatch[1]), + email: `recipient.${recipientIndex}@documenso.com`, + name: `Recipient ${recipientIndex}`, + recipientIndex, }; }; @@ -339,6 +346,7 @@ export const insertFieldsFromPlaceholdersInPDF = async ( teamId: number, envelopeId: EnvelopeIdOptions, requestMetadata: ApiRequestMetadata, + envelopeItemId?: string, ): Promise => { const placeholders = await extractPlaceholdersFromPDF(pdf); @@ -347,15 +355,15 @@ export const insertFieldsFromPlaceholdersInPDF = async ( } /* - A structure that maps the recipient email to the recipient index. - Example: 'recipient.1@documenso.com' => 1 + A structure that maps the recipient index to the recipient name. + Example: 1 => 'Recipient 1' */ - const recipientEmailToIndex = new Map(); + const recipientPlaceholders = new Map(); for (const placeholder of placeholders) { - const { email, recipientIndex } = extractRecipientPlaceholder(placeholder.recipient); + const { name, recipientIndex } = extractRecipientPlaceholder(placeholder.recipient); - recipientEmailToIndex.set(email, recipientIndex); + recipientPlaceholders.set(recipientIndex, name); } /* @@ -363,13 +371,11 @@ export const insertFieldsFromPlaceholdersInPDF = async ( Example: [{ email: 'recipient.1@documenso.com', name: 'Recipient 1', role: 'SIGNER', signingOrder: 1 }] */ const recipientsToCreate = Array.from( - recipientEmailToIndex.entries(), - ([email, recipientIndex]) => { - const placeholderInfo = generateRecipientPlaceholder(recipientIndex); - + recipientPlaceholders.entries(), + ([recipientIndex, name]) => { return { - email, - name: placeholderInfo.name, + email: `recipient.${recipientIndex}@documenso.com`, + name, role: RecipientRole.SIGNER, signingOrder: recipientIndex, }; @@ -398,36 +404,53 @@ export const insertFieldsFromPlaceholdersInPDF = async ( }); } - let createdRecipients: Pick[]; + const existingRecipients = await prisma.recipient.findMany({ + where: { + envelopeId: envelope.id, + }, + select: { + id: true, + email: true, + }, + }); - if (envelope.type === EnvelopeType.DOCUMENT) { - const { recipients } = await createDocumentRecipients({ - userId, - teamId, - id: envelopeId, - recipients: recipientsToCreate, - requestMetadata, - }); + const existingEmails = new Set(existingRecipients.map((r) => r.email.toLowerCase())); + const recipientsToCreateFiltered = recipientsToCreate.filter( + (r) => !existingEmails.has(r.email.toLowerCase()), + ); - createdRecipients = recipients; - } else if (envelope.type === EnvelopeType.TEMPLATE) { - const templateId = - envelopeId.type === 'templateId' - ? envelopeId.id - : mapSecondaryIdToTemplateId(envelope.secondaryId); + let createdRecipients: Pick[] = existingRecipients; - const { recipients } = await createTemplateRecipients({ - userId, - teamId, - templateId, - recipients: recipientsToCreate, - }); + if (recipientsToCreateFiltered.length > 0) { + if (envelope.type === EnvelopeType.DOCUMENT) { + const { recipients } = await createDocumentRecipients({ + userId, + teamId, + id: envelopeId, + recipients: recipientsToCreateFiltered, + requestMetadata, + }); - createdRecipients = recipients; - } else { - throw new AppError(AppErrorCode.INVALID_BODY, { - message: `Invalid envelope type: ${envelope.type}`, - }); + createdRecipients = [...existingRecipients, ...recipients]; + } else if (envelope.type === EnvelopeType.TEMPLATE) { + const templateId = + envelopeId.type === 'templateId' + ? envelopeId.id + : mapSecondaryIdToTemplateId(envelope.secondaryId); + + const { recipients } = await createTemplateRecipients({ + userId, + teamId, + templateId, + recipients: recipientsToCreateFiltered, + }); + + createdRecipients = [...existingRecipients, ...recipients]; + } else { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: `Invalid envelope type: ${envelope.type}`, + }); + } } const fieldsToCreate: FieldToCreate[] = []; @@ -461,6 +484,7 @@ export const insertFieldsFromPlaceholdersInPDF = async ( fieldsToCreate.push({ ...placeholder.fieldAndMeta, + envelopeItemId, recipientId, pageNumber: placeholder.page, pageX: xPercent,