mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
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.
264 lines
7.6 KiB
TypeScript
264 lines
7.6 KiB
TypeScript
import type { DocumentData, Envelope, EnvelopeItem } from '@prisma/client';
|
|
import {
|
|
DocumentSigningOrder,
|
|
DocumentStatus,
|
|
EnvelopeType,
|
|
RecipientRole,
|
|
SendStatus,
|
|
SigningStatus,
|
|
WebhookTriggerEvents,
|
|
} from '@prisma/client';
|
|
|
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
import { prisma } from '@documenso/prisma';
|
|
|
|
import { jobs } from '../../jobs/client';
|
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
|
import {
|
|
ZWebhookDocumentSchema,
|
|
mapEnvelopeToWebhookDocumentPayload,
|
|
} from '../../types/webhook-payload';
|
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
|
import { isDocumentCompleted } from '../../utils/document';
|
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|
|
|
export type SendDocumentOptions = {
|
|
id: EnvelopeIdOptions;
|
|
userId: number;
|
|
teamId: number;
|
|
sendEmail?: boolean;
|
|
requestMetadata: ApiRequestMetadata;
|
|
};
|
|
|
|
export const sendDocument = async ({
|
|
id,
|
|
userId,
|
|
teamId,
|
|
sendEmail,
|
|
requestMetadata,
|
|
}: SendDocumentOptions) => {
|
|
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
|
id,
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId,
|
|
teamId,
|
|
});
|
|
|
|
const envelope = await prisma.envelope.findFirst({
|
|
where: envelopeWhereInput,
|
|
include: {
|
|
recipients: {
|
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
|
},
|
|
documentMeta: true,
|
|
envelopeItems: {
|
|
select: {
|
|
id: true,
|
|
documentData: {
|
|
select: {
|
|
type: true,
|
|
id: true,
|
|
data: true,
|
|
initialData: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!envelope) {
|
|
throw new Error('Document not found');
|
|
}
|
|
|
|
if (envelope.recipients.length === 0) {
|
|
throw new Error('Document has no recipients');
|
|
}
|
|
|
|
if (isDocumentCompleted(envelope.status)) {
|
|
throw new Error('Can not send completed document');
|
|
}
|
|
|
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
|
|
|
const signingOrder = envelope.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
|
|
|
let recipientsToNotify = envelope.recipients;
|
|
|
|
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
|
// Get the currently active recipient.
|
|
recipientsToNotify = envelope.recipients
|
|
.filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC)
|
|
.slice(0, 1);
|
|
|
|
// Secondary filter so we aren't resending if the current active recipient has already
|
|
// received the envelope.
|
|
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
|
|
}
|
|
|
|
if (envelope.envelopeItems.length === 0) {
|
|
throw new Error('Missing envelope items');
|
|
}
|
|
|
|
if (envelope.formValues) {
|
|
await Promise.all(
|
|
envelope.envelopeItems.map(async (envelopeItem) => {
|
|
await injectFormValuesIntoDocument(envelope, envelopeItem);
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
|
// decide if we want to enforce this for API & templates.
|
|
// const fields = await getFieldsForDocument({
|
|
// documentId: documentId,
|
|
// userId: userId,
|
|
// });
|
|
|
|
// const fieldsWithSignerEmail = fields.map((field) => ({
|
|
// ...field,
|
|
// signerEmail:
|
|
// envelope.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
|
// }));
|
|
|
|
// const everySignerHasSignature = document?.Recipient.every(
|
|
// (recipient) =>
|
|
// recipient.role !== RecipientRole.SIGNER ||
|
|
// fieldsWithSignerEmail.some(
|
|
// (field) => field.type === 'SIGNATURE' && field.signerEmail === recipient.email,
|
|
// ),
|
|
// );
|
|
|
|
// if (!everySignerHasSignature) {
|
|
// throw new Error('Some signers have not been assigned a signature field.');
|
|
// }
|
|
|
|
const allRecipientsHaveNoActionToTake = envelope.recipients.every(
|
|
(recipient) =>
|
|
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
|
);
|
|
|
|
if (allRecipientsHaveNoActionToTake) {
|
|
await jobs.triggerJob({
|
|
name: 'internal.seal-document',
|
|
payload: {
|
|
documentId: legacyDocumentId,
|
|
requestMetadata: requestMetadata?.requestMetadata,
|
|
},
|
|
});
|
|
|
|
// Keep the return type the same for the `sendDocument` method
|
|
return await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
id: envelope.id,
|
|
},
|
|
include: {
|
|
documentMeta: true,
|
|
recipients: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
|
if (envelope.status === DocumentStatus.DRAFT) {
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
|
envelopeId: envelope.id,
|
|
metadata: requestMetadata,
|
|
data: {},
|
|
}),
|
|
});
|
|
}
|
|
|
|
return await tx.envelope.update({
|
|
where: {
|
|
id: envelope.id,
|
|
},
|
|
data: {
|
|
status: DocumentStatus.PENDING,
|
|
},
|
|
include: {
|
|
documentMeta: true,
|
|
recipients: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
|
envelope.documentMeta,
|
|
).recipientSigningRequest;
|
|
|
|
// Only send email if one of the following is true:
|
|
// - It is explicitly set
|
|
// - The email is enabled for signing requests AND sendEmail is undefined
|
|
if (sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined)) {
|
|
await Promise.all(
|
|
recipientsToNotify.map(async (recipient) => {
|
|
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
|
return;
|
|
}
|
|
|
|
await jobs.triggerJob({
|
|
name: 'send.signing.requested.email',
|
|
payload: {
|
|
userId,
|
|
documentId: legacyDocumentId,
|
|
recipientId: recipient.id,
|
|
requestMetadata: requestMetadata?.requestMetadata,
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
|
|
await triggerWebhook({
|
|
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
|
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)),
|
|
userId,
|
|
teamId,
|
|
});
|
|
|
|
return updatedEnvelope;
|
|
};
|
|
|
|
const injectFormValuesIntoDocument = async (
|
|
envelope: Envelope,
|
|
envelopeItem: Pick<EnvelopeItem, 'id'> & { documentData: DocumentData },
|
|
) => {
|
|
const file = await getFileServerSide(envelopeItem.documentData);
|
|
|
|
const prefilled = await insertFormValuesInPdf({
|
|
pdf: Buffer.from(file),
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
formValues: envelope.formValues as Record<string, string | number | boolean>,
|
|
});
|
|
|
|
let fileName = envelope.title;
|
|
|
|
if (!envelope.title.endsWith('.pdf')) {
|
|
fileName = `${envelope.title}.pdf`;
|
|
}
|
|
|
|
const newDocumentData = await putPdfFileServerSide({
|
|
name: fileName,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
|
});
|
|
|
|
await prisma.envelopeItem.update({
|
|
where: {
|
|
id: envelopeItem.id,
|
|
},
|
|
data: {
|
|
documentDataId: newDocumentData.id,
|
|
},
|
|
});
|
|
};
|