mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
chore: improve recipient creation with pattern matching and helper function
This commit is contained in:
@ -1,33 +1,50 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const PLACEHOLDER_PDF_PATH = path.join(
|
||||
const SINGLE_PLACEHOLDER_PDF_PATH = path.join(
|
||||
__dirname,
|
||||
'../../../assets/project-proposal-single-recipient.pdf',
|
||||
);
|
||||
|
||||
const MULTIPLE_PLACEHOLDER_PDF_PATH = path.join(
|
||||
__dirname,
|
||||
'../../../assets/project-proposal-multiple-fields-and-recipients.pdf',
|
||||
);
|
||||
|
||||
const setupUserAndSignIn = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
return { user, team };
|
||||
};
|
||||
|
||||
const uploadPdfAndContinue = async (page: Page, pdfPath: string, continueClicks: number = 1) => {
|
||||
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||
await fileInput.waitFor({ state: 'attached' });
|
||||
await fileInput.setInputFiles(pdfPath);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
for (let i = 0; i < continueClicks; i++) {
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
};
|
||||
|
||||
test.describe('PDF Placeholders with single recipient', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||
await fileInput.waitFor({ state: 'attached' });
|
||||
await fileInput.setInputFiles(PLACEHOLDER_PDF_PATH);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, SINGLE_PLACEHOLDER_PDF_PATH, 1);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
await expect(page.getByPlaceholder('Email')).toHaveValue('recipient.1@documenso.com');
|
||||
@ -37,22 +54,8 @@ test.describe('PDF Placeholders with single recipient', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically place fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||
await fileInput.waitFor({ state: 'attached' });
|
||||
await fileInput.setInputFiles(PLACEHOLDER_PDF_PATH);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, SINGLE_PLACEHOLDER_PDF_PATH, 2);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
@ -65,22 +68,8 @@ test.describe('PDF Placeholders with single recipient', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically configure fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||
await fileInput.waitFor({ state: 'attached' });
|
||||
await fileInput.setInputFiles(PLACEHOLDER_PDF_PATH);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, SINGLE_PLACEHOLDER_PDF_PATH, 2);
|
||||
|
||||
await page.getByText('Text').nth(1).click();
|
||||
await page.getByRole('button', { name: 'Advanced settings' }).click();
|
||||
@ -96,3 +85,45 @@ test.describe('PDF Placeholders with single recipient', () => {
|
||||
await expect(page.getByRole('combobox')).toHaveText('Right');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PDF Placeholders with multiple recipients', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, MULTIPLE_PLACEHOLDER_PDF_PATH, 1);
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').first()).toHaveValue(
|
||||
'recipient.1@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').first()).toHaveValue('Recipient 1');
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').nth(1)).toHaveValue(
|
||||
'recipient.2@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').nth(1)).toHaveValue('Recipient 2');
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').nth(2)).toHaveValue(
|
||||
'recipient.3@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').nth(2)).toHaveValue('Recipient 3');
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, MULTIPLE_PLACEHOLDER_PDF_PATH, 2);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]').nth(1)).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]').nth(2)).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="EMAIL"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="EMAIL"]').nth(1)).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="NAME"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="TEXT"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="NUMBER"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
|
||||
import { type Envelope, EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
|
||||
import PDFParser from 'pdf2json';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -288,7 +288,7 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
|
||||
});
|
||||
};
|
||||
|
||||
export const replacePlaceholdersInPDF = async (pdf: Buffer): Promise<Buffer> => {
|
||||
export const removePlaceholdersFromPDF = async (pdf: Buffer): Promise<Buffer> => {
|
||||
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||
|
||||
const pdfDoc = await PDFDocument.load(new Uint8Array(pdf));
|
||||
@ -352,6 +352,81 @@ const extractRecipientPlaceholder = (placeholder: string): RecipientPlaceholderI
|
||||
};
|
||||
};
|
||||
|
||||
const createRecipients = async (
|
||||
recipientPlaceholders: Map<number, string>,
|
||||
envelope: Pick<Envelope, 'id' | 'type' | 'secondaryId'>,
|
||||
envelopeId: EnvelopeIdOptions,
|
||||
userId: number,
|
||||
teamId: number,
|
||||
requestMetadata: ApiRequestMetadata,
|
||||
): Promise<Pick<Recipient, 'id' | 'email'>[]> => {
|
||||
const recipientsToCreate = Array.from(
|
||||
recipientPlaceholders.entries(),
|
||||
([recipientIndex, name]) => {
|
||||
return {
|
||||
email: `recipient.${recipientIndex}@documenso.com`,
|
||||
name,
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: recipientIndex,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const existingRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingEmails = new Set(existingRecipients.map((r) => r.email));
|
||||
const recipientsToCreateFiltered = recipientsToCreate.filter(
|
||||
(recipient) => !existingEmails.has(recipient.email),
|
||||
);
|
||||
|
||||
if (recipientsToCreateFiltered.length === 0) {
|
||||
return existingRecipients;
|
||||
}
|
||||
|
||||
const newRecipients = await match(envelope.type)
|
||||
.with(EnvelopeType.DOCUMENT, async () => {
|
||||
const { recipients } = await createDocumentRecipients({
|
||||
userId,
|
||||
teamId,
|
||||
id: envelopeId,
|
||||
recipients: recipientsToCreateFiltered,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
return recipients;
|
||||
})
|
||||
.with(EnvelopeType.TEMPLATE, async () => {
|
||||
const templateId =
|
||||
envelopeId.type === 'templateId'
|
||||
? envelopeId.id
|
||||
: mapSecondaryIdToTemplateId(envelope.secondaryId ?? '');
|
||||
|
||||
const { recipients } = await createTemplateRecipients({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
recipients: recipientsToCreateFiltered,
|
||||
});
|
||||
|
||||
return recipients;
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Invalid envelope type: ${envelope.type}`,
|
||||
});
|
||||
});
|
||||
|
||||
return [...existingRecipients, ...newRecipients];
|
||||
};
|
||||
|
||||
export const insertFieldsFromPlaceholdersInPDF = async (
|
||||
pdf: Buffer,
|
||||
userId: number,
|
||||
@ -359,6 +434,7 @@ export const insertFieldsFromPlaceholdersInPDF = async (
|
||||
envelopeId: EnvelopeIdOptions,
|
||||
requestMetadata: ApiRequestMetadata,
|
||||
envelopeItemId?: string,
|
||||
recipients?: Pick<Recipient, 'id' | 'email'>[],
|
||||
): Promise<Buffer> => {
|
||||
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||
|
||||
@ -378,22 +454,6 @@ export const insertFieldsFromPlaceholdersInPDF = async (
|
||||
recipientPlaceholders.set(recipientIndex, name);
|
||||
}
|
||||
|
||||
/*
|
||||
Create a list of recipients to create.
|
||||
Example: [{ email: 'recipient.1@documenso.com', name: 'Recipient 1', role: 'SIGNER', signingOrder: 1 }]
|
||||
*/
|
||||
const recipientsToCreate = Array.from(
|
||||
recipientPlaceholders.entries(),
|
||||
([recipientIndex, name]) => {
|
||||
return {
|
||||
email: `recipient.${recipientIndex}@documenso.com`,
|
||||
name,
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: recipientIndex,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: envelopeId,
|
||||
userId,
|
||||
@ -416,53 +476,19 @@ export const insertFieldsFromPlaceholdersInPDF = async (
|
||||
});
|
||||
}
|
||||
|
||||
const existingRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
let createdRecipients: Pick<Recipient, 'id' | 'email'>[];
|
||||
|
||||
const existingEmails = new Set(existingRecipients.map((r) => r.email));
|
||||
const recipientsToCreateFiltered = recipientsToCreate.filter(
|
||||
(recipient) => !existingEmails.has(recipient.email),
|
||||
);
|
||||
|
||||
let createdRecipients: Pick<Recipient, 'id' | 'email'>[] = existingRecipients;
|
||||
|
||||
if (recipientsToCreateFiltered.length > 0) {
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
const { recipients } = await createDocumentRecipients({
|
||||
userId,
|
||||
teamId,
|
||||
id: envelopeId,
|
||||
recipients: recipientsToCreateFiltered,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
if (recipients && recipients.length > 0) {
|
||||
createdRecipients = recipients;
|
||||
} else {
|
||||
createdRecipients = await createRecipients(
|
||||
recipientPlaceholders,
|
||||
envelope,
|
||||
envelopeId,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldsToCreate: FieldToCreate[] = [];
|
||||
@ -479,8 +505,30 @@ export const insertFieldsFromPlaceholdersInPDF = async (
|
||||
const widthPercent = (placeholder.width / placeholder.pageWidth) * 100;
|
||||
const heightPercent = (placeholder.height / placeholder.pageHeight) * 100;
|
||||
|
||||
const { email } = extractRecipientPlaceholder(placeholder.recipient);
|
||||
const recipient = createdRecipients.find((r) => r.email === email);
|
||||
let recipient: Pick<Recipient, 'id' | 'email'> | 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, {
|
||||
|
||||
Reference in New Issue
Block a user