Files
sign/packages/lib/server-only/document/send-document.tsx

247 lines
6.8 KiB
TypeScript
Raw Normal View History

import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import {
DocumentSigningOrder,
DocumentStatus,
RecipientRole,
SendStatus,
SigningStatus,
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
2025-01-13 13:41:53 +11:00
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { getFile } from '../../universal/upload/get-file';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type SendDocumentOptions = {
documentId: number;
userId: number;
2024-02-22 13:39:34 +11:00
teamId?: number;
sendEmail?: boolean;
requestMetadata: ApiRequestMetadata;
2023-09-20 09:06:28 +00:00
};
export const sendDocument = async ({
documentId,
userId,
2024-02-22 13:39:34 +11:00
teamId,
sendEmail,
requestMetadata,
}: SendDocumentOptions) => {
const document = await prisma.document.findUnique({
where: {
id: documentId,
2024-02-22 13:39:34 +11:00
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
2024-02-22 13:39:34 +11:00
}
: {
userId,
teamId: null,
}),
},
include: {
2025-01-13 13:41:53 +11:00
recipients: {
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
},
documentMeta: true,
documentData: true,
},
});
if (!document) {
throw new Error('Document not found');
}
2025-01-13 13:41:53 +11:00
if (document.recipients.length === 0) {
throw new Error('Document has no recipients');
}
if (document.status === DocumentStatus.COMPLETED) {
throw new Error('Can not send completed document');
}
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
2025-01-13 13:41:53 +11:00
let recipientsToNotify = document.recipients;
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
// Get the currently active recipient.
2025-01-13 13:41:53 +11:00
recipientsToNotify = document.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 document.
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
}
const { documentData } = document;
if (!documentData.data) {
throw new Error('Document data not found');
}
if (document.formValues) {
const file = await getFile(documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(file),
feat: force signature fields for document signers (#1139) ## Description Show a dialog when the document has signers with no signature fields placed. ## Changes Made Created a new dialog that'll be triggered when the document owner tries to send a document to the signers without placing signature fields. The document owners can't proceed to the next step unless they add signature fields. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. https://github.com/documenso/documenso/assets/25515812/f1b5c34e-2ce0-40e3-804c-f05d23045710 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced "Direct Links" for async signing, allowing users to create documents from templates using public links. - Added `MissingSignatureFieldDialog` component to ensure users don't miss adding signature fields. - **Enhancements** - Updated blog content to provide guidance on contract management and announce new pricing plans. - **Bug Fixes** - Improved async signing process for better efficiency and control. - **Refactor** - Improved internal code structure and import order for stripe-related functionality. - **Tests** - Enhanced e2e tests to verify signature presence before document creation and updated test flows for document approval. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: David Nguyen <davidngu28@gmail.com>
2024-06-24 11:01:57 +03:00
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
formValues: document.formValues as Record<string, string | number | boolean>,
});
let fileName = document.title;
if (!document.title.endsWith('.pdf')) {
fileName = `${document.title}.pdf`;
}
const newDocumentData = await putPdfFile({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
const result = await prisma.document.update({
where: {
id: document.id,
},
data: {
documentDataId: newDocumentData.id,
},
});
Object.assign(document, result);
}
feat: force signature fields for document signers (#1139) ## Description Show a dialog when the document has signers with no signature fields placed. ## Changes Made Created a new dialog that'll be triggered when the document owner tries to send a document to the signers without placing signature fields. The document owners can't proceed to the next step unless they add signature fields. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. https://github.com/documenso/documenso/assets/25515812/f1b5c34e-2ce0-40e3-804c-f05d23045710 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced "Direct Links" for async signing, allowing users to create documents from templates using public links. - Added `MissingSignatureFieldDialog` component to ensure users don't miss adding signature fields. - **Enhancements** - Updated blog content to provide guidance on contract management and announce new pricing plans. - **Bug Fixes** - Improved async signing process for better efficiency and control. - **Refactor** - Improved internal code structure and import order for stripe-related functionality. - **Tests** - Enhanced e2e tests to verify signature presence before document creation and updated test flows for document approval. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: David Nguyen <davidngu28@gmail.com>
2024-06-24 11:01:57 +03:00
// 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:
// document.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 isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
document.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({
2024-06-19 13:28:14 +10:00
name: 'send.signing.requested.email',
2024-05-22 21:57:05 +10:00
payload: {
userId,
documentId,
recipientId: recipient.id,
requestMetadata: requestMetadata?.requestMetadata,
},
2024-05-22 21:57:05 +10:00
});
}),
);
}
2025-01-13 13:41:53 +11:00
const allRecipientsHaveNoActionToTake = document.recipients.every(
(recipient) =>
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
);
2024-04-26 02:17:56 +00:00
if (allRecipientsHaveNoActionToTake) {
await jobs.triggerJob({
name: 'internal.seal-document',
payload: {
documentId,
requestMetadata: requestMetadata?.requestMetadata,
},
});
2024-04-26 02:17:56 +00:00
// Keep the return type the same for the `sendDocument` method
return await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
2024-12-14 01:23:35 +09:00
documentMeta: true,
2025-01-13 13:41:53 +11:00
recipients: true,
2024-04-26 02:17:56 +00:00
},
});
}
2024-02-15 18:20:10 +11:00
const updatedDocument = await prisma.$transaction(async (tx) => {
if (document.status === DocumentStatus.DRAFT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
documentId: document.id,
metadata: requestMetadata,
2024-02-15 18:20:10 +11:00
data: {},
}),
});
}
return await tx.document.update({
where: {
id: documentId,
},
data: {
status: DocumentStatus.PENDING,
2024-02-15 18:20:10 +11:00
},
include: {
documentMeta: true,
2025-01-13 13:41:53 +11:00
recipients: true,
},
2024-02-15 18:20:10 +11:00
});
});
2024-02-24 11:18:58 +02:00
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_SENT,
2025-01-13 13:41:53 +11:00
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
userId,
teamId,
2024-02-24 11:18:58 +02:00
});
return updatedDocument;
};