IO-2433 Basic completion webhook, S3 upload, audit trail.

This commit is contained in:
Patrick Fic
2026-02-27 15:44:23 -08:00
parent e25174ff97
commit 52f43a600c
8 changed files with 559 additions and 86 deletions

View File

@@ -2,23 +2,43 @@
const { Documenso } = require("@documenso/sdk-typescript");
const axios = require("axios");
const { jsrAuthString } = require("../utils/utils");
const logger = require("../utils/logger");
const DOCUMENSO_API_KEY = "api_asojim0czruv13ud";//Done on a by team basis,
const documenso = new Documenso({
apiKey: DOCUMENSO_API_KEY,//Done on a by team basis,
serverURL: "https://stg-app.documenso.com/api/v2",
});
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");
async function distributeDocument(req, res) {
try {
const client = req.userGraphQLClient;
const { documentId } = req.body;
const distributeResult = await documenso.documents.distribute({
documentId,
});
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
jobid: req.body.jobid,
bodyshopid: req.body.bodyshopid,
operation: `Esignature document with title ${distributeResult.title} (ID: ${documentId}) distributed to recipients.`,
useremail: req.user?.email,
type: 'esig-distribute'
}
})
res.json({ success: true, distributeResult });
} catch (error) {
console.error("Error distributing document:", error?.data);
logger.log(`esig-distribute-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while distributing the document." });
}
}
@@ -27,25 +47,32 @@ async function newEsignDocument(req, res) {
try {
const client = req.userGraphQLClient;
const { pdf: fileBuffer, esigFields } = await RenderTemplate({ client, req })
const { bodyshop } = req.body
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 createDocumentResponse = await documenso.documents.create({
payload: {
title: `Repair Authorization - ${new Date().toLocaleString()}`,
title: esigData?.title,
externalId: req.body.jobid,
recipients: [
{
email: "patrick.fic@convenient-brands.com",
name: "Customer Fullname",
email: "patrick@imexsystems.ca",//jobData.ownr_ea,
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
role: "SIGNER",
}
],
meta: {
timezone: "America/Vancouver",
timezone: bodyshop.timezone,
dateFormat: "MM/dd/yyyy hh:mm a",
language: "en",
subject: "Repair Authorization for ABC Collision",
message: "To perform repairs on your vehicle, we must receive digital authorization. Please review and sign the document to proceed with repairs. ",
subject: esigData?.subject,
message: esigData?.message,
}
},
file: fileBlob
@@ -55,35 +82,44 @@ async function newEsignDocument(req, res) {
documentId: createDocumentResponse.id,
});
if (esigFields && esigFields.length > 0) {
console.log("Adding placeholder fields.")
if (esigData?.fields && esigData.fields.length > 0) {
try {
// await axios.post(`https://stg-app.documenso.com/api/v2/envelope/field/create-many`, {
// envelopeId: createDocumentResponse.envelopeId,
// data: esigFields.map(sigField => ({ ...sigField, recipientId: result.recipients[0].id, }))
// }, {
// headers: {
// Authorization: DOCUMENSO_API_KEY
// }
// })
const fieldResult = await documenso.envelopes.fields.createMany({
await documenso.envelopes.fields.createMany({
envelopeId: createDocumentResponse.envelopeId,
data: esigFields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
data: esigData.fields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
});
} catch (error) {
console.log("Error adding placeholders", JSON.stringify(error, null, 2));
logger.log(`esig-new-fields-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
}
}
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
//add to job audit trail.
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
obj: {
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'
}
})
res.json({ token: presignToken.token, documentId: createDocumentResponse.id, envelopeId: createDocumentResponse.envelopeId });
}
catch (error) {
console.error("Error in newEsignDocument:", error);
logger.log(`esig-new-error`, "ERROR", "esig", "api", {
message: error.message, stack: error.stack,
body: req.body
});
res.status(500).json({ error: "An error occurred while creating the e-sign document." });
}
}
@@ -95,10 +131,9 @@ async function RenderTemplate({ req }) {
const jsreportClient = new jsreport("https://reports.test.imex.online", process.env.JSR_USER, process.env.JSR_PASSWORD);
const { templateObject, bodyshop } = req.body;
let { contextData, useShopSpecificTemplate, shopSpecificFolder, esigFields } = await fetchContextData({ templateObject, jsrAuth, req });
//TODO - Refactor to pull template content and render on server instead of posting back to client for rendering. This is necessary to get the rendered PDF buffer that we can then upload to Documenso.
const { ignoreCustomMargins } = { ignoreCustomMargins: false }// Templates[templateObject.name];
let { contextData, useShopSpecificTemplate, shopSpecificFolder, esigData } = await fetchContextData({ templateObject, jsrAuth, req });
const { ignoreCustomMargins } = { ignoreCustomMargins: false }// Templates[templateObject.name];
let reportRequest = {
template: {
name: useShopSpecificTemplate ? `/${bodyshop.imexshopid}/${templateObject.name}` : `/${templateObject.name}`,
@@ -138,32 +173,29 @@ async function RenderTemplate({ req }) {
//Check render object and download. It should be the PDF?
const pdfBuffer = await render.body()
return { pdf: pdfBuffer, esigFields }
return { pdf: pdfBuffer, esigData }
}
const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
const { bodyshop } = req.body
const server = "https://reports.test.imex.online";
//jsreport.headers["FirebaseAuthorization"] = req.headers.authorization;
const folders = await axios.get(`${server}/odata/folders`, {
const folders = await axios.get(`${JSR_SERVER}/odata/folders`, {
headers: { Authorization: jsrAuth }
});
const shopSpecificFolder = folders.data.value.find((f) => f.name === bodyshop.imexshopid);
const jsReportQueries = await axios.get(
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
{ headers: { Authorization: jsrAuth } }
);
const jsReportEsig = await axios.get(
`${server}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
{ headers: { Authorization: jsrAuth } }
);
let templateQueryToExecute;
let esigFields;
let esigData;
let useShopSpecificTemplate = false;
// let shopSpecificTemplate;
@@ -179,7 +211,7 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificEsig) {
esigFields = (atob(shopSpecificEsig.content));
esigData = (atob(shopSpecificEsig.content));
}
}
@@ -188,23 +220,14 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
useShopSpecificTemplate = false;
templateQueryToExecute = atob(generalTemplate.content);
}
if (!esigFields) {
if (!esigData) {
const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate && generalTemplate.content) {
esigFields = atob(generalTemplate?.content);
esigData = atob(generalTemplate?.content);
}
}
// Commented out for future revision debugging
// console.log('Template Object');
// console.dir(templateObject);
// console.log('Unmodified Query');
// console.dir(templateQueryToExecute);
// const hasFilters = templateObject?.filters?.length > 0;
// const hasSorters = templateObject?.sorters?.length > 0;
// const hasDefaultSorters = templateObject?.defaultSorters?.length > 0;
const client = req.userGraphQLClient;
@@ -219,11 +242,20 @@ const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
);
contextData = data;
}
let parsedEsigData
try {
parsedEsigData = esigData ? JSON.parse(esigData) : null;
} catch (error) {
console.log("Error parsing esig data", error);
parsedEsigData = {}
}
return {
contextData,
useShopSpecificTemplate,
shopSpecificFolder,
esigFields: esigFields ? JSON.parse(esigFields) : [] //TODO: Do the parsing earlier and harden this. Causes a lot of failures on mini format issues.
esigData: parsedEsigData
};
// }
@@ -234,3 +266,21 @@ module.exports = {
newEsignDocument,
distributeDocument
}
// const sample_esig_for_jsr = {
// "fields": [
// {
// "placeholder": "[[signature]]",
// "type": "SIGNATURE"
// },
// {
// "placeholder": "[[date]]",
// "type": "DATE"
// }
// ],
// "subject": "CASL Auth Set in JSR",
// "message": "CASL Message set in JSR",
// "title": "CASL Title set in JSR"
// }