feat: signing order (#1290)

Adds the ability to specify an optional signing order for documents.
When specified a document will be considered sequential with recipients
only being allowed to sign in the order that they were specified in.
This commit is contained in:
Ephraim Duncan
2024-09-16 12:36:45 +00:00
committed by GitHub
parent 357bdd374f
commit 3d644db286
66 changed files with 1999 additions and 606 deletions

View File

@ -41,11 +41,10 @@ test.describe('[EE_ONLY]', () => {
// Add 2 signers.
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient 1');
await page.getByRole('button', { name: 'Add Signer' }).click();
await page
.getByRole('textbox', { name: 'Email', exact: true })
.fill('recipient2@documenso.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
await page.getByLabel('Email').nth(1).fill('recipient2@documenso.com');
await page.getByLabel('Name').nth(1).fill('Recipient 2');
// Display advanced settings.
await page.getByLabel('Show advanced settings').check();
@ -77,9 +76,11 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
// Add 2 signers.
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient 1');
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
await page.getByLabel('Email').nth(1).fill('recipient2@documenso.com');
await page.getByLabel('Name').nth(1).fill('Recipient 2');
// Advanced settings should not be visible for non EE users.
await expect(page.getByLabel('Show advanced settings')).toBeHidden();

View File

@ -4,7 +4,13 @@ import path from 'node:path';
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentStatus,
FieldType,
RecipientRole,
SigningStatus,
} from '@documenso/prisma/client';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,
@ -137,8 +143,9 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByPlaceholder('Email').fill('user1@example.com');
await page.getByPlaceholder('Name').fill('User 1');
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('user2@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('User 2');
await page.getByLabel('Email').nth(1).fill('user2@example.com');
await page.getByLabel('Name').nth(1).fill('User 2');
await page.getByRole('button', { name: 'Continue' }).click();
@ -217,20 +224,20 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByPlaceholder('Name').fill('User 1');
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('user2@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('User 2');
await page.getByLabel('Email').nth(1).fill('user2@example.com');
await page.getByLabel('Name').nth(1).fill('User 2');
await page.locator('button[role="combobox"]').nth(1).click();
await page.getByLabel('Receives copy').click();
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).nth(1).fill('user3@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(2).fill('User 3');
await page.getByLabel('Email').nth(2).fill('user3@example.com');
await page.getByLabel('Name').nth(2).fill('User 3');
await page.locator('button[role="combobox"]').nth(2).click();
await page.getByLabel('Needs to approve').click();
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).nth(2).fill('user4@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(3).fill('User 4');
await page.getByLabel('Email').nth(3).fill('user4@example.com');
await page.getByLabel('Name').nth(3).fill('User 4');
await page.locator('button[role="combobox"]').nth(3).click();
await page.getByLabel('Needs to view').click();
@ -503,3 +510,163 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
}).toPass();
});
test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recipients in sequential order', async ({
page,
}) => {
const user = await seedUser();
const document = await seedBlankDocument(user);
await apiSignin({
page,
email: user.email,
redirectPath: `/documents/${document.id}/edit`,
});
const documentTitle = `Sequential-Signing-${Date.now()}.pdf`;
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await page.getByLabel('Title').fill(documentTitle);
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
await page.getByLabel('Enable signing order').check();
for (let i = 1; i <= 3; i++) {
if (i > 1) {
await page.getByRole('button', { name: 'Add Signer' }).click();
}
await page
.getByPlaceholder('Email')
.nth(i - 1)
.fill(`user${i}@example.com`);
await page
.getByPlaceholder('Name')
.nth(i - 1)
.fill(`User ${i}`);
}
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
for (let i = 1; i <= 3; i++) {
if (i > 1) {
await page.getByText(`User ${i} (user${i}@example.com)`).click();
}
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
position: {
x: 100,
y: 100 * i,
},
});
await page.getByText(`User ${i} (user${i}@example.com)`).click();
}
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
await page.getByRole('button', { name: 'Send' }).click();
await page.waitForURL('/documents');
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
const createdDocument = await prisma.document.findFirst({
where: { title: documentTitle },
include: { Recipient: true },
});
expect(createdDocument).not.toBeNull();
expect(createdDocument?.Recipient.length).toBe(3);
for (let i = 0; i < 3; i++) {
const recipient = createdDocument?.Recipient.find(
(r) => r.email === `user${i + 1}@example.com`,
);
expect(recipient).not.toBeNull();
const fields = await prisma.field.findMany({
where: { recipientId: recipient?.id, documentId: createdDocument?.id },
});
const recipientField = fields[0];
if (i > 0) {
const previousRecipient = await prisma.recipient.findFirst({
where: { email: `user${i}@example.com`, documentId: createdDocument?.id },
});
expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
}
await page.goto(`/sign/${recipient?.token}`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
const canvas = page.locator('canvas#signature');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
await page.getByRole('button', { name: 'Sign', exact: true }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${recipient?.token}/complete`);
await expect(page.getByText('Document Signed')).toBeVisible();
const updatedRecipient = await prisma.recipient.findFirst({
where: { id: recipient?.id },
});
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
}
// Wait for the document to be signed.
await page.waitForTimeout(5000);
const finalDocument = await prisma.document.findFirst({
where: { id: createdDocument?.id },
});
expect(finalDocument?.status).toBe(DocumentStatus.COMPLETED);
});
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
page,
}) => {
const user = await seedUser();
const { document, recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
fields: [FieldType.SIGNATURE],
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }, { signingOrder: 3 }],
updateDocumentOptions: {
documentMeta: {
create: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
},
},
});
const pendingRecipient = recipients.find((r) => r.signingOrder === 2);
await page.goto(`/sign/${pendingRecipient?.token}`);
await expect(page).toHaveURL(`/sign/${pendingRecipient?.token}/waiting`);
const activeRecipient = recipients.find((r) => r.signingOrder === 1);
await page.goto(`/sign/${activeRecipient?.token}`);
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
});

View File

@ -42,10 +42,8 @@ test.describe('[EE_ONLY]', () => {
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient 1');
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
await page
.getByRole('textbox', { name: 'Email', exact: true })
.fill('recipient2@documenso.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
// Display advanced settings.
await page.getByLabel('Show advanced settings').check();
@ -94,8 +92,8 @@ test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => {
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient 1');
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
// Advanced settings should not be visible for non EE users.
await expect(page.getByLabel('Show advanced settings')).toBeHidden();

View File

@ -76,8 +76,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient 1');
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
// Apply require passkey for Recipient 1.
if (isBillingEnabled) {
@ -211,8 +211,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient 1');
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('Recipient 2');
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
// Apply require passkey for Recipient 1.
if (isBillingEnabled) {