mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add envelopes (#2025)
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
This commit is contained in:
@ -5,6 +5,7 @@ import type { Field, Signature } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
FieldType,
|
||||
Prisma,
|
||||
RecipientRole,
|
||||
@ -31,9 +32,11 @@ import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/doc
|
||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||
@ -43,11 +46,13 @@ import {
|
||||
createRecipientAuthOptions,
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentFromDirectTemplateOptions = {
|
||||
@ -90,7 +95,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
requestMetadata,
|
||||
user,
|
||||
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
const directTemplateEnvelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
directLink: {
|
||||
token: directTemplateToken,
|
||||
@ -103,8 +108,12 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
},
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -115,19 +124,29 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!template?.directLink?.enabled) {
|
||||
if (!directTemplateEnvelope?.directLink?.enabled) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
|
||||
}
|
||||
|
||||
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
|
||||
directTemplateEnvelope.secondaryId,
|
||||
);
|
||||
|
||||
if (directTemplateEnvelope.envelopeItems.length < 1) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Invalid number of envelope items',
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: template.teamId,
|
||||
teamId: directTemplateEnvelope.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const { recipients, directLink, user: templateOwner } = template;
|
||||
const { recipients, directLink, user: templateOwner } = directTemplateEnvelope;
|
||||
|
||||
const directTemplateRecipient = recipients.find(
|
||||
(recipient) => recipient.id === directLink.directTemplateRecipientId,
|
||||
@ -139,7 +158,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (template.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
||||
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
||||
}
|
||||
|
||||
@ -151,7 +170,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } =
|
||||
extractDocumentAuthMethods({
|
||||
documentAuth: template.authOptions,
|
||||
documentAuth: directTemplateEnvelope.authOptions,
|
||||
});
|
||||
|
||||
const directRecipientName = user?.name || initialDirectRecipientName;
|
||||
@ -171,10 +190,13 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
directTemplateRecipient.authOptions,
|
||||
);
|
||||
|
||||
const nonDirectTemplateRecipients = template.recipients.filter(
|
||||
const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
|
||||
(recipient) => recipient.id !== directTemplateRecipient.id,
|
||||
);
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, template.templateMeta);
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(
|
||||
settings,
|
||||
directTemplateEnvelope.documentMeta,
|
||||
);
|
||||
|
||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
||||
// Only process fields that are either required or have been signed by the user
|
||||
@ -202,11 +224,11 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: template.authOptions,
|
||||
documentAuthOptions: directTemplateEnvelope.authOptions,
|
||||
recipient: {
|
||||
authOptions: directTemplateRecipient.authOptions,
|
||||
email: directRecipientEmail,
|
||||
documentId: template.id,
|
||||
envelopeId: directTemplateEnvelope.id,
|
||||
},
|
||||
field: templateField,
|
||||
userId: user?.id,
|
||||
@ -267,29 +289,73 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const initialRequestTime = new Date();
|
||||
|
||||
const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => {
|
||||
const documentData = await tx.documentData.create({
|
||||
data: {
|
||||
type: template.templateDocumentData.type,
|
||||
data: template.templateDocumentData.data,
|
||||
initialData: template.templateDocumentData.initialData,
|
||||
},
|
||||
});
|
||||
// Key = original envelope item ID
|
||||
// Value = duplicated envelope item ID.
|
||||
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
||||
|
||||
// Create the document and non direct template recipients.
|
||||
const document = await tx.document.create({
|
||||
// Duplicate the envelope item data.
|
||||
const envelopeItemsToCreate = await Promise.all(
|
||||
directTemplateEnvelope.envelopeItems.map(async (item, i) => {
|
||||
const buffer = await getFileServerSide(item.documentData);
|
||||
|
||||
const titleToUse = item.title || directTemplateEnvelope.title;
|
||||
|
||||
const duplicatedFile = await putPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(buffer),
|
||||
});
|
||||
|
||||
const newDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: duplicatedFile.type,
|
||||
data: duplicatedFile.data,
|
||||
initialData: duplicatedFile.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const newEnvelopeItemId = prefixedId('envelope_item');
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[item.id] = newEnvelopeItemId;
|
||||
|
||||
return {
|
||||
id: newEnvelopeItemId,
|
||||
title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
|
||||
documentDataId: newDocumentData.id,
|
||||
order: item.order !== undefined ? item.order : i + 1,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: derivedDocumentMeta,
|
||||
});
|
||||
|
||||
const incrementedDocumentId = await incrementDocumentId();
|
||||
|
||||
const { createdEnvelope, recipientId, token } = await prisma.$transaction(async (tx) => {
|
||||
// Create the envelope and non direct template recipients.
|
||||
const createdEnvelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId: incrementedDocumentId.formattedDocumentId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
internalVersion: 1,
|
||||
qrToken: prefixedId('qr'),
|
||||
source: DocumentSource.TEMPLATE_DIRECT_LINK,
|
||||
templateId: template.id,
|
||||
userId: template.userId,
|
||||
teamId: template.teamId,
|
||||
title: template.title,
|
||||
templateId: directTemplateEnvelopeLegacyId,
|
||||
userId: directTemplateEnvelope.userId,
|
||||
teamId: directTemplateEnvelope.teamId,
|
||||
title: directTemplateEnvelope.title,
|
||||
createdAt: initialRequestTime,
|
||||
status: DocumentStatus.PENDING,
|
||||
externalId: directTemplateExternalId,
|
||||
visibility: settings.documentVisibility,
|
||||
documentDataId: documentData.id,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
data: envelopeItemsToCreate,
|
||||
},
|
||||
},
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
globalActionAuth: templateAuthOptions.globalActionAuth,
|
||||
@ -319,9 +385,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
create: derivedDocumentMeta,
|
||||
},
|
||||
documentMetaId: documentMeta.id,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
@ -330,13 +394,18 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||
|
||||
Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
|
||||
const recipient = document.recipients.find(
|
||||
const recipient = createdEnvelope.recipients.find(
|
||||
(recipient) => recipient.email === templateRecipient.email,
|
||||
);
|
||||
|
||||
@ -346,7 +415,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
|
||||
templateRecipient.fields.map((field) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -371,7 +441,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
// Create the direct recipient and their non signature fields.
|
||||
const createdDirectRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
email: directRecipientEmail,
|
||||
name: directRecipientName,
|
||||
authOptions: createRecipientAuthOptions({
|
||||
@ -387,7 +457,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
fields: {
|
||||
createMany: {
|
||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
@ -417,7 +488,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const field = await tx.field.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
recipientId: createdDirectRecipient.id,
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
@ -466,7 +538,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -474,17 +546,17 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title: document.title,
|
||||
title: createdEnvelope.title,
|
||||
source: {
|
||||
type: DocumentSource.TEMPLATE_DIRECT_LINK,
|
||||
templateId: template.id,
|
||||
templateId: directTemplateEnvelopeLegacyId,
|
||||
directRecipientEmail,
|
||||
},
|
||||
},
|
||||
}),
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -502,7 +574,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -547,7 +619,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
),
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
user: {
|
||||
id: user?.id,
|
||||
name: user?.name,
|
||||
@ -572,10 +644,10 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
||||
recipientName: directRecipientEmail,
|
||||
recipientRole: directTemplateRecipient.role,
|
||||
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
|
||||
document.id
|
||||
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(createdEnvelope.team?.url)}/${
|
||||
createdEnvelope.id
|
||||
}`,
|
||||
documentName: document.title,
|
||||
documentName: createdEnvelope.title,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||
});
|
||||
|
||||
@ -600,8 +672,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
createdEnvelope,
|
||||
token: createdDirectRecipient.token,
|
||||
documentId: document.id,
|
||||
recipientId: createdDirectRecipient.id,
|
||||
};
|
||||
});
|
||||
@ -609,18 +681,21 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
try {
|
||||
// This handles sending emails and sealing the document if required.
|
||||
await sendDocument({
|
||||
documentId,
|
||||
userId: template.userId,
|
||||
teamId: template.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: createdEnvelope.id,
|
||||
},
|
||||
userId: createdEnvelope.userId,
|
||||
teamId: createdEnvelope.teamId,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
const createdDocument = await prisma.document.findFirstOrThrow({
|
||||
// Refetch envelope so we get the final data.
|
||||
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: createdEnvelope.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
@ -628,9 +703,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId: template.userId,
|
||||
teamId: template.teamId ?? undefined,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
|
||||
userId: refetchedEnvelope.userId,
|
||||
teamId: refetchedEnvelope.teamId ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[CREATE_DOCUMENT_FROM_DIRECT_TEMPLATE]:', err);
|
||||
@ -641,7 +716,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
return {
|
||||
token,
|
||||
documentId,
|
||||
documentId: incrementedDocumentId.documentId,
|
||||
recipientId,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user