From 807d094cf2ca2fc0dfa30150af2c6c7741ce859b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=83=98=E3=83=89?= Date: Wed, 27 May 2026 05:30:31 +0300 Subject: [PATCH] fix: email dictated direct template signer (#2810) --- .../e2e/templates/direct-templates.spec.ts | 140 +++++++++++++++++- .../lib/server-only/document/send-document.ts | 4 - .../create-document-from-direct-template.ts | 2 - 3 files changed, 132 insertions(+), 14 deletions(-) diff --git a/packages/app-tests/e2e/templates/direct-templates.spec.ts b/packages/app-tests/e2e/templates/direct-templates.spec.ts index eaa25ec40..69a4c9ad2 100644 --- a/packages/app-tests/e2e/templates/direct-templates.spec.ts +++ b/packages/app-tests/e2e/templates/direct-templates.spec.ts @@ -1,4 +1,5 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta'; import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth'; import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { formatDirectTemplatePath } from '@documenso/lib/utils/templates'; @@ -7,10 +8,11 @@ import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates'; import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users'; import { expect, test } from '@playwright/test'; -import { DocumentSigningOrder, RecipientRole } from '@prisma/client'; +import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client'; import { customAlphabet } from 'nanoid'; import { apiSignin } from '../fixtures/authentication'; +import { signSignaturePad } from '../fixtures/signature'; // Duped from `packages/lib/utils/teams.ts` due to errors when importing that file. const formatDocumentsPath = (teamUrl: string) => `/t/${teamUrl}/documents`; @@ -18,6 +20,47 @@ const formatTemplatesPath = (teamUrl: string) => `/t/${teamUrl}/templates`; const nanoid = customAlphabet('1234567890abcdef', 10); +const expectSigningRequestJobForRecipient = async (recipientId: number) => { + const job = await prisma.backgroundJob.findFirst({ + where: { + jobId: 'send.signing.requested.email', + payload: { + path: ['recipientId'], + equals: recipientId, + }, + }, + }); + + expect(job).not.toBeNull(); +}; + +const seedSignatureFieldForRecipient = async (options: { + envelopeId: string; + recipientId: number; + positionY: number; +}) => { + const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({ + where: { envelopeId: options.envelopeId }, + }); + + return await prisma.field.create({ + data: { + envelopeId: options.envelopeId, + envelopeItemId: envelopeItem.id, + recipientId: options.recipientId, + type: FieldType.SIGNATURE, + page: 1, + positionX: 5, + positionY: options.positionY, + width: 20, + height: 5, + customText: '', + inserted: false, + fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES, + }, + }); +}; + test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) => { const { team, owner, organisation } = await seedTeam({ createTeamMembers: 1, @@ -256,11 +299,24 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex }, }); + const directTemplateRecipient = template.recipients[0]; + + if (!directTemplateRecipient) { + throw new Error('Expected direct template recipient to exist'); + } + + // All SIGNER recipients need a signature field for sendDocument to dispatch emails. + const directSignatureField = await seedSignatureFieldForRecipient({ + envelopeId: template.id, + recipientId: directTemplateRecipient.id, + positionY: 10, + }); + const originalName = 'Signer 2'; const originalSecondSignerEmail = seedTestEmail(); // Add another signer - await prisma.recipient.create({ + const secondRecipient = await prisma.recipient.create({ data: { signingOrder: 2, envelopeId: template.id, @@ -271,6 +327,12 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex }, }); + await seedSignatureFieldForRecipient({ + envelopeId: template.id, + recipientId: secondRecipient.id, + positionY: 20, + }); + // Check that the direct template link is accessible. await page.goto(formatDirectTemplatePath(template.directLink?.token || '')); await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); @@ -279,6 +341,12 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail()); await page.getByRole('button', { name: 'Continue' }).click(); + + // Sign the direct template recipient's signature field via the UI. + await signSignaturePad(page); + await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click(); + await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true'); + await page.getByRole('button', { name: 'Complete' }).click(); await expect(page.getByText('Next Recipient Name')).toBeVisible(); @@ -309,8 +377,15 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2); - expect(updatedSecondRecipient?.name).toBe(newName); - expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail); + expect(updatedSecondRecipient).toBeDefined(); + + if (!updatedSecondRecipient) { + throw new Error('Expected second recipient to exist'); + } + + expect(updatedSecondRecipient.name).toBe(newName); + expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail); + await expectSigningRequestJobForRecipient(updatedSecondRecipient.id); }); test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with next signer dictation', async ({ @@ -338,11 +413,24 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex }, }); + const directTemplateRecipient = template.recipients[0]; + + if (!directTemplateRecipient) { + throw new Error('Expected direct template recipient to exist'); + } + + // All SIGNER recipients need a signature field for sendDocument to dispatch emails. + const directSignatureField = await seedSignatureFieldForRecipient({ + envelopeId: template.id, + recipientId: directTemplateRecipient.id, + positionY: 10, + }); + const originalName = 'Signer 2'; const originalSecondSignerEmail = seedTestEmail(); // Add another signer - await prisma.recipient.create({ + const secondRecipient = await prisma.recipient.create({ data: { signingOrder: 2, envelopeId: template.id, @@ -353,10 +441,39 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex }, }); + await seedSignatureFieldForRecipient({ + envelopeId: template.id, + recipientId: secondRecipient.id, + positionY: 20, + }); + // Check that the direct template link is accessible. await page.goto(formatDirectTemplatePath(template.directLink?.token || '')); await expect(page.getByRole('heading', { name: 'Team direct template link 1' })).toBeVisible(); - await page.waitForTimeout(100); + + // Wait for the PDF and the Konva canvas overlay to be ready. + await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 }); + const canvas = page.locator('.konva-container canvas').first(); + await expect(canvas).toBeVisible({ timeout: 30_000 }); + + // Sign the direct template recipient's signature field via the canvas-based V2 UI. + await signSignaturePad(page); + + const canvasBox = await canvas.boundingBox(); + + if (!canvasBox) { + throw new Error('Canvas bounding box not found'); + } + + const x = + (Number(directSignatureField.positionX) / 100) * canvasBox.width + + ((Number(directSignatureField.width) / 100) * canvasBox.width) / 2; + const y = + (Number(directSignatureField.positionY) / 100) * canvasBox.height + + ((Number(directSignatureField.height) / 100) * canvasBox.height) / 2; + + await canvas.click({ position: { x, y } }); + await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 }); await page.getByRole('button', { name: 'Complete' }).click(); @@ -394,6 +511,13 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2); - expect(updatedSecondRecipient?.name).toBe(newName); - expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail); + expect(updatedSecondRecipient).toBeDefined(); + + if (!updatedSecondRecipient) { + throw new Error('Expected second recipient to exist'); + } + + expect(updatedSecondRecipient.name).toBe(newName); + expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail); + await expectSigningRequestJobForRecipient(updatedSecondRecipient.id); }); diff --git a/packages/lib/server-only/document/send-document.ts b/packages/lib/server-only/document/send-document.ts index 6fc2a026d..09997e0c3 100644 --- a/packages/lib/server-only/document/send-document.ts +++ b/packages/lib/server-only/document/send-document.ts @@ -104,10 +104,6 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad recipientsToNotify = envelope.recipients .filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC) .slice(0, 1); - - // Secondary filter so we aren't resending if the current active recipient has already - // received the envelope. - recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT); } if (envelope.envelopeItems.length === 0) { diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index b59c1920a..4e984a513 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -669,8 +669,6 @@ export const createDocumentFromDirectTemplate = async ({ await tx.recipient.update({ where: { id: nextRecipient.id }, data: { - sendStatus: SendStatus.SENT, - sentAt: new Date(), ...(nextSigner && documentMeta?.allowDictateNextSigner ? { name: nextSigner.name,