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, QUERY_DOCUMENSO_KEY, GET_DOCUMENSO_KEY_BY_JOBID } = require("../graphql-client/queries"); const { uploadFileBuffer } = require("../media/imgproxy-media"); const client = require('../graphql-client/graphql-client').client; 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", } async function esignWebhook(req, res) { try { const message = req.body 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_OPENED: //TODO: DR: Add notification for document opened. await client.request(UPDATE_ESIGNATURE_DOCUMENT, { external_document_id: documentId, esig_update: { status: "OPENED", opened: true, } }) 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. // Here you can add any additional processing you want to do when a document is created break; case webhookTypeEnums.DOCUMENT_COMPLETED: //TODO: DR: Add notification for document completed. await handleDocumentCompleted(message.payload); // Here you can add any additional processing you want to do when a document is completed break; case webhookTypeEnums.DOCUMENT_SIGNED: // Here you can add any additional processing you want to do when a document is 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 }); } } 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`; if (jobs_by_pk?.bodyshop?.uselocalmediaserver) { //TODO:LMS not yet implemented. } else { //S3 Upload const uploadResult = await uploadFileBuffer({ key, buffer, contentType: "application/pdf" }); if (!uploadResult.success) { logger.log(`esig-webhook-s3-upload-error`, "ERROR", "redis", "api", { message: uploadResult.message, stack: uploadResult.stack, jobid: jobid, documentId: payload.id }); } else { logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", { jobid: jobid, documentId: payload.id, s3Key: key, bucket: uploadResult.bucket }); 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' } }) //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(), } }) } } } catch (error) { logger.log(`esig-webhook-event-completed-error`, "ERROR", "redis", "api", { message: error.message, stack: error.stack, payload }); } } module.exports = { esignWebhook } function replaceAccents(str) { // Verifies if the String has accents and replace them if (str.search(/[\xC0-\xFF]/g) > -1) { str = str .replace(/[\xC0-\xC5]/g, "A") .replace(/[\xC6]/g, "AE") .replace(/[\xC7]/g, "C") .replace(/[\xC8-\xCB]/g, "E") .replace(/[\xCC-\xCF]/g, "I") .replace(/[\xD0]/g, "D") .replace(/[\xD1]/g, "N") .replace(/[\xD2-\xD6\xD8]/g, "O") .replace(/[\xD9-\xDC]/g, "U") .replace(/[\xDD]/g, "Y") .replace(/[\xDE]/g, "P") .replace(/[\xE0-\xE5]/g, "a") .replace(/[\xE6]/g, "ae") .replace(/[\xE7]/g, "c") .replace(/[\xE8-\xEB]/g, "e") .replace(/[\xEC-\xEF]/g, "i") .replace(/[\xF1]/g, "n") .replace(/[\xF2-\xF6\xF8]/g, "o") .replace(/[\xF9-\xFC]/g, "u") .replace(/[\xFE]/g, "p") .replace(/[\xFD\xFF]/g, "y"); } return str; }