feat: enhance recipient placeholder extraction and management in PDF processing

This commit is contained in:
Catalin Pit
2025-11-03 12:23:11 +02:00
parent b3ed80d721
commit b37748654e

View File

@ -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 { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope'; import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToTemplateId } 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 { prisma } from '@documenso/prisma';
import { getPageSize } from './get-page-size'; import { getPageSize } from './get-page-size';
@ -43,6 +42,7 @@ type PlaceholderInfo = {
}; };
type FieldToCreate = TFieldAndMeta & { type FieldToCreate = TFieldAndMeta & {
envelopeItemId?: string;
recipientId: number; recipientId: number;
pageNumber: number; pageNumber: number;
pageX: number; pageX: number;
@ -51,6 +51,12 @@ type FieldToCreate = TFieldAndMeta & {
height: number; height: number;
}; };
type RecipientPlaceholderInfo = {
email: string;
name: string;
recipientIndex: number;
};
/* /*
Questions for later: Questions for later:
- Does it handle multi-page PDFs? ✅ YES! ✅ - Does it handle multi-page PDFs? ✅ YES! ✅
@ -316,9 +322,7 @@ export const replacePlaceholdersInPDF = async (pdf: Buffer): Promise<Buffer> =>
return Buffer.from(modifiedPdfBytes); return Buffer.from(modifiedPdfBytes);
}; };
const extractRecipientPlaceholder = ( const extractRecipientPlaceholder = (placeholder: string): RecipientPlaceholderInfo => {
placeholder: string,
): { email: string; recipientIndex: number } => {
const indexMatch = placeholder.match(/^r(\d+)$/i); const indexMatch = placeholder.match(/^r(\d+)$/i);
if (!indexMatch) { if (!indexMatch) {
@ -327,9 +331,12 @@ const extractRecipientPlaceholder = (
}); });
} }
const recipientIndex = Number(indexMatch[1]);
return { return {
email: `recipient.${indexMatch[1]}@documenso.com`, email: `recipient.${recipientIndex}@documenso.com`,
recipientIndex: Number(indexMatch[1]), name: `Recipient ${recipientIndex}`,
recipientIndex,
}; };
}; };
@ -339,6 +346,7 @@ export const insertFieldsFromPlaceholdersInPDF = async (
teamId: number, teamId: number,
envelopeId: EnvelopeIdOptions, envelopeId: EnvelopeIdOptions,
requestMetadata: ApiRequestMetadata, requestMetadata: ApiRequestMetadata,
envelopeItemId?: string,
): Promise<Buffer> => { ): Promise<Buffer> => {
const placeholders = await extractPlaceholdersFromPDF(pdf); const placeholders = await extractPlaceholdersFromPDF(pdf);
@ -347,15 +355,15 @@ export const insertFieldsFromPlaceholdersInPDF = async (
} }
/* /*
A structure that maps the recipient email to the recipient index. A structure that maps the recipient index to the recipient name.
Example: 'recipient.1@documenso.com' => 1 Example: 1 => 'Recipient 1'
*/ */
const recipientEmailToIndex = new Map<string, number>(); const recipientPlaceholders = new Map<number, string>();
for (const placeholder of placeholders) { 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 }] Example: [{ email: 'recipient.1@documenso.com', name: 'Recipient 1', role: 'SIGNER', signingOrder: 1 }]
*/ */
const recipientsToCreate = Array.from( const recipientsToCreate = Array.from(
recipientEmailToIndex.entries(), recipientPlaceholders.entries(),
([email, recipientIndex]) => { ([recipientIndex, name]) => {
const placeholderInfo = generateRecipientPlaceholder(recipientIndex);
return { return {
email, email: `recipient.${recipientIndex}@documenso.com`,
name: placeholderInfo.name, name,
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
signingOrder: recipientIndex, signingOrder: recipientIndex,
}; };
@ -398,36 +404,53 @@ export const insertFieldsFromPlaceholdersInPDF = async (
}); });
} }
let createdRecipients: Pick<Recipient, 'id' | 'email'>[]; const existingRecipients = await prisma.recipient.findMany({
where: {
envelopeId: envelope.id,
},
select: {
id: true,
email: true,
},
});
if (envelope.type === EnvelopeType.DOCUMENT) { const existingEmails = new Set(existingRecipients.map((r) => r.email.toLowerCase()));
const { recipients } = await createDocumentRecipients({ const recipientsToCreateFiltered = recipientsToCreate.filter(
userId, (r) => !existingEmails.has(r.email.toLowerCase()),
teamId, );
id: envelopeId,
recipients: recipientsToCreate,
requestMetadata,
});
createdRecipients = recipients; let createdRecipients: Pick<Recipient, 'id' | 'email'>[] = existingRecipients;
} else if (envelope.type === EnvelopeType.TEMPLATE) {
const templateId =
envelopeId.type === 'templateId'
? envelopeId.id
: mapSecondaryIdToTemplateId(envelope.secondaryId);
const { recipients } = await createTemplateRecipients({ if (recipientsToCreateFiltered.length > 0) {
userId, if (envelope.type === EnvelopeType.DOCUMENT) {
teamId, const { recipients } = await createDocumentRecipients({
templateId, userId,
recipients: recipientsToCreate, teamId,
}); id: envelopeId,
recipients: recipientsToCreateFiltered,
requestMetadata,
});
createdRecipients = recipients; createdRecipients = [...existingRecipients, ...recipients];
} else { } else if (envelope.type === EnvelopeType.TEMPLATE) {
throw new AppError(AppErrorCode.INVALID_BODY, { const templateId =
message: `Invalid envelope type: ${envelope.type}`, 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[] = []; const fieldsToCreate: FieldToCreate[] = [];
@ -461,6 +484,7 @@ export const insertFieldsFromPlaceholdersInPDF = async (
fieldsToCreate.push({ fieldsToCreate.push({
...placeholder.fieldAndMeta, ...placeholder.fieldAndMeta,
envelopeItemId,
recipientId, recipientId,
pageNumber: placeholder.page, pageNumber: placeholder.page,
pageX: xPercent, pageX: xPercent,