diff --git a/apps/remix/server/api/files/files.helpers.ts b/apps/remix/server/api/files/files.helpers.ts index ddce3653d..705de788d 100644 --- a/apps/remix/server/api/files/files.helpers.ts +++ b/apps/remix/server/api/files/files.helpers.ts @@ -11,6 +11,10 @@ import { DocumentStatus, type EnvelopeType, EnvelopeType as EnvelopeTypeEnum, + FieldType, + type Prisma, + type Recipient, + RecipientRole, SigningStatus, type TemplateType, TemplateType as TemplateTypeEnum, @@ -56,6 +60,8 @@ type EnvelopeForPendingDownload = { internalVersion: number; }; +type PendingDownloadRecipient = Pick; + /** * Options shape varies by `version`: * - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers. @@ -78,11 +84,11 @@ type HandleEnvelopeItemFileRequestOptions = { envelope: EnvelopeForPendingDownload; /** - * When set, only fields from recipients who have signed, plus the fields of - * the recipient owning this token, are burned in. Keeps recipient downloads - * in parity with what the signing page shows them. + * Limits the PDF to fields visible to this recipient. Assistants can also + * see non-signature fields for unsigned recipients after them. */ recipientToken?: string; + recipient?: PendingDownloadRecipient; } ); @@ -168,6 +174,7 @@ const handlePendingFileRequest = async ({ envelope, documentData, recipientToken, + recipient, context: c, }: PendingFileRequestOptions) => { if (envelope.status !== DocumentStatus.PENDING) { @@ -190,17 +197,44 @@ const handlePendingFileRequest = async ({ }); } + let visibleFieldWhere: Prisma.FieldWhereInput = {}; + + if (recipient?.role === RecipientRole.ASSISTANT) { + visibleFieldWhere = { + OR: [ + { + recipient: { + signingStatus: SigningStatus.SIGNED, + }, + }, + { + type: { + not: FieldType.SIGNATURE, + }, + recipient: { + signingStatus: { + not: SigningStatus.SIGNED, + }, + signingOrder: { + gt: recipient.signingOrder ?? 0, + }, + }, + }, + ], + }; + } else if (recipientToken) { + visibleFieldWhere = { + recipient: { + OR: [{ signingStatus: SigningStatus.SIGNED }, { token: recipientToken }], + }, + }; + } + const fields = await prisma.field.findMany({ where: { envelopeItemId, inserted: true, - ...(recipientToken - ? { - recipient: { - OR: [{ signingStatus: SigningStatus.SIGNED }, { token: recipientToken }], - }, - } - : {}), + ...visibleFieldWhere, }, include: { signature: true, diff --git a/apps/remix/server/api/files/files.ts b/apps/remix/server/api/files/files.ts index 7676637a7..08d6160d4 100644 --- a/apps/remix/server/api/files/files.ts +++ b/apps/remix/server/api/files/files.ts @@ -311,7 +311,20 @@ export const filesRoute = new Hono() const envelopeItem = await prisma.envelopeItem.findUnique({ where: envelopeWhereQuery, include: { - envelope: true, + envelope: { + include: { + recipients: { + where: { + token, + }, + select: { + role: true, + signingOrder: true, + token: true, + }, + }, + }, + }, documentData: true, }, }); @@ -338,6 +351,7 @@ export const filesRoute = new Hono() envelopeItemId: envelopeItem.id, envelope: envelopeItem.envelope, recipientToken: token, + recipient: envelopeItem.envelope.recipients[0], }); } diff --git a/packages/app-tests/e2e/api/v2/partial-signed-pdf-download.spec.ts b/packages/app-tests/e2e/api/v2/partial-signed-pdf-download.spec.ts index cc4866f7c..207712671 100644 --- a/packages/app-tests/e2e/api/v2/partial-signed-pdf-download.spec.ts +++ b/packages/app-tests/e2e/api/v2/partial-signed-pdf-download.spec.ts @@ -324,6 +324,80 @@ test.describe('API V2 partial signed PDF downloads', () => { expect(afterCompletionResponse.headers().etag).toBe(recipientOneResponse.headers().etag); }); + test('includes fields filled by an assistant in their partial PDF', async ({ request }) => { + const { envelope, distributeResult } = await apiSeedPendingDocument(request, { + recipients: [ + { + email: 'partial-assistant@test.documenso.com', + name: 'Partial Assistant', + role: 'ASSISTANT', + signingOrder: 1, + }, + { + email: 'partial-assisted-signer@test.documenso.com', + name: 'Partial Assisted Signer', + role: 'SIGNER', + signingOrder: 2, + }, + { + email: 'partial-other-signer@test.documenso.com', + name: 'Partial Other Signer', + role: 'SIGNER', + signingOrder: 3, + }, + ], + fieldsPerRecipient: [ + [], + [ + { type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 5, width: 15, height: 5 }, + { type: FieldType.TEXT, page: 1, positionX: 5, positionY: 15, width: 15, height: 5 }, + ], + [{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 25, width: 15, height: 5 }], + ], + }); + + const assistant = distributeResult.recipients.find( + (recipient) => recipient.email === 'partial-assistant@test.documenso.com', + ); + const signer = distributeResult.recipients.find( + (recipient) => recipient.email === 'partial-assisted-signer@test.documenso.com', + ); + const other = distributeResult.recipients.find( + (recipient) => recipient.email === 'partial-other-signer@test.documenso.com', + ); + const textField = envelope.fields.find( + (field) => field.recipientId === signer?.id && field.type === FieldType.TEXT, + ); + + if (!assistant || !signer || !other || !textField) { + throw new Error('Expected assistant test fields'); + } + + await trpcMutation(request, 'field.signFieldWithToken', { + token: assistant.token, + fieldId: textField.id, + value: 'Filled by assistant', + isBase64: false, + }); + + const download = (token: string) => + request.get( + `${WEBAPP_BASE_URL}/api/files/token/${token}/envelopeItem/${envelope.envelopeItems[0].id}/download/pending`, + ); + + const [assistantPdf, signerPdf, otherPdf] = await Promise.all([ + download(assistant.token), + download(signer.token), + download(other.token), + ]); + + expect(assistantPdf.status()).toBe(200); + expect(signerPdf.status()).toBe(200); + expect(otherPdf.status()).toBe(200); + expect(assistantPdf.headers().etag).toBe(signerPdf.headers().etag); + expect(assistantPdf.headers().etag).not.toBe(otherPdf.headers().etag); + }); + test('rejects a recipient token pending download once the envelope is completed', async ({ request }) => { const { envelope, distributeResult } = await apiSeedPendingDocument(request);