fix: include assistant fields in pending PDFs

This commit is contained in:
ephraimduncan
2026-08-25 04:34:43 +00:00
parent e6ad94a58f
commit d32c7596be
3 changed files with 133 additions and 11 deletions
+44 -10
View File
@@ -11,6 +11,10 @@ import {
DocumentStatus, DocumentStatus,
type EnvelopeType, type EnvelopeType,
EnvelopeType as EnvelopeTypeEnum, EnvelopeType as EnvelopeTypeEnum,
FieldType,
type Prisma,
type Recipient,
RecipientRole,
SigningStatus, SigningStatus,
type TemplateType, type TemplateType,
TemplateType as TemplateTypeEnum, TemplateType as TemplateTypeEnum,
@@ -56,6 +60,8 @@ type EnvelopeForPendingDownload = {
internalVersion: number; internalVersion: number;
}; };
type PendingDownloadRecipient = Pick<Recipient, 'role' | 'signingOrder'>;
/** /**
* Options shape varies by `version`: * Options shape varies by `version`:
* - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers. * - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers.
@@ -78,11 +84,11 @@ type HandleEnvelopeItemFileRequestOptions = {
envelope: EnvelopeForPendingDownload; envelope: EnvelopeForPendingDownload;
/** /**
* When set, only fields from recipients who have signed, plus the fields of * Limits the PDF to fields visible to this recipient. Assistants can also
* the recipient owning this token, are burned in. Keeps recipient downloads * see non-signature fields for unsigned recipients after them.
* in parity with what the signing page shows them.
*/ */
recipientToken?: string; recipientToken?: string;
recipient?: PendingDownloadRecipient;
} }
); );
@@ -168,6 +174,7 @@ const handlePendingFileRequest = async ({
envelope, envelope,
documentData, documentData,
recipientToken, recipientToken,
recipient,
context: c, context: c,
}: PendingFileRequestOptions) => { }: PendingFileRequestOptions) => {
if (envelope.status !== DocumentStatus.PENDING) { 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({ const fields = await prisma.field.findMany({
where: { where: {
envelopeItemId, envelopeItemId,
inserted: true, inserted: true,
...(recipientToken ...visibleFieldWhere,
? {
recipient: {
OR: [{ signingStatus: SigningStatus.SIGNED }, { token: recipientToken }],
},
}
: {}),
}, },
include: { include: {
signature: true, signature: true,
+15 -1
View File
@@ -311,7 +311,20 @@ export const filesRoute = new Hono<HonoEnv>()
const envelopeItem = await prisma.envelopeItem.findUnique({ const envelopeItem = await prisma.envelopeItem.findUnique({
where: envelopeWhereQuery, where: envelopeWhereQuery,
include: { include: {
envelope: true, envelope: {
include: {
recipients: {
where: {
token,
},
select: {
role: true,
signingOrder: true,
token: true,
},
},
},
},
documentData: true, documentData: true,
}, },
}); });
@@ -338,6 +351,7 @@ export const filesRoute = new Hono<HonoEnv>()
envelopeItemId: envelopeItem.id, envelopeItemId: envelopeItem.id,
envelope: envelopeItem.envelope, envelope: envelopeItem.envelope,
recipientToken: token, recipientToken: token,
recipient: envelopeItem.envelope.recipients[0],
}); });
} }
@@ -324,6 +324,80 @@ test.describe('API V2 partial signed PDF downloads', () => {
expect(afterCompletionResponse.headers().etag).toBe(recipientOneResponse.headers().etag); 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 }) => { test('rejects a recipient token pending download once the envelope is completed', async ({ request }) => {
const { envelope, distributeResult } = await apiSeedPendingDocument(request); const { envelope, distributeResult } = await apiSeedPendingDocument(request);