338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
const { Documenso } = require("@documenso/sdk-typescript");
|
|
const logger = require("../utils/logger");
|
|
const {
|
|
QUERY_META_FOR_ESIG_COMPLETION,
|
|
INSERT_ESIGNATURE_COMPLETED_DOCOUMENT,
|
|
UPDATE_ESIGNATURE_DOCUMENT,
|
|
DISTRIBUTE_ESIGNATURE_DOCUMENT,
|
|
GET_DOCUMENSO_KEY_BY_JOBID
|
|
} = require("../graphql-client/queries");
|
|
const replaceAccents = require("../utils/replaceAccents");
|
|
const { uploadFileBuffer } = require("../media/imgproxy-media");
|
|
const {
|
|
dispatchEsignDocumentOpenedNotification,
|
|
dispatchEsignDocumentCompletedNotification,
|
|
dispatchEsignDocumentUploadFailedNotification
|
|
} = require("../notifications/esignNotifications");
|
|
const axios = require("axios");
|
|
const normalizeUrl = require("normalize-url");
|
|
|
|
const client = require("../graphql-client/graphql-client").client;
|
|
|
|
/**
|
|
* Enumeration of webhook event types received from the e-signature service. These events represent different stages in
|
|
* the document signing process, such as when a document is created, sent, opened, signed, completed, rejected,
|
|
* cancelled, or when a reminder is sent. This enumeration is used to handle incoming webhook events and trigger
|
|
* appropriate actions based on the event type.
|
|
* @type {{DOCUMENT_CREATED: string, DOCUMENT_SENT: string, DOCUMENT_COMPLETED: string, DOCUMENT_REJECTED: string, DOCUMENT_CANCELLED: string, DOCUMENT_OPENED: string, DOCUMENT_SIGNED: string, DOCUMENT_REMINDER_SENT: string}}
|
|
*/
|
|
const webhookTypeEnums = {
|
|
DOCUMENT_CREATED: "DOCUMENT_CREATED",
|
|
DOCUMENT_SENT: "DOCUMENT_SENT",
|
|
DOCUMENT_COMPLETED: "DOCUMENT_COMPLETED",
|
|
DOCUMENT_REJECTED: "DOCUMENT_REJECTED",
|
|
DOCUMENT_CANCELLED: "DOCUMENT_CANCELLED",
|
|
DOCUMENT_OPENED: "DOCUMENT_OPENED",
|
|
DOCUMENT_SIGNED: "DOCUMENT_SIGNED",
|
|
DOCUMENT_REMINDER_SENT: "DOCUMENT_REMINDER_SENT"
|
|
};
|
|
|
|
/**
|
|
* Safely dispatches e-sign notifications by catching and logging any errors that occur during the dispatch process.
|
|
* This ensures that failures in notification dispatch do not affect the main flow of processing webhook events.
|
|
* The function takes an object containing the promise returned by the notification dispatch function, the event name,
|
|
* job ID, and document ID for logging purposes.
|
|
* @param param0
|
|
* @param param0.promise
|
|
* @param param0.eventName
|
|
* @param param0.jobid
|
|
* @param param0.documentId
|
|
*/
|
|
function dispatchEsignNotificationSafely({ promise, eventName, jobid, documentId }) {
|
|
promise.catch((error) => {
|
|
logger.log("esig-notification-dispatch-error", "ERROR", "notifications", "api", {
|
|
eventName,
|
|
jobid,
|
|
documentId,
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles incoming webhook events from the e-signature service. It processes different event types such as document
|
|
* opened, completed, rejected, etc., updates the document status in the database accordingly, and dispatches
|
|
* notifications to users. The function also includes error handling to log any issues that occur during processing and
|
|
* ensure a proper response is sent back to the webhook sender.
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function esignWebhook(req, res) {
|
|
try {
|
|
const message = req.body;
|
|
const documentPayload = message.payload || message.payload?.payload || {};
|
|
const externalId = documentPayload.externalId || documentPayload.payload?.externalId || "";
|
|
const [jobid, uploadedBy] = externalId.split("|");
|
|
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
|
|
event: message.event,
|
|
body: message
|
|
});
|
|
|
|
const documentId = (message.payload?.id || message.payload?.payload?.id)?.toString();
|
|
//TODO: Implement checks to prevent this from going backwards in status? If a request fails, it retries, which could cause a document marked as completed to be marked as rejected if the rejection event is processed after the completion event.
|
|
switch (message.event) {
|
|
case webhookTypeEnums.DOCUMENT_REMINDER_SENT:
|
|
break;
|
|
case webhookTypeEnums.DOCUMENT_OPENED:
|
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
|
external_document_id: documentId,
|
|
esig_update: {
|
|
status: "OPENED",
|
|
opened: true
|
|
}
|
|
});
|
|
dispatchEsignNotificationSafely({
|
|
promise: dispatchEsignDocumentOpenedNotification({
|
|
jobId: jobid,
|
|
documentId,
|
|
title: documentPayload.title,
|
|
uploadedBy,
|
|
logger
|
|
}),
|
|
eventName: webhookTypeEnums.DOCUMENT_OPENED,
|
|
jobid,
|
|
documentId
|
|
});
|
|
break;
|
|
case webhookTypeEnums.DOCUMENT_REJECTED:
|
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
|
external_document_id: documentId,
|
|
esig_update: {
|
|
status: "REJECTED",
|
|
rejected: true
|
|
}
|
|
});
|
|
break;
|
|
case webhookTypeEnums.DOCUMENT_CREATED:
|
|
//This is largely a throwaway event we know it was created.
|
|
break;
|
|
case webhookTypeEnums.DOCUMENT_COMPLETED:
|
|
await handleDocumentCompleted(message.payload);
|
|
break;
|
|
case webhookTypeEnums.DOCUMENT_SIGNED:
|
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
|
external_document_id: documentId,
|
|
esig_update: {
|
|
status: "SIGNED"
|
|
}
|
|
});
|
|
break;
|
|
default:
|
|
res.status(200).json({ message: "Unsupported event type." });
|
|
logger.log(`esig-webhook-received-unknown`, "ERROR", "redis", "api", {
|
|
event: message.event,
|
|
body: message
|
|
});
|
|
return;
|
|
}
|
|
logger.log(`esig-webhook-processed`, "INFO", "redis", "api", {
|
|
event: message.event,
|
|
documentId: message.payload?.payload?.id,
|
|
jobid: message.payload?.payload?.externalId?.split("|")[0] || null
|
|
});
|
|
res.sendStatus(200);
|
|
} catch (error) {
|
|
logger.log(`esig-webhook-error`, "ERROR", "redis", "api", {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
body: req.body
|
|
});
|
|
res.status(500).json({ message: "Error processing webhook event.", error: error.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the processing of a document completion event. This includes downloading the completed document from the
|
|
* e-signature service, uploading it to either a local media server or S3 depending on the bodyshop configuration,
|
|
* updating the document record in the database, and dispatching a notification about the document completion.
|
|
* The function also includes error handling to log any issues that occur during processing.
|
|
* @param payload
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function handleDocumentCompleted(payload) {
|
|
try {
|
|
//Split the external id to get the uploaded user.
|
|
const [jobid, uploaded_by] = payload.externalId.split("|");
|
|
if (!jobid || !uploaded_by) {
|
|
throw new Error(`Invalid externalId format. Expected "jobid|uploaded_by", got "${payload.externalId}"`);
|
|
}
|
|
const { jobs_by_pk } = await client.request(QUERY_META_FOR_ESIG_COMPLETION, {
|
|
jobid
|
|
});
|
|
|
|
//Have to use globally authed cleint since this a webhook.
|
|
const {
|
|
jobs_by_pk: {
|
|
bodyshop: { documenso_api_key }
|
|
}
|
|
} = await client.request(GET_DOCUMENSO_KEY_BY_JOBID, {
|
|
jobid
|
|
});
|
|
const documenso = new Documenso({
|
|
apiKey: documenso_api_key,
|
|
serverURL: "https://sign.imex.online/api/v2"
|
|
});
|
|
|
|
const document = await documenso.document.documentDownload({
|
|
documentId: payload.id
|
|
});
|
|
|
|
const response = await fetch(document.downloadUrl);
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
const buffer = Buffer.from(arrayBuffer);
|
|
let key = `${jobs_by_pk.bodyshop.id}/${jobs_by_pk.id}/${replaceAccents(document.filename).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.pdf`;
|
|
const notifyUploadFailure = () =>
|
|
dispatchEsignNotificationSafely({
|
|
promise: dispatchEsignDocumentUploadFailedNotification({
|
|
jobId: jobs_by_pk.id,
|
|
documentId: payload.id.toString(),
|
|
title: payload.title,
|
|
uploadedBy: uploaded_by,
|
|
logger
|
|
}),
|
|
eventName: "DOCUMENT_UPLOAD_FAILED",
|
|
jobid,
|
|
documentId: payload.id.toString()
|
|
});
|
|
|
|
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
|
|
const {
|
|
bodyshop: { localmediaserverhttp, localmediatoken }
|
|
} = jobs_by_pk;
|
|
|
|
const options = {
|
|
headers: {
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
ims_token: localmediatoken
|
|
}
|
|
};
|
|
const formData = new FormData();
|
|
const fileName = document.filename?.toLowerCase().endsWith(".pdf")
|
|
? document.filename
|
|
: `${document.filename || `esignature-document-${payload.id}`}.pdf`;
|
|
const pdfBlob = new Blob([buffer], { type: "application/pdf" });
|
|
|
|
formData.append("jobid", jobid);
|
|
formData.append("file", pdfBlob, fileName);
|
|
|
|
try {
|
|
const imexMediaServerResponse = await axios.post(
|
|
normalizeUrl(`${localmediaserverhttp}/jobs/upload`),
|
|
formData,
|
|
options
|
|
);
|
|
|
|
if (imexMediaServerResponse.status === 200) {
|
|
//Succesful upload - we don't really need to do anything here.
|
|
} else {
|
|
throw new Error(
|
|
`Local media server upload failed with status ${imexMediaServerResponse.status}: ${imexMediaServerResponse.statusText}`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.log(`esig-webhook-lms-upload-error`, "ERROR", "redis", "api", {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
jobid,
|
|
documentId: payload.id
|
|
});
|
|
notifyUploadFailure();
|
|
throw error;
|
|
}
|
|
} else {
|
|
try {
|
|
const uploadResult = await uploadFileBuffer({ key, buffer, contentType: "application/pdf" });
|
|
|
|
if (uploadResult.success) {
|
|
logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", {
|
|
jobid: jobid,
|
|
documentId: payload.id,
|
|
s3Key: key,
|
|
bucket: uploadResult.bucket
|
|
});
|
|
|
|
//insert the document record with the s3 key and bucket info.
|
|
await client.request(INSERT_ESIGNATURE_COMPLETED_DOCOUMENT, {
|
|
docInput: {
|
|
jobid: jobs_by_pk.id,
|
|
uploaded_by: uploaded_by,
|
|
key,
|
|
type: "application/pdf",
|
|
extension: "pdf",
|
|
bodyshopid: jobs_by_pk.bodyshop.id,
|
|
size: buffer.length,
|
|
takenat: new Date().toISOString()
|
|
}
|
|
});
|
|
} else {
|
|
const uploadError = new Error(uploadResult.message || "S3 upload failed");
|
|
uploadError.stack = uploadResult.stack || uploadError.stack;
|
|
throw uploadError;
|
|
}
|
|
} catch (error) {
|
|
logger.log(`esig-webhook-s3-upload-error`, "ERROR", "redis", "api", {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
jobid: jobid,
|
|
documentId: payload.id
|
|
});
|
|
notifyUploadFailure();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
//Update the audit trail and records to mark the document as completed.
|
|
await client.request(DISTRIBUTE_ESIGNATURE_DOCUMENT, {
|
|
external_document_id: payload.id.toString(),
|
|
esig_update: {
|
|
status: "COMPLETED",
|
|
completed: true,
|
|
completed_at: new Date().toISOString()
|
|
},
|
|
audit: {
|
|
jobid: jobs_by_pk.id,
|
|
bodyshopid: jobs_by_pk.bodyshop.id,
|
|
operation: `Esignature document with title ${payload.title} (ID: ${payload.documentMeta.id}) has been completed.`,
|
|
useremail: uploaded_by,
|
|
type: "esig-complete"
|
|
}
|
|
});
|
|
|
|
dispatchEsignNotificationSafely({
|
|
promise: dispatchEsignDocumentCompletedNotification({
|
|
jobId: jobs_by_pk.id,
|
|
documentId: payload.id.toString(),
|
|
title: payload.title,
|
|
uploadedBy: uploaded_by,
|
|
logger
|
|
}),
|
|
eventName: webhookTypeEnums.DOCUMENT_COMPLETED,
|
|
jobid,
|
|
documentId: payload.id.toString()
|
|
});
|
|
} catch (error) {
|
|
logger.log(`esig-webhook-event-completed-error`, "ERROR", "redis", "api", {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
payload
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
esignWebhook
|
|
};
|