feat: extend use envelope response (#2192)

This commit is contained in:
David Nguyen
2025-11-14 13:52:19 +11:00
committed by GitHub
parent 6ec1c3a3fb
commit 56526f9448
10 changed files with 158 additions and 14 deletions

View File

@ -28,9 +28,13 @@ import {
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type {
TUseEnvelopePayload,
TUseEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/use-envelope.types';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
@ -3074,6 +3078,82 @@ test.describe('Document API V2', () => {
});
});
test.describe('Envelope use endpoint', () => {
test('should block unauthorized access to envelope use endpoint', async ({ request }) => {
const doc = await seedTemplate({
title: 'Team template 1',
userId: userA.id,
teamId: teamA.id,
internalVersion: 2,
});
const payload: TUseEnvelopePayload = {
envelopeId: doc.id,
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/use`, {
headers: { Authorization: `Bearer ${tokenB}` },
multipart: formData,
});
expect(res.ok()).toBeFalsy();
expect(res.status()).toBe(404);
});
test('should allow authorized access to envelope use endpoint', async ({ page, request }) => {
const doc = await seedTemplate({
title: 'Team template 1',
userId: userA.id,
teamId: teamA.id,
internalVersion: 2,
});
const payload: TUseEnvelopePayload = {
envelopeId: doc.id,
distributeDocument: true,
recipients: [
{
id: doc.recipients[0].id,
email: doc.recipients[0].email,
name: 'New Name',
},
],
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/use`, {
headers: { Authorization: `Bearer ${tokenA}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const data: TUseEnvelopeResponse = await res.json();
const createdEnvelope = await prisma.envelope.findFirst({
where: {
id: data.id,
},
include: {
recipients: true,
},
});
expect(createdEnvelope).toBeDefined();
expect(createdEnvelope?.recipients.length).toBe(1);
expect(createdEnvelope?.recipients[0].email).toBe(doc.recipients[0].email);
expect(createdEnvelope?.recipients[0].name).toBe('New Name');
expect(createdEnvelope?.recipients[0].token).toBe(data.recipients[0].token);
expect(createdEnvelope?.recipients[0].token).not.toBe(doc.recipients[0].token);
});
});
test.describe('Envelope distribute endpoint', () => {
test('should block unauthorized access to envelope distribute endpoint', async ({
request,

View File

@ -44,7 +44,7 @@ export const resendDocument = async ({
recipients,
teamId,
requestMetadata,
}: ResendDocumentOptions): Promise<void> => {
}: ResendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@ -103,7 +103,7 @@ export const resendDocument = async ({
).recipientSigningRequest;
if (!isRecipientSigningRequestEmailEnabled) {
return;
return envelope;
}
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
@ -230,4 +230,6 @@ export const resendDocument = async ({
);
}),
);
return envelope;
};

View File

@ -110,7 +110,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
data: {
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: 1,
internalVersion: options.internalVersion ?? 1,
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: {
@ -143,6 +143,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
documentData: true,
},
},
recipients: true,
},
});
};

View File

@ -1,7 +1,7 @@
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { ZGenericSuccessResponse } from '../schema';
import { authenticatedProcedure } from '../trpc';
import {
ZDistributeEnvelopeRequestSchema,
@ -45,7 +45,7 @@ export const distributeEnvelopeRoute = authenticatedProcedure
});
}
await sendDocument({
const envelope = await sendDocument({
userId: ctx.user.id,
id: {
type: 'envelopeId',
@ -55,5 +55,17 @@ export const distributeEnvelopeRoute = authenticatedProcedure
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
return {
success: true,
id: envelope.id,
recipients: envelope.recipients.map((recipient) => ({
id: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: formatSigningLink(recipient.token),
})),
};
});

View File

@ -4,6 +4,7 @@ import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZRecipientWithSigningUrlSchema } from './schema';
export const distributeEnvelopeMeta: TrpcRouteMeta = {
openapi: {
@ -31,7 +32,10 @@ export const ZDistributeEnvelopeRequestSchema = z.object({
}).optional(),
});
export const ZDistributeEnvelopeResponseSchema = ZSuccessResponseSchema;
export const ZDistributeEnvelopeResponseSchema = ZSuccessResponseSchema.extend({
id: z.string().describe('The ID of the envelope that was sent.'),
recipients: ZRecipientWithSigningUrlSchema.array(),
});
export type TDistributeEnvelopeRequest = z.infer<typeof ZDistributeEnvelopeRequestSchema>;
export type TDistributeEnvelopeResponse = z.infer<typeof ZDistributeEnvelopeResponseSchema>;

View File

@ -1,6 +1,6 @@
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { ZGenericSuccessResponse } from '../schema';
import { authenticatedProcedure } from '../trpc';
import {
ZRedistributeEnvelopeRequestSchema,
@ -23,7 +23,7 @@ export const redistributeEnvelopeRoute = authenticatedProcedure
},
});
await resendDocument({
const envelope = await resendDocument({
userId: ctx.user.id,
teamId,
id: {
@ -34,5 +34,17 @@ export const redistributeEnvelopeRoute = authenticatedProcedure
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
return {
success: true,
id: envelope.id,
recipients: envelope.recipients.map((recipient) => ({
id: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: formatSigningLink(recipient.token),
})),
};
});

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZRecipientWithSigningUrlSchema } from './schema';
export const redistributeEnvelopeMeta: TrpcRouteMeta = {
openapi: {
@ -22,7 +23,10 @@ export const ZRedistributeEnvelopeRequestSchema = z.object({
.describe('The IDs of the recipients to redistribute the envelope to.'),
});
export const ZRedistributeEnvelopeResponseSchema = ZSuccessResponseSchema;
export const ZRedistributeEnvelopeResponseSchema = ZSuccessResponseSchema.extend({
id: z.string().describe('The ID of the envelope that was redistributed.'),
recipients: ZRecipientWithSigningUrlSchema.array(),
});
export type TRedistributeEnvelopeRequest = z.infer<typeof ZRedistributeEnvelopeRequestSchema>;
export type TRedistributeEnvelopeResponse = z.infer<typeof ZRedistributeEnvelopeResponseSchema>;

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
import RecipientSchema from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
// Common schemas between envelope routes.
export const ZRecipientWithSigningUrlSchema = RecipientSchema.pick({
id: true,
name: true,
email: true,
token: true,
role: true,
signingOrder: true,
}).extend({
signingUrl: z.string().describe('The URL which the recipient uses to sign the document.'),
});

View File

@ -6,6 +6,7 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { authenticatedProcedure } from '../trpc';
import {
@ -26,7 +27,7 @@ export const useEnvelopeRoute = authenticatedProcedure
const {
envelopeId,
externalId,
recipients,
recipients = [],
distributeDocument,
customDocumentData = [],
folderId,
@ -166,5 +167,14 @@ export const useEnvelopeRoute = authenticatedProcedure
return {
id: createdEnvelope.id,
recipients: createdEnvelope.recipients.map((recipient) => ({
id: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: formatSigningLink(recipient.token),
})),
};
});

View File

@ -19,6 +19,7 @@ import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import type { TrpcRouteMeta } from '../trpc';
import { ZRecipientWithSigningUrlSchema } from './schema';
export const useEnvelopeMeta: TrpcRouteMeta = {
openapi: {
@ -44,7 +45,8 @@ export const ZUseEnvelopePayloadSchema = z.object({
signingOrder: z.number().optional(),
}),
)
.describe('The information of the recipients to create the document with.'),
.describe('The information of the recipients to create the document with.')
.optional(),
distributeDocument: z
.boolean()
.describe('Whether to create the document as pending and distribute it to recipients.')
@ -114,6 +116,7 @@ export const ZUseEnvelopeRequestSchema = zodFormData({
export const ZUseEnvelopeResponseSchema = z.object({
id: z.string().describe('The ID of the created envelope.'),
recipients: ZRecipientWithSigningUrlSchema.array(),
});
export type TUseEnvelopePayload = z.infer<typeof ZUseEnvelopePayloadSchema>;