mirror of
https://github.com/documenso/documenso.git
synced 2026-07-23 00:13:46 +10:00
ba0dead96f
Currently direct templates can be created without the required signatures fields for signers. This means that the document can be fully signed by everyone but will ultimately fail the sealing step which leaves the document in an unrecoverable state.
827 lines
28 KiB
TypeScript
827 lines
28 KiB
TypeScript
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
|
import { prisma } from '@documenso/prisma';
|
|
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
|
|
import type { Field, Signature } from '@prisma/client';
|
|
import {
|
|
DocumentSigningOrder,
|
|
DocumentSource,
|
|
DocumentStatus,
|
|
EnvelopeType,
|
|
FieldType,
|
|
Prisma,
|
|
RecipientRole,
|
|
SendStatus,
|
|
SigningStatus,
|
|
WebhookTriggerEvents,
|
|
} from '@prisma/client';
|
|
import { DateTime } from 'luxon';
|
|
import { match } from 'ts-pattern';
|
|
import { z } from 'zod';
|
|
|
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
import { jobs } from '../../jobs/client';
|
|
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
|
|
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
|
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
|
import { ZFieldMetaSchema } from '../../types/field-meta';
|
|
import { ZSignatureLevelSchema } from '../../types/signature-level';
|
|
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } 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';
|
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|
import {
|
|
createDocumentAuthOptions,
|
|
createRecipientAuthOptions,
|
|
extractDocumentAuthMethods,
|
|
} from '../../utils/document-auth';
|
|
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
|
import { getRecipientsWithMissingFields } from '../../utils/recipients';
|
|
import { sendDocument } from '../document/send-document';
|
|
import { validateFieldAuth } from '../document/validate-field-auth';
|
|
import { incrementDocumentId } from '../envelope/increment-id';
|
|
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
|
|
import { resolveSignatureLevel } from '../signature-level/resolve-signature-level';
|
|
import { getTeamSettings } from '../team/get-team-settings';
|
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|
|
|
export type CreateDocumentFromDirectTemplateOptions = {
|
|
directRecipientName?: string;
|
|
directRecipientEmail: string;
|
|
directTemplateToken: string;
|
|
directTemplateExternalId?: string;
|
|
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
|
templateUpdatedAt: Date;
|
|
requestMetadata: ApiRequestMetadata;
|
|
user?: {
|
|
id: number;
|
|
name?: string;
|
|
email: string;
|
|
};
|
|
nextSigner?: {
|
|
email: string;
|
|
name: string;
|
|
};
|
|
};
|
|
|
|
type CreatedDirectRecipientField = {
|
|
field: Field & { signature?: Signature | null };
|
|
derivedRecipientActionAuth?: TRecipientActionAuthTypes;
|
|
};
|
|
|
|
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
|
token: z.string(),
|
|
envelopeId: z.string(),
|
|
documentId: z.number(),
|
|
recipientId: z.number(),
|
|
});
|
|
|
|
export type TCreateDocumentFromDirectTemplateResponse = z.infer<typeof ZCreateDocumentFromDirectTemplateResponseSchema>;
|
|
|
|
export const createDocumentFromDirectTemplate = async ({
|
|
directRecipientName: initialDirectRecipientName,
|
|
directRecipientEmail,
|
|
directTemplateToken,
|
|
directTemplateExternalId,
|
|
signedFieldValues,
|
|
templateUpdatedAt,
|
|
nextSigner,
|
|
requestMetadata,
|
|
user,
|
|
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
|
|
const directTemplateEnvelope = await prisma.envelope.findFirst({
|
|
where: {
|
|
directLink: {
|
|
token: directTemplateToken,
|
|
},
|
|
},
|
|
include: {
|
|
recipients: {
|
|
include: {
|
|
fields: true,
|
|
},
|
|
},
|
|
directLink: true,
|
|
envelopeItems: {
|
|
include: {
|
|
documentData: true,
|
|
},
|
|
},
|
|
documentMeta: true,
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
},
|
|
},
|
|
team: {
|
|
select: {
|
|
organisationId: true,
|
|
organisation: {
|
|
select: {
|
|
organisationClaim: {
|
|
select: {
|
|
recipientCount: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!directTemplateEnvelope?.directLink?.enabled) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
|
|
}
|
|
|
|
if (
|
|
nextSigner &&
|
|
(!directTemplateEnvelope.documentMeta?.allowDictateNextSigner ||
|
|
directTemplateEnvelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL)
|
|
) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
|
|
});
|
|
}
|
|
|
|
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(directTemplateEnvelope.secondaryId);
|
|
|
|
if (directTemplateEnvelope.envelopeItems.length < 1) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Invalid number of envelope items',
|
|
});
|
|
}
|
|
|
|
const settings = await getTeamSettings({
|
|
userId: directTemplateEnvelope.userId,
|
|
teamId: directTemplateEnvelope.teamId,
|
|
});
|
|
|
|
const { recipients, directLink } = directTemplateEnvelope;
|
|
|
|
const directTemplateRecipient = recipients.find((recipient) => recipient.id === directLink.directTemplateRecipientId);
|
|
|
|
if (!directTemplateRecipient || directTemplateRecipient.role === RecipientRole.CC) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Invalid or missing direct recipient',
|
|
});
|
|
}
|
|
|
|
const recipientsWithMissingFields = getRecipientsWithMissingFields(
|
|
recipients,
|
|
recipients.flatMap((recipient) => recipient.fields),
|
|
);
|
|
|
|
if (recipientsWithMissingFields.length > 0) {
|
|
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
|
message: 'One or more signers on this direct template are missing a signature field',
|
|
});
|
|
}
|
|
|
|
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
|
}
|
|
|
|
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } = extractDocumentAuthMethods({
|
|
documentAuth: directTemplateEnvelope.authOptions,
|
|
});
|
|
|
|
let directRecipientName = user?.name || initialDirectRecipientName;
|
|
|
|
// Ensure typesafety when we add more options.
|
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
|
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => false) // Not supported for direct templates
|
|
.with(undefined, () => true)
|
|
.exhaustive();
|
|
|
|
if (!isAccessAuthValid) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You must be logged in' });
|
|
}
|
|
|
|
const directTemplateRecipientAuthOptions = ZRecipientAuthOptionsSchema.parse(directTemplateRecipient.authOptions);
|
|
|
|
const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
|
|
(recipient) => recipient.id !== directTemplateRecipient.id,
|
|
);
|
|
|
|
// Carry the template's level forward, coercing if the instance mode has
|
|
// changed since the template was created. ZSignatureLevelSchema parses the
|
|
// free-form TEXT column defensively. Resolved before meta extraction so
|
|
// signingOrder picks up the TSP-appropriate default + assertion.
|
|
const signatureLevel = resolveSignatureLevel({
|
|
requested: ZSignatureLevelSchema.parse(directTemplateEnvelope.signatureLevel),
|
|
strict: false,
|
|
});
|
|
|
|
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, directTemplateEnvelope.documentMeta, signatureLevel);
|
|
|
|
// The resulting document contains every non-direct template recipient plus the
|
|
// direct recipient that is signing now. A recipientCount of 0 means unlimited.
|
|
// This mirrors the check in `sendDocument`, but must be done here because this
|
|
// flow creates the document directly in PENDING and swallows `sendDocument` errors.
|
|
const maximumRecipientCount = directTemplateEnvelope.team.organisation.organisationClaim.recipientCount;
|
|
const resultingRecipientCount = nonDirectTemplateRecipients.length + 1;
|
|
|
|
if (maximumRecipientCount > 0 && resultingRecipientCount > maximumRecipientCount) {
|
|
throw new AppError('RECIPIENT_LIMIT_EXCEEDED', {
|
|
message: `You cannot send a document with more than ${maximumRecipientCount} recipients`,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
// 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
|
|
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
|
|
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
|
|
|
// Custom logic for V2 to include all fields, since v1 excludes read only
|
|
// and prefilled fields.
|
|
if (directTemplateEnvelope.internalVersion === 2) {
|
|
return true;
|
|
}
|
|
|
|
// Include if it's required or has a signed value
|
|
return isRequiredField(templateField) || signedFieldValue !== undefined;
|
|
});
|
|
|
|
const createDirectRecipientFieldArgs = await Promise.all(
|
|
fieldsToProcess.map(async (templateField) => {
|
|
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
|
|
|
if (isRequiredField(templateField) && !signedFieldValue) {
|
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
message: 'Invalid, missing or changed fields',
|
|
});
|
|
}
|
|
|
|
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
|
|
directRecipientName = signedFieldValue?.value;
|
|
}
|
|
|
|
const derivedRecipientActionAuth = await validateFieldAuth({
|
|
documentAuthOptions: directTemplateEnvelope.authOptions,
|
|
recipient: {
|
|
authOptions: directTemplateRecipient.authOptions,
|
|
email: directRecipientEmail,
|
|
envelopeId: directTemplateEnvelope.id,
|
|
},
|
|
field: templateField,
|
|
userId: user?.id,
|
|
authOptions: signedFieldValue?.authOptions,
|
|
});
|
|
|
|
if (!signedFieldValue) {
|
|
return {
|
|
templateField,
|
|
customText: '',
|
|
derivedRecipientActionAuth,
|
|
signature: null,
|
|
};
|
|
}
|
|
|
|
const { value, isBase64 } = signedFieldValue;
|
|
|
|
const isSignatureField =
|
|
templateField.type === FieldType.SIGNATURE || templateField.type === FieldType.FREE_SIGNATURE;
|
|
|
|
let customText = !isSignatureField ? value : '';
|
|
|
|
const signatureImageAsBase64 = isSignatureField && isBase64 ? value : undefined;
|
|
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
|
|
|
|
if (templateField.type === FieldType.DATE) {
|
|
customText = DateTime.now().setZone(derivedDocumentMeta.timezone).toFormat(derivedDocumentMeta.dateFormat);
|
|
}
|
|
|
|
if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
|
|
throw new Error('Signature field must have a signature');
|
|
}
|
|
|
|
return {
|
|
templateField,
|
|
customText,
|
|
derivedRecipientActionAuth,
|
|
signature: isSignatureField
|
|
? {
|
|
signatureImageAsBase64,
|
|
typedSignature,
|
|
}
|
|
: null,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const directTemplateNonSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature === null);
|
|
|
|
const directTemplateSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature !== null);
|
|
|
|
// Enforce the organisation document-creation limit before creating the document.
|
|
await assertOrganisationRatesAndLimits({
|
|
organisationId: directTemplateEnvelope.team.organisationId,
|
|
type: 'document',
|
|
count: 1,
|
|
});
|
|
|
|
const initialRequestTime = new Date();
|
|
|
|
// Key = original envelope item ID
|
|
// Value = duplicated envelope item ID.
|
|
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
|
|
|
// 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 { documentData: newDocumentData } = await putPdfFileServerSide({
|
|
name: titleToUse,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(buffer),
|
|
});
|
|
|
|
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: directTemplateEnvelope.internalVersion,
|
|
signatureLevel,
|
|
qrToken: prefixedId('qr'),
|
|
source: DocumentSource.TEMPLATE_DIRECT_LINK,
|
|
templateId: directTemplateEnvelopeLegacyId,
|
|
userId: directTemplateEnvelope.userId,
|
|
teamId: directTemplateEnvelope.teamId,
|
|
title: directTemplateEnvelope.title,
|
|
createdAt: initialRequestTime,
|
|
status: DocumentStatus.PENDING,
|
|
externalId: directTemplateExternalId,
|
|
visibility: settings.documentVisibility,
|
|
envelopeItems: {
|
|
createMany: {
|
|
data: envelopeItemsToCreate,
|
|
},
|
|
},
|
|
authOptions: createDocumentAuthOptions({
|
|
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
|
globalActionAuth: templateAuthOptions.globalActionAuth,
|
|
}),
|
|
recipients: {
|
|
createMany: {
|
|
data: nonDirectTemplateRecipients.map((recipient) => {
|
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions);
|
|
|
|
return {
|
|
email: recipient.email,
|
|
name: recipient.name,
|
|
role: recipient.role,
|
|
authOptions: createRecipientAuthOptions({
|
|
accessAuth: authOptions.accessAuth,
|
|
actionAuth: authOptions.actionAuth,
|
|
}),
|
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
signingOrder: recipient.signingOrder,
|
|
token: nanoid(),
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
documentMetaId: documentMeta.id,
|
|
},
|
|
include: {
|
|
recipients: true,
|
|
team: {
|
|
select: {
|
|
url: true,
|
|
},
|
|
},
|
|
envelopeItems: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
|
|
|
Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
|
|
const recipient = createdEnvelope.recipients.find((recipient) => recipient.email === templateRecipient.email);
|
|
|
|
if (!recipient) {
|
|
throw new Error('Recipient not found.');
|
|
}
|
|
|
|
nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
|
|
templateRecipient.fields.map((field) => ({
|
|
envelopeId: createdEnvelope.id,
|
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
|
recipientId: recipient.id,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: field.fieldMeta,
|
|
})),
|
|
);
|
|
});
|
|
|
|
await tx.field.createMany({
|
|
data: nonDirectRecipientFieldsToCreate.map((field) => ({
|
|
...field,
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
|
})),
|
|
});
|
|
|
|
// Create the direct recipient and their non signature fields.
|
|
const createdDirectRecipient = await tx.recipient.create({
|
|
data: {
|
|
envelopeId: createdEnvelope.id,
|
|
email: directRecipientEmail,
|
|
name: directRecipientName,
|
|
authOptions: createRecipientAuthOptions({
|
|
accessAuth: directTemplateRecipientAuthOptions.accessAuth,
|
|
actionAuth: directTemplateRecipientAuthOptions.actionAuth,
|
|
}),
|
|
role: directTemplateRecipient.role,
|
|
token: nanoid(),
|
|
signingStatus: SigningStatus.SIGNED,
|
|
sendStatus: SendStatus.SENT,
|
|
signedAt: initialRequestTime,
|
|
signingOrder: directTemplateRecipient.signingOrder,
|
|
fields: {
|
|
createMany: {
|
|
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => {
|
|
let inserted = true;
|
|
|
|
// Custom logic for V2 to only insert if values exist.
|
|
if (directTemplateEnvelope.internalVersion === 2) {
|
|
inserted = customText !== '';
|
|
}
|
|
|
|
return {
|
|
envelopeId: createdEnvelope.id,
|
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
|
type: templateField.type,
|
|
page: templateField.page,
|
|
positionX: templateField.positionX,
|
|
positionY: templateField.positionY,
|
|
width: templateField.width,
|
|
height: templateField.height,
|
|
customText: customText ?? '',
|
|
inserted,
|
|
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
fields: true,
|
|
},
|
|
});
|
|
|
|
// Create any direct recipient signature fields.
|
|
// Note: It's done like this because we can't nest things in createMany.
|
|
const createdDirectRecipientSignatureFields: CreatedDirectRecipientField[] = await Promise.all(
|
|
directTemplateSignatureFields.map(async ({ templateField, signature, derivedRecipientActionAuth }) => {
|
|
if (!signature) {
|
|
throw new Error('Not possible.');
|
|
}
|
|
|
|
const field = await tx.field.create({
|
|
data: {
|
|
envelopeId: createdEnvelope.id,
|
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
|
recipientId: createdDirectRecipient.id,
|
|
type: templateField.type,
|
|
page: templateField.page,
|
|
positionX: templateField.positionX,
|
|
positionY: templateField.positionY,
|
|
width: templateField.width,
|
|
height: templateField.height,
|
|
customText: '',
|
|
inserted: true,
|
|
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
|
signature: {
|
|
create: {
|
|
recipientId: createdDirectRecipient.id,
|
|
signatureImageAsBase64: signature.signatureImageAsBase64,
|
|
typedSignature: signature.typedSignature,
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
signature: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
field,
|
|
derivedRecipientActionAuth,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
|
...createdDirectRecipient.fields.map((field) => ({
|
|
field,
|
|
derivedRecipientActionAuth: undefined,
|
|
})),
|
|
...createdDirectRecipientSignatureFields,
|
|
];
|
|
|
|
/**
|
|
* Create the following audit logs.
|
|
* - DOCUMENT_CREATED
|
|
* - DOCUMENT_FIELD_INSERTED
|
|
* - DOCUMENT_RECIPIENT_COMPLETED
|
|
*/
|
|
const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
title: createdEnvelope.title,
|
|
source: {
|
|
type: DocumentSource.TEMPLATE_DIRECT_LINK,
|
|
templateId: directTemplateEnvelopeLegacyId,
|
|
directRecipientEmail,
|
|
},
|
|
},
|
|
}),
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: createdDirectRecipient.email,
|
|
recipientId: createdDirectRecipient.id,
|
|
recipientName: createdDirectRecipient.name,
|
|
recipientRole: createdDirectRecipient.role,
|
|
accessAuth: derivedRecipientAccessAuth || undefined,
|
|
},
|
|
}),
|
|
...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) =>
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: createdDirectRecipient.email,
|
|
recipientId: createdDirectRecipient.id,
|
|
recipientName: createdDirectRecipient.name,
|
|
recipientRole: createdDirectRecipient.role,
|
|
fieldId: field.secondaryId,
|
|
field: match(field.type)
|
|
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, (type) => ({
|
|
type,
|
|
data: field.signature?.signatureImageAsBase64 || field.signature?.typedSignature || '',
|
|
}))
|
|
.with(
|
|
FieldType.DATE,
|
|
FieldType.EMAIL,
|
|
FieldType.INITIALS,
|
|
FieldType.NAME,
|
|
FieldType.TEXT,
|
|
FieldType.NUMBER,
|
|
FieldType.CHECKBOX,
|
|
FieldType.DROPDOWN,
|
|
FieldType.RADIO,
|
|
(type) => ({
|
|
type,
|
|
data: field.customText,
|
|
}),
|
|
)
|
|
.exhaustive(),
|
|
fieldSecurity: derivedRecipientActionAuth
|
|
? {
|
|
type: derivedRecipientActionAuth,
|
|
}
|
|
: undefined,
|
|
},
|
|
}),
|
|
),
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: createdDirectRecipient.email,
|
|
recipientId: createdDirectRecipient.id,
|
|
recipientName: createdDirectRecipient.name,
|
|
recipientRole: createdDirectRecipient.role,
|
|
actionAuth: createdDirectRecipient.authOptions?.actionAuth ?? [],
|
|
},
|
|
}),
|
|
];
|
|
|
|
if (nextSigner) {
|
|
const pendingRecipients = await tx.recipient.findMany({
|
|
select: {
|
|
id: true,
|
|
signingOrder: true,
|
|
name: true,
|
|
email: true,
|
|
role: true,
|
|
},
|
|
where: {
|
|
envelopeId: createdEnvelope.id,
|
|
signingStatus: {
|
|
not: SigningStatus.SIGNED,
|
|
},
|
|
role: {
|
|
not: RecipientRole.CC,
|
|
},
|
|
},
|
|
// Composite sort so our next recipient is always the one with the lowest signing order or id
|
|
// if there is a tie.
|
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
|
});
|
|
|
|
const nextRecipient = pendingRecipients[0];
|
|
|
|
if (nextRecipient) {
|
|
auditLogsToCreate.push(
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
name: user?.name || directRecipientName || '',
|
|
email: user?.email || directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: nextRecipient.email,
|
|
recipientName: nextRecipient.name,
|
|
recipientId: nextRecipient.id,
|
|
recipientRole: nextRecipient.role,
|
|
changes: [
|
|
{
|
|
type: RECIPIENT_DIFF_TYPE.NAME,
|
|
from: nextRecipient.name,
|
|
to: nextSigner.name,
|
|
},
|
|
{
|
|
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
|
from: nextRecipient.email,
|
|
to: nextSigner.email,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
|
|
await tx.recipient.update({
|
|
where: { id: nextRecipient.id },
|
|
data: {
|
|
...(nextSigner && documentMeta?.allowDictateNextSigner
|
|
? {
|
|
name: nextSigner.name,
|
|
email: nextSigner.email,
|
|
}
|
|
: {}),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
await tx.documentAuditLog.createMany({
|
|
data: auditLogsToCreate,
|
|
});
|
|
|
|
const templateAttachments = await tx.envelopeAttachment.findMany({
|
|
where: {
|
|
envelopeId: directTemplateEnvelope.id,
|
|
},
|
|
});
|
|
|
|
if (templateAttachments.length > 0) {
|
|
await tx.envelopeAttachment.createMany({
|
|
data: templateAttachments.map((attachment) => ({
|
|
envelopeId: createdEnvelope.id,
|
|
type: attachment.type,
|
|
label: attachment.label,
|
|
data: attachment.data,
|
|
})),
|
|
});
|
|
}
|
|
|
|
return {
|
|
createdEnvelope,
|
|
token: createdDirectRecipient.token,
|
|
recipientId: createdDirectRecipient.id,
|
|
};
|
|
});
|
|
|
|
const emailSettings = extractDerivedDocumentEmailSettings(documentMeta);
|
|
|
|
if (emailSettings.ownerDocumentCreated) {
|
|
await jobs.triggerJob({
|
|
name: 'send.document.created.from.direct.template.email',
|
|
payload: {
|
|
envelopeId: createdEnvelope.id,
|
|
recipientId,
|
|
},
|
|
});
|
|
}
|
|
|
|
try {
|
|
// This handles sending emails and sealing the document if required.
|
|
await sendDocument({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: createdEnvelope.id,
|
|
},
|
|
userId: createdEnvelope.userId,
|
|
teamId: createdEnvelope.teamId,
|
|
requestMetadata,
|
|
});
|
|
|
|
// Refetch envelope so we get the final data.
|
|
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
id: createdEnvelope.id,
|
|
},
|
|
include: {
|
|
documentMeta: true,
|
|
recipients: true,
|
|
},
|
|
});
|
|
|
|
await triggerWebhook({
|
|
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
|
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
|
|
userId: refetchedEnvelope.userId,
|
|
teamId: refetchedEnvelope.teamId ?? undefined,
|
|
});
|
|
} catch (err) {
|
|
console.error('[CREATE_DOCUMENT_FROM_DIRECT_TEMPLATE]:', err);
|
|
|
|
// Don't launch an error since the document has already been created.
|
|
// Log and reseal as required until we configure middleware.
|
|
}
|
|
|
|
return {
|
|
token,
|
|
envelopeId: createdEnvelope.id,
|
|
documentId: incrementedDocumentId.documentId,
|
|
recipientId,
|
|
};
|
|
};
|