Files
bodyshop/server/esign/webhook.js

388 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { Documenso } = require("@documenso/sdk-typescript");
const fs = require("fs");
const path = require("path");
const logger = require("../utils/logger");
const { QUERY_META_FOR_ESIG_COMPLETION, INSERT_ESIGNATURE_DOCUMENT, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
const { uploadFileBuffer } = require("../media/imgproxy-media");
const client = require('../graphql-client/graphql-client').client;
const documenso = new Documenso({
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
serverURL: "https://stg-app.documenso.com/api/v2",
});
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) {
console.log("Esign Webhook Received:", req.body);
try {
const message = req.body
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
event: message.event,
body: message
});
switch (message.event) {
case webhookTypeEnums.DOCUMENT_CREATED:
//This is largely a throwaway event we know it was created.
console.log("Document created event received. Document ID:", message.payload.documentId);
// Here you can add any additional processing you want to do when a document is created
break;
case webhookTypeEnums.DOCUMENT_COMPLETED:
console.log("Document completed event received. Document ID:", message.payload.documentId);
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:
console.log("Document signed event received. Document ID:", message.payload.documentId);
// Here you can add any additional processing you want to do when a document is signed
break;
default:
console.log(`Unhandled event type: ${message.event}`);
}
// const result = await documenso.documents.download({
// documentId: req.body.payload.id,
// });
// result.resultingBuffer = Buffer.from(result.resultingArrayBuffer);
// // Save the document to a file for testing purposes
// const downloadsDir = path.join(__dirname, '../downloads');
// if (!fs.existsSync(downloadsDir)) {
// fs.mkdirSync(downloadsDir, { recursive: true });
// }
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
// fs.writeFileSync(filePath, result.resultingBuffer);
// console.log(result)
res.sendStatus(200)
} catch (error) {
logger.log(`esig-webhook-error`, "ERROR", "redis", "api", {
message: error.message, stack: error.stack,
body: req.body
});
// const downloadsDir = path.join(__dirname, '../downloads');
// if (!fs.existsSync(downloadsDir)) {
// fs.mkdirSync(downloadsDir, { recursive: true });
// }
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
// fs.writeFileSync(filePath, Buffer.from(err.body));
// console.error("Error handling esign webhook:", err);
res.sendStatus(500)
}
}
async function handleDocumentCompleted(payload = sampleComplete) {
//Check if the bodyshop is on image proxy or not
try {
const { jobs_by_pk } = await client.request(QUERY_META_FOR_ESIG_COMPLETION, {
jobid: payload.externalId
});
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);
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
//LMS not yet implemented.
} else {
//S3 Upload
let key = `${jobs_by_pk.bodyshop.id}/${jobs_by_pk.id}/${replaceAccents(document.filename).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.pdf`;
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: payload.externalId,
documentId: payload.id
});
} else {
logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", {
jobid: payload.externalId,
documentId: payload.id,
s3Key: key,
bucket: uploadResult.bucket
});
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
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: "patrick@imex.dev", //TODO: Figure out the hardcoded bypass.
type: 'esig-complete'
}
})
//insert the document record with the s3 key and bucket info.
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
docInput: {
jobid: jobs_by_pk.id,
uploaded_by: "patrick@imex.dev", //TODO: Figure out the hard coded bypass.
key,
type: "application/pdf",
extension: "pdf",
bodyshopid: jobs_by_pk.bodyshop.id,
size: buffer.length, //Leftover from Cloudinary. We don't do any optimization on upload, so it will always be file.size.
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
}
const sampleComplete = {
"id": 10929,
"title": "CASL Title set in JSR",
"source": "DOCUMENT",
"status": "COMPLETED",
"teamId": 742,
"userId": 654,
"Recipient": [
{
"id": 24997,
"name": "James Tschetter",
"role": "SIGNER",
"email": "patrick@imexsystems.ca",
"token": "uMom0GwL29NBqMfohGpUE",
"signedAt": "2026-02-27T22:11:52.835Z",
"expiresAt": "2026-05-28T22:10:48.991Z",
"documentId": 10929,
"readStatus": "OPENED",
"sendStatus": "SENT",
"templateId": null,
"authOptions": {
"accessAuth": [],
"actionAuth": []
},
"signingOrder": null,
"signingStatus": "SIGNED",
"rejectionReason": null,
"documentDeletedAt": null,
"expirationNotifiedAt": null
}
],
"createdAt": "2026-02-27T22:10:10.580Z",
"deletedAt": null,
"updatedAt": "2026-02-27T22:11:57.753Z",
"externalId": null,
"formValues": null,
"recipients": [
{
"id": 24997,
"name": "James Tschetter",
"role": "SIGNER",
"email": "patrick@imexsystems.ca",
"token": "uMom0GwL29NBqMfohGpUE",
"signedAt": "2026-02-27T22:11:52.835Z",
"expiresAt": "2026-05-28T22:10:48.991Z",
"documentId": 10929,
"readStatus": "OPENED",
"sendStatus": "SENT",
"templateId": null,
"authOptions": {
"accessAuth": [],
"actionAuth": []
},
"signingOrder": null,
"signingStatus": "SIGNED",
"rejectionReason": null,
"documentDeletedAt": null,
"expirationNotifiedAt": null
}
],
"templateId": null,
"visibility": "EVERYONE",
"authOptions": {
"globalAccessAuth": [],
"globalActionAuth": []
},
"completedAt": "2026-02-27T22:11:57.752Z",
"documentMeta": {
"id": "cmm5g3y7u00ecad1sv3ague1w",
"message": "CASL Message set in JSR",
"subject": "CASL Auth Set in JSR",
"language": "en",
"timezone": "Etc/UTC",
"dateFormat": "yyyy-MM-dd hh:mm a",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"emailSettings": {
"documentDeleted": true,
"documentPending": true,
"recipientSigned": true,
"recipientRemoved": true,
"documentCompleted": true,
"ownerDocumentCompleted": true,
"recipientSigningRequest": true
},
"distributionMethod": "EMAIL",
"drawSignatureEnabled": true,
"typedSignatureEnabled": true,
"allowDictateNextSigner": false,
"uploadSignatureEnabled": true
}
}
// const sampleBody = {
// event: "DOCUMENT_COMPLETED",
// payload: {
// Recipient: [
// {
// authOptions: {
// accessAuth: [
// ],
// actionAuth: [
// ],
// },
// documentDeletedAt: null,
// documentId: 9827,
// email: "patrick@imexsystems.ca",
// expired: null,
// id: 13311,
// name: "Customer Fullname",
// readStatus: "OPENED",
// rejectionReason: null,
// role: "SIGNER",
// sendStatus: "SENT",
// signedAt: "2026-01-30T18:29:12.648Z",
// signingOrder: null,
// signingStatus: "SIGNED",
// templateId: null,
// token: "uiEWIsXUPTbWHd7QedVgt",
// },
// ],
// authOptions: {
// globalAccessAuth: [
// ],
// globalActionAuth: [
// ],
// },
// completedAt: "2026-01-30T18:29:16.279Z",
// createdAt: "2026-01-30T18:28:48.861Z",
// deletedAt: null,
// documentMeta: {
// allowDictateNextSigner: false,
// dateFormat: "yyyy-MM-dd hh:mm a",
// distributionMethod: "EMAIL",
// drawSignatureEnabled: true,
// emailSettings: {
// documentCompleted: true,
// documentDeleted: true,
// documentPending: true,
// ownerDocumentCompleted: true,
// recipientRemoved: false,
// recipientSigned: true,
// recipientSigningRequest: true,
// },
// id: "cml17vfb200qjad1t2spxnc1n",
// language: "en",
// message: "To perform repairs on your vehicle, we must receive digital authorization. Please review and sign the document to proceed with repairs. ",
// redirectUrl: null,
// signingOrder: "PARALLEL",
// subject: "Repair Authorization for ABC Collision",
// timezone: "Etc/UTC",
// typedSignatureEnabled: true,
// uploadSignatureEnabled: true,
// },
// externalId: null,
// formValues: null,
// id: 9827,
// recipients: [
// {
// authOptions: {
// accessAuth: [
// ],
// actionAuth: [
// ],
// },
// documentDeletedAt: null,
// documentId: 9827,
// email: "patrick@imexsystems.ca",
// expired: null,
// id: 13311,
// name: "Customer Fullname",
// readStatus: "OPENED",
// rejectionReason: null,
// role: "SIGNER",
// sendStatus: "SENT",
// signedAt: "2026-01-30T18:29:12.648Z",
// signingOrder: null,
// signingStatus: "SIGNED",
// templateId: null,
// token: "uiEWIsXUPTbWHd7QedVgt",
// },
// ],
// source: "DOCUMENT",
// status: "COMPLETED",
// teamId: 742,
// templateId: null,
// title: "Repair Authorization - 1/30/2026, 6:28:48 PM",
// updatedAt: "2026-01-30T18:29:16.280Z",
// userId: 654,
// visibility: "EVERYONE",
// },
// createdAt: "2026-01-30T18:29:18.504Z",
// webhookEndpoint: "https://dev.patrickfic.com/esign/webhook",
// }
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;
}
`Unexpected Status or Content-Type: Status 200 Content-Type application/pdf\nBody: %PDF-1.7\n%<25><><EFBFBD><EFBFBD>\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n/Names 74 0 R\n/Dests 75 0 R\n/Info 77 0 R\n/Lang (en-US)\n/Version /1.7\n>>\nendobj\n77 0 obj\n<<\n/Type /Info\n/CreationDate (D:20260227230617Z00'00')\n/Producer <FEFF007000640066002D006C006900620020002800680074007400700073003A002F002F006700690074006800750062002E0063006F006D002F0048006F007000640069006E0067002F007000640066002D006C006900620029>\n/ModDate (D:20260227231057Z)…<>5[<5B>><3E>Wu7<><37>V<EFBFBD><56><EFBFBD><EFBFBD><EFBFBD>Pw<50>WX<57>ܮJ'6NWg<57>vYϳ<><CFB3><EFBFBD><EFBFBD><EFBFBD>Щr<D0A9>\n\t+<2B>1<EFBFBD><10>m{휑 <0C>hwb<><62><EFBFBD>8<EFBFBD><38>q y<>1e<31>)۱<>5m<35><6D><08><>MVM!<21>m<EFBFBD>[A<><41><10>{l<><6C>\t<EFBFBD>hia4<61><34>Tm<54><6D>8<><38>a<>e<EFBFBD>}<7D> ߫<><DFAB><15>]MVpяG<D18F><47>֏<EFBFBD>jJ<"<22>A<EFBFBD>mO*<2A>P<EFBFBD> <0B><><><7F><EFBFBD><EFBFBD>ѧЛ\nendstream\nendobj\n26 0 obj\n<<\n/Length 478/Filter /FlateDecode\n>>\nstream\nx<EFBFBD>MSK<EFBFBD>9<08><>)<29><>*<04>O<EFBFBD>i<EFBFBD><69>,<2C><>o <20><>kS%<25>$<EFBFBD><EFBFBD>hR\rS'<27>I<EFBFBD><49>~<7E><03><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T[/<2F>{<05>k<EFBFBD>FC#<23><>֛<><D69B><EFBFBD>;Ӏ<>[<5B>⫀m<E2AB80>|Q<1F><>\x1b<EFBFBD><16>><3E> R<><52><EFBFBD><EFBFBD><EFBFBD>a<EFBFBD>E#<23>pI<70><49>._H<5F>ᆫt<E186AB>k<EFBFBD>D3p<33>I<EFBFBD><49><EFBFBD><EFBFBD><01>W2<57><32><EFBFBD>oJ0<4A>j<EFBFBD><6A><EFBFBD>j#<23><>!<21>$<EFBFBD><EFBFBD>-<2D><08><><EFBFBD><EFBFBD><EFBFBD><><CEAB><EFBFBD>TI|8D<38>H<1C><>Y<EFBFBD><59>x<EFBFBD><78><EFBFBD><EFBFBD>1<EFBFBD>73%<25>u<EFBFBD>T<EFBFBD><54>Ӑ.rcb<63>x<EFBFBD><78>Dd6=<3D><>Oڏ1 ^<5E>-<2D>...and 252354 more chars`