mirror of
https://github.com/documenso/documenso.git
synced 2025-11-09 20:12:31 +10:00
During our field rework that makes fields appear more accurately between signing and the completed pdf we swapped to using text fields. Unfortunately as part of that we dropped using the Noto font for the text field causing ANSI encoding issues when encountering certain characters. This change restores the font and handles a nasty issue we had with our form flattening reverting our selected font.
244 lines
7.0 KiB
TypeScript
244 lines
7.0 KiB
TypeScript
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
|
import { nanoid } from 'nanoid';
|
|
import path from 'node:path';
|
|
import { PDFDocument } from 'pdf-lib';
|
|
|
|
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { signPdf } from '@documenso/signing';
|
|
|
|
import {
|
|
ZWebhookDocumentSchema,
|
|
mapDocumentToWebhookDocumentPayload,
|
|
} from '../../types/webhook-payload';
|
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
|
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
|
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
|
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
|
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
|
import { flattenForm } from '../pdf/flatten-form';
|
|
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
|
import { legacy_insertFieldInPDF } from '../pdf/legacy-insert-field-in-pdf';
|
|
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
|
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|
import { sendCompletedEmail } from './send-completed-email';
|
|
|
|
export type SealDocumentOptions = {
|
|
documentId: number;
|
|
sendEmail?: boolean;
|
|
isResealing?: boolean;
|
|
requestMetadata?: RequestMetadata;
|
|
};
|
|
|
|
export const sealDocument = async ({
|
|
documentId,
|
|
sendEmail = true,
|
|
isResealing = false,
|
|
requestMetadata,
|
|
}: SealDocumentOptions) => {
|
|
const document = await prisma.document.findFirstOrThrow({
|
|
where: {
|
|
id: documentId,
|
|
},
|
|
include: {
|
|
documentData: true,
|
|
documentMeta: true,
|
|
recipients: true,
|
|
team: {
|
|
select: {
|
|
teamGlobalSettings: {
|
|
select: {
|
|
includeSigningCertificate: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const { documentData } = document;
|
|
|
|
if (!documentData) {
|
|
throw new Error(`Document ${document.id} has no document data`);
|
|
}
|
|
|
|
const recipients = await prisma.recipient.findMany({
|
|
where: {
|
|
documentId: document.id,
|
|
role: {
|
|
not: RecipientRole.CC,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Determine if the document has been rejected by checking if any recipient has rejected it
|
|
const rejectedRecipient = recipients.find(
|
|
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
|
);
|
|
|
|
const isRejected = Boolean(rejectedRecipient);
|
|
|
|
// Get the rejection reason from the rejected recipient
|
|
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
|
|
|
|
// If the document is not rejected, ensure all recipients have signed
|
|
if (
|
|
!isRejected &&
|
|
recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)
|
|
) {
|
|
throw new Error(`Document ${document.id} has unsigned recipients`);
|
|
}
|
|
|
|
const fields = await prisma.field.findMany({
|
|
where: {
|
|
documentId: document.id,
|
|
},
|
|
include: {
|
|
signature: true,
|
|
},
|
|
});
|
|
|
|
// Skip the field check if the document is rejected
|
|
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
|
throw new Error(`Document ${document.id} has unsigned required fields`);
|
|
}
|
|
|
|
if (isResealing) {
|
|
// If we're resealing we want to use the initial data for the document
|
|
// so we aren't placing fields on top of eachother.
|
|
documentData.data = documentData.initialData;
|
|
}
|
|
|
|
// !: Need to write the fields onto the document as a hard copy
|
|
const pdfData = await getFileServerSide(documentData);
|
|
|
|
const certificateData =
|
|
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
|
? await getCertificatePdf({
|
|
documentId,
|
|
language: document.documentMeta?.language,
|
|
}).catch(() => null)
|
|
: null;
|
|
|
|
const doc = await PDFDocument.load(pdfData);
|
|
|
|
// Normalize and flatten layers that could cause issues with the signature
|
|
normalizeSignatureAppearances(doc);
|
|
await flattenForm(doc);
|
|
flattenAnnotations(doc);
|
|
|
|
// Add rejection stamp if the document is rejected
|
|
if (isRejected && rejectionReason) {
|
|
await addRejectionStampToPdf(doc, rejectionReason);
|
|
}
|
|
|
|
if (certificateData) {
|
|
const certificate = await PDFDocument.load(certificateData);
|
|
|
|
const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
|
|
|
|
certificatePages.forEach((page) => {
|
|
doc.addPage(page);
|
|
});
|
|
}
|
|
|
|
for (const field of fields) {
|
|
document.useLegacyFieldInsertion
|
|
? await legacy_insertFieldInPDF(doc, field)
|
|
: await insertFieldInPDF(doc, field);
|
|
}
|
|
|
|
// Re-flatten post-insertion to handle fields that create arcoFields
|
|
await flattenForm(doc);
|
|
|
|
const pdfBytes = await doc.save();
|
|
|
|
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
|
|
|
const { name } = path.parse(document.title);
|
|
|
|
// Add suffix based on document status
|
|
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
|
|
|
const { data: newData } = await putPdfFileServerSide({
|
|
name: `${name}${suffix}`,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
|
});
|
|
|
|
const postHog = PostHogServerClient();
|
|
|
|
if (postHog) {
|
|
postHog.capture({
|
|
distinctId: nanoid(),
|
|
event: 'App: Document Sealed',
|
|
properties: {
|
|
documentId: document.id,
|
|
isRejected,
|
|
},
|
|
});
|
|
}
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.document.update({
|
|
where: {
|
|
id: document.id,
|
|
},
|
|
data: {
|
|
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
|
completedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
await tx.documentData.update({
|
|
where: {
|
|
id: documentData.id,
|
|
},
|
|
data: {
|
|
data: newData,
|
|
},
|
|
});
|
|
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
|
documentId: document.id,
|
|
requestMetadata,
|
|
user: null,
|
|
data: {
|
|
transactionId: nanoid(),
|
|
...(isRejected ? { isRejected: true, rejectionReason } : {}),
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
if (sendEmail && !isResealing) {
|
|
await sendCompletedEmail({ documentId, requestMetadata });
|
|
}
|
|
|
|
const updatedDocument = await prisma.document.findFirstOrThrow({
|
|
where: {
|
|
id: document.id,
|
|
},
|
|
include: {
|
|
documentData: true,
|
|
documentMeta: true,
|
|
recipients: true,
|
|
},
|
|
});
|
|
|
|
await triggerWebhook({
|
|
event: isRejected
|
|
? WebhookTriggerEvents.DOCUMENT_REJECTED
|
|
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
|
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
|
userId: document.userId,
|
|
teamId: document.teamId ?? undefined,
|
|
});
|
|
};
|