Eisgnature Migrations, webhook handling, and clean up.

This commit is contained in:
Patrick Fic
2026-03-25 15:24:14 -07:00
parent e17b57c705
commit d4c7298334
23 changed files with 615 additions and 67 deletions

View File

@@ -10,7 +10,7 @@ const documenso = new Documenso({
});
const JSR_SERVER = "https://reports.test.imex.online";
const jsreport = require("@jsreport/nodejs-client");
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIGNATURE_DOCUMENT, DISTRIBUTE_ESIGNATURE_DOCUMENT, QUERY_ESIGNATURE_BY_EXTERNAL_ID, UPDATE_ESIGNATURE_DOCUMENT } = require("../graphql-client/queries");
async function distributeDocument(req, res) {
@@ -22,8 +22,12 @@ async function distributeDocument(req, res) {
documentId,
});
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
const auditEntry = await client.request(DISTRIBUTE_ESIGNATURE_DOCUMENT, {
external_document_id: documentId.toString(),
esig_update: {
status: "SENT"
},
audit: {
jobid: req.body.jobid,
bodyshopid: req.body.bodyshopid,
operation: `Esignature document with title ${distributeResult.title} (ID: ${documentId}) distributed to recipients.`,
@@ -39,17 +43,31 @@ async function distributeDocument(req, res) {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while distributing the document." });
res.status(500).json({ error: "An error occurred while distributing the document.", message: error.message });
}
}
async function deleteDocument(req, res) {
try {
//TODO: Add in logic to check if doc exists, is deletable etc.
const client = req.userGraphQLClient;
const { documentId } = req.body;
//TODO: This needs to be hardened to prevent deleting other people's documents, completed ones, etc.
const { esignature_documents } = await client.request(QUERY_ESIGNATURE_BY_EXTERNAL_ID, { external_document_id: documentId.toString() });
if (!esignature_documents || esignature_documents.length === 0) {
//return res.status(404).json({ error: "Document not found" });
}
const deleteResult = await documenso.documents.delete({
documentId
documentId: (documentId)
});
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
external_document_id: documentId.toString(),
esig_update: {
status: "DELETED"
}
})
res.json({ success: true, deleteResult });
} catch (error) {
console.error("Error deleting document:", error?.data);
@@ -61,6 +79,23 @@ async function deleteDocument(req, res) {
}
}
async function viewDocument(req, res) {
try {
const { documentId } = req.body;
const document = await documenso.document.documentDownload({
documentId: parseInt(documentId)
});
res.json({ success: true, document });
} catch (error) {
console.error("Error viewing document:", error?.data);
logger.log(`esig-view-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while retrieving the document.", message: error.message });
}
}
async function newEsignDocument(req, res) {
try {
const client = req.userGraphQLClient;
@@ -68,21 +103,18 @@ async function newEsignDocument(req, res) {
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req })
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
//Get the Job data.
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid: req.body.jobid });
const recipients = [{
email: "patrick@imexsystems.ca",//jobData.ownr_ea,
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
role: "SIGNER",
}]
const createDocumentResponse = await documenso.documents.create({
payload: {
title: esigData?.title || `Esign request from ${bodyshop.shopname}`,
externalId: `${req.body.jobid}|${req.user?.email}`, //Have to pass the uploaded by later on. Limited to 255 chars.
recipients: [
{
email: "allan@imexsystems.ca",//jobData.ownr_ea,
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
role: "SIGNER",
}
],
recipients,
meta: {
timezone: bodyshop.timezone,
dateFormat: "MM/dd/yyyy hh:mm a",
@@ -99,7 +131,6 @@ async function newEsignDocument(req, res) {
documentId: createDocumentResponse.id,
});
if (esigData?.fields && esigData.fields.length > 0) {
try {
await documenso.envelopes.fields.createMany({
@@ -117,16 +148,23 @@ async function newEsignDocument(req, res) {
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
//add to job audit trail.
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
const auditEntry = await client.request(INSERT_ESIGNATURE_DOCUMENT, {
audit: {
jobid: req.body.jobid,
bodyshopid: bodyshop.id,
operation: `Esignature document created. Subject: ${esigData?.subject || "No subject"}, Message: ${esigData?.message || "No message"}. Document ID: ${createDocumentResponse.id} Envlope ID: ${createDocumentResponse.envelopeId}`,
useremail: req.user?.email,
type: 'esig-create'
},
esig: {
jobid: req.body.jobid,
external_document_id: createDocumentResponse.id.toString(),
//envelope_id: createDocumentResponse.envelopeId,
subject: esigData?.subject || "No subject",
message: esigData?.message || "No message",
title: esigData?.title || "No title",
status: "DRAFT",
recipients: recipients,
}
})
@@ -283,7 +321,8 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
module.exports = {
newEsignDocument,
distributeDocument,
deleteDocument
deleteDocument,
viewDocument
}

View File

@@ -1,10 +1,10 @@
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 { QUERY_META_FOR_ESIG_COMPLETION, INSERT_ESIGNATURE_COMPLETED_DOCOUMENT, UPDATE_ESIGNATURE_DOCUMENT, DISTRIBUTE_ESIGNATURE_DOCUMENT } = require("../graphql-client/queries");
const { uploadFileBuffer } = require("../media/imgproxy-media");
const { log } = require("node-persist");
const client = require('../graphql-client/graphql-client').client;
const documenso = new Documenso({
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
@@ -30,38 +30,56 @@ async function esignWebhook(req, res) {
body: message
});
//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:
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
external_document_id: message.payload?.payload?.id?.toString(),
esig_update: {
status: "OPENED",
opened: true,
}
})
break;
case webhookTypeEnums.DOCUMENT_REJECTED:
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
external_document_id: message.payload?.payload?.id?.toString(),
esig_update: {
status: "REJECTED",
rejected: true,
}
})
break;
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);
console.log("Document created event received. Document ID:", message.payload?.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);
console.log("Document completed event received. Document ID:", message.payload?.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);
console.log("Document signed event received. Document ID:", message.payload?.payload?.documentId);
// 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: message.payload?.payload?.id?.toString(),
esig_update: {
status: "SIGNED",
}
})
break;
default:
console.log(`Unhandled event type: ${message.event}`);
res.status(200).json({ message: "Unsupported event type." });
logger.log(`esig-webhook-received-unknown`, "ERROR", "redis", "api", {
event: message.event,
body: message
});
return;
}
// 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)
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) {
@@ -69,25 +87,14 @@ async function esignWebhook(req, res) {
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)
res.status(500).json({ message: "Error processing webhook event.", error: error.message });
}
}
async function handleDocumentCompleted(payload = sampleComplete) {
//Check if the bodyshop is on image proxy or not
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}"`);
}
@@ -106,7 +113,7 @@ async function handleDocumentCompleted(payload = sampleComplete) {
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) {
//LMS not yet implemented.
//TODO:LMS not yet implemented.
} else {
//S3 Upload
@@ -125,8 +132,15 @@ async function handleDocumentCompleted(payload = sampleComplete) {
s3Key: key,
bucket: uploadResult.bucket
});
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
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.`,
@@ -134,8 +148,10 @@ async function handleDocumentCompleted(payload = sampleComplete) {
type: 'esig-complete'
}
})
//insert the document record with the s3 key and bucket info.
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
await client.request(INSERT_ESIGNATURE_COMPLETED_DOCOUMENT, {
docInput: {
jobid: jobs_by_pk.id,
uploaded_by: uploaded_by,

View File

@@ -3261,6 +3261,45 @@ exports.QUERY_JOB_FOR_SIGNATURE = `query QUERY_JOB_FOR_SIGNATURE($jobid: uuid!)
}
}
`
exports.INSERT_ESIGNATURE_DOCUMENT = `mutation INSERT_ESIGNATURE_DOCUMENT($audit: audit_trail_insert_input!, $esig: esignature_documents_insert_input!) {
insert_audit_trail_one(object: $audit) {
id
}
insert_esignature_documents_one(object: $esig){
id
}
}`
exports.QUERY_ESIGNATURE_BY_EXTERNAL_ID = `query QUERY_ESIGNATURE_BY_EXTERNAL_ID($external_document_id: String!) {
esignature_documents(where: {external_document_id: {_eq: $external_document_id}}) {
id
jobid
external_document_id
}
}`
exports.DISTRIBUTE_ESIGNATURE_DOCUMENT = `mutation DISTRIBUTE_ESIGNATURE_DOCUMENT($external_document_id: String!, $esig_update: esignature_documents_set_input!, $audit: audit_trail_insert_input!) {
insert_audit_trail_one(object: $audit) {
id
}
update_esignature_documents(where: {external_document_id: {_eq: $external_document_id}}, _set: $esig_update) {
affected_rows
returning {
id
}
}
}
`
exports.UPDATE_ESIGNATURE_DOCUMENT = `mutation UPDATE_ESIGNATURE_DOCUMENT($external_document_id: String!, $esig_update: esignature_documents_set_input!) {
update_esignature_documents(where: {external_document_id: {_eq: $external_document_id}}, _set: $esig_update) {
affected_rows
returning {
id
}
}
}
`
exports.INSERT_ESIG_AUDIT_TRAIL = `mutation INSERT_ESIG_AUDIT_TRAIL($obj: audit_trail_insert_input!) {
insert_audit_trail_one(object: $obj) {
@@ -3283,7 +3322,7 @@ exports.QUERY_META_FOR_ESIG_COMPLETION = `query QUERY_META_FOR_ESIG_COMPLETION($
}
}`
exports.INSERT_ESIGNATURE_DOCUMENT = `mutation INSERT_ESIGNATURE_DOCUMENT($docInput: documents_insert_input!) {
exports.INSERT_ESIGNATURE_COMPLETED_DOCOUMENT = `mutation INSERT_ESIGNATURE_COMPLETED_DOCOUMENT($docInput: documents_insert_input!) {
insert_documents_one(object: $docInput) {
id
name

View File

@@ -3,7 +3,7 @@ const router = express.Router();
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
const { newEsignDocument, distributeDocument, deleteDocument } = require("../esign/esign-new");
const { newEsignDocument, distributeDocument, viewDocument, deleteDocument } = require("../esign/esign-new");
const { esignWebhook } = require("../esign/webhook");
//router.use(validateFirebaseIdTokenMiddleware);
@@ -11,6 +11,7 @@ const { esignWebhook } = require("../esign/webhook");
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
router.post("/delete", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, deleteDocument);
router.post("/view", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, viewDocument);
router.post("/webhook", esignWebhook);