fix: sort CC recipients last (#2930)

This commit is contained in:
Catalin Pit
2026-07-24 09:29:46 +03:00
committed by GitHub
parent c02dfaba1a
commit e4897fa686
14 changed files with 1081 additions and 142 deletions
@@ -4,7 +4,14 @@ import { prisma } from '@documenso/prisma';
import { seedBlankDocument, seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import {
DocumentSigningOrder,
DocumentStatus,
FieldType,
RecipientRole,
SendStatus,
SigningStatus,
} from '@prisma/client';
import { DateTime } from 'luxon';
import { apiSignin } from '../fixtures/authentication';
@@ -225,15 +232,20 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByLabel('Receives copy').click();
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByLabel('Email').nth(2).fill('user3@example.com');
await page.getByLabel('Name').nth(2).fill('User 3');
await page.getByRole('combobox').nth(2).click();
// CC recipients are kept last, so new rows are inserted above the CC row.
await expect(page.getByLabel('Email')).toHaveCount(3);
await page.getByLabel('Email').nth(1).fill('user3@example.com');
await page.getByLabel('Name').nth(1).fill('User 3');
await page.getByRole('combobox').nth(1).click();
await page.getByLabel('Needs to approve').click();
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByLabel('Email').nth(3).fill('user4@example.com');
await page.getByLabel('Name').nth(3).fill('User 4');
await page.getByRole('combobox').nth(3).click();
await expect(page.getByLabel('Email')).toHaveCount(4);
await page.getByLabel('Email').nth(2).fill('user4@example.com');
await page.getByLabel('Name').nth(2).fill('User 4');
await page.getByRole('combobox').nth(2).click();
await page.getByLabel('Needs to view').click();
await page.getByRole('button', { name: 'Continue' }).click();
@@ -661,3 +673,182 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode',
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
});
test('[DOCUMENT_FLOW]: should skip CC recipients in sequential signing order', async ({ page }) => {
const { user, team } = await seedUser();
const { document, recipients } = await seedPendingDocumentWithFullFields({
teamId: team.id,
owner: user,
recipients: ['signer1@example.com', 'cc@example.com', 'signer2@example.com'],
fields: [FieldType.SIGNATURE],
recipientsCreateOptions: [
{ signingOrder: 1 },
{
// CC recipients are created pre-signed, mirroring production behaviour.
signingOrder: 2,
role: RecipientRole.CC,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
{ signingOrder: 3 },
],
});
await prisma.documentMeta.update({
where: {
id: document.documentMetaId,
},
data: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const firstSigner = recipients.find((r) => r.email === 'signer1@example.com');
const ccRecipient = recipients.find((r) => r.email === 'cc@example.com');
const lastSigner = recipients.find((r) => r.email === 'signer2@example.com');
// CC recipients cannot have fields.
await prisma.field.deleteMany({
where: {
recipientId: ccRecipient?.id,
},
});
// Sequential order is enforced: the last signer must wait while the first signer is pending.
await page.goto(`/sign/${lastSigner?.token}`);
await expect(page).toHaveURL(`/sign/${lastSigner?.token}/waiting`);
// Sign as the first signer.
await page.goto(`/sign/${firstSigner?.token}`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await signSignaturePad(page);
const firstSignerField = await prisma.field.findFirstOrThrow({
where: { recipientId: firstSigner?.id },
});
await page.locator(`#field-${firstSignerField.id}`).getByRole('button').click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${firstSigner?.token}/complete`);
// The CC recipient at order 2 must not block the last signer at order 3.
await page.goto(`/sign/${lastSigner?.token}`);
await expect(page).not.toHaveURL(`/sign/${lastSigner?.token}/waiting`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await signSignaturePad(page);
const lastSignerField = await prisma.field.findFirstOrThrow({
where: { recipientId: lastSigner?.id },
});
await page.locator(`#field-${lastSignerField.id}`).getByRole('button').click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${lastSigner?.token}/complete`);
// The document completes without any action from the CC recipient.
await expect
.poll(
async () => {
const finalDocument = await prisma.envelope.findFirstOrThrow({
where: { id: document.id },
});
return finalDocument.status;
},
{ timeout: 30_000 },
)
.toBe(DocumentStatus.COMPLETED);
});
test('[DOCUMENT_FLOW]: should skip unsigned CC recipients in sequential signing order', async ({ page }) => {
const { user, team } = await seedUser();
const { document, recipients } = await seedPendingDocumentWithFullFields({
teamId: team.id,
owner: user,
recipients: ['signer1@example.com', 'cc@example.com', 'signer2@example.com'],
fields: [FieldType.SIGNATURE],
recipientsCreateOptions: [
{ signingOrder: 1 },
{
// Legacy/inconsistent data: a CC recipient that was never marked as signed.
signingOrder: 2,
role: RecipientRole.CC,
signingStatus: SigningStatus.NOT_SIGNED,
},
{ signingOrder: 3 },
],
});
await prisma.documentMeta.update({
where: {
id: document.documentMetaId,
},
data: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const firstSigner = recipients.find((r) => r.email === 'signer1@example.com');
const ccRecipient = recipients.find((r) => r.email === 'cc@example.com');
const lastSigner = recipients.find((r) => r.email === 'signer2@example.com');
// CC recipients cannot have fields.
await prisma.field.deleteMany({
where: {
recipientId: ccRecipient?.id,
},
});
// Sign as the first signer.
await page.goto(`/sign/${firstSigner?.token}`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await signSignaturePad(page);
const firstSignerField = await prisma.field.findFirstOrThrow({
where: { recipientId: firstSigner?.id },
});
await page.locator(`#field-${firstSignerField.id}`).getByRole('button').click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${firstSigner?.token}/complete`);
// The unsigned CC recipient at order 2 must not block the last signer at order 3.
await page.goto(`/sign/${lastSigner?.token}`);
await expect(page).not.toHaveURL(`/sign/${lastSigner?.token}/waiting`);
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await signSignaturePad(page);
const lastSignerField = await prisma.field.findFirstOrThrow({
where: { recipientId: lastSigner?.id },
});
await page.locator(`#field-${lastSignerField.id}`).getByRole('button').click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${lastSigner?.token}/complete`);
// The document completes without any action from the CC recipient.
await expect
.poll(
async () => {
const finalDocument = await prisma.envelope.findFirstOrThrow({
where: { id: document.id },
});
return finalDocument.status;
},
{ timeout: 30_000 },
)
.toBe(DocumentStatus.COMPLETED);
});