489 lines
17 KiB
JavaScript
489 lines
17 KiB
JavaScript
|
|
const { Documenso } = require("@documenso/sdk-typescript");
|
|
const axios = require("axios");
|
|
const { jsrAuthString } = require("../utils/utils");
|
|
const logger = require("../utils/logger");
|
|
//Need to pull the key dynamically to send documents.
|
|
const JSR_SERVER = process.env.JSR_URL || "https://reports.imex.online";
|
|
const jsreport = require("@jsreport/nodejs-client");
|
|
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIGNATURE_DOCUMENT, DISTRIBUTE_ESIGNATURE_DOCUMENT, QUERY_ESIGNATURE_BY_EXTERNAL_ID, UPDATE_ESIGNATURE_DOCUMENT, QUERY_DOCUMENSO_KEY, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
|
|
const _ = require("lodash");
|
|
|
|
function parseJsonField(value, fallback = null) {
|
|
if (value === undefined || value === null) {
|
|
return fallback;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return value;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function getDefaultEsignData({ esigData, bodyshop, fileName }) {
|
|
const fallbackTitle = fileName || `Esign request from ${bodyshop.shopname}`;
|
|
|
|
return {
|
|
...esigData,
|
|
title: esigData?.title || fallbackTitle,
|
|
subject: esigData?.subject || `Esign request from ${bodyshop.shopname}`,
|
|
message: esigData?.message || `Please review and sign the document from ${bodyshop.shopname}.`
|
|
};
|
|
}
|
|
|
|
function createClientError(message, statusCode = 400) {
|
|
const error = new Error(message);
|
|
error.statusCode = statusCode;
|
|
return error;
|
|
}
|
|
|
|
function isValidEmail(email) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
}
|
|
|
|
function getJobOwnerName(jobData, email) {
|
|
const ownerName = [jobData?.ownr_fn, jobData?.ownr_ln].filter(Boolean).join(" ").trim();
|
|
return ownerName || jobData?.ownr_co_nm || email;
|
|
}
|
|
|
|
function getJobOwnerRecipients(jobData) {
|
|
const ownerEmail = jobData?.ownr_ea?.trim();
|
|
|
|
if (!ownerEmail) {
|
|
throw createClientError("Job owner email is required before sending an e-signature request.");
|
|
}
|
|
|
|
if (!isValidEmail(ownerEmail)) {
|
|
throw createClientError(`Job owner email "${ownerEmail}" is not valid.`);
|
|
}
|
|
|
|
return [
|
|
{
|
|
email: ownerEmail,
|
|
name: getJobOwnerName(jobData, ownerEmail),
|
|
role: "SIGNER"
|
|
}
|
|
];
|
|
}
|
|
|
|
async function getDocumensoClient({ bodyshopid, req }) {
|
|
const client = req.userGraphQLClient;
|
|
const { bodyshops_by_pk: { documenso_api_key } } = await client.request(QUERY_DOCUMENSO_KEY, { bodyshopid });
|
|
return new Documenso({
|
|
apiKey: documenso_api_key,//Done on a by team basis,
|
|
serverURL: "https://sign.imex.online/api/v2",
|
|
});
|
|
}
|
|
|
|
async function createEsignDocumentFromPdf({ req, bodyshop, pdfBuffer, esigData, fileName }) {
|
|
const resolvedEsigData = getDefaultEsignData({ esigData, bodyshop, fileName });
|
|
const fileBlob = new Blob([pdfBuffer], { type: "application/pdf" });
|
|
const jobid = req.body.jobid;
|
|
const client = req.userGraphQLClient;
|
|
|
|
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid });
|
|
const recipients = getJobOwnerRecipients(jobData);
|
|
|
|
const documenso = await getDocumensoClient({ bodyshopid: bodyshop.id, req })
|
|
|
|
const createDocumentResponse = await documenso.documents.create({
|
|
payload: {
|
|
title: resolvedEsigData.title,
|
|
externalId: `${jobid}|${req.user?.email}`,
|
|
recipients,
|
|
meta: {
|
|
timezone: bodyshop.timezone,
|
|
dateFormat: "MM/dd/yyyy hh:mm a",
|
|
language: "en",
|
|
subject: resolvedEsigData.subject,
|
|
message: resolvedEsigData.message,
|
|
}
|
|
},
|
|
file: fileBlob
|
|
});
|
|
|
|
const documentResult = await documenso.documents.get({
|
|
documentId: createDocumentResponse.id,
|
|
});
|
|
|
|
if (resolvedEsigData?.fields && resolvedEsigData.fields.length > 0) {
|
|
try {
|
|
await documenso.envelopes.fields.createMany({
|
|
envelopeId: createDocumentResponse.envelopeId,
|
|
data: resolvedEsigData.fields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
|
|
});
|
|
} catch (error) {
|
|
logger.log(`esig-new-fields-error`, "ERROR", "esig", "api", {
|
|
message: error.message, stack: error.stack,
|
|
body: req.body
|
|
});
|
|
}
|
|
}
|
|
|
|
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({});
|
|
|
|
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
|
|
audit: {
|
|
jobid,
|
|
bodyshopid: bodyshop.id,
|
|
operation: `Esignature document created. Subject: ${resolvedEsigData.subject || "No subject"}, Message: ${resolvedEsigData.message || "No message"}. Document ID: ${createDocumentResponse.id} Envlope ID: ${createDocumentResponse.envelopeId}`,
|
|
useremail: req.user?.email,
|
|
type: 'esig-create'
|
|
},
|
|
esig: {
|
|
jobid,
|
|
external_document_id: createDocumentResponse.id.toString(),
|
|
subject: resolvedEsigData.subject || "No subject",
|
|
message: resolvedEsigData.message || "No message",
|
|
title: resolvedEsigData.title || "No title",
|
|
status: "DRAFT",
|
|
recipients: recipients,
|
|
}
|
|
});
|
|
|
|
return {
|
|
token: presignToken.token,
|
|
documentId: createDocumentResponse.id,
|
|
envelopeId: createDocumentResponse.envelopeId
|
|
};
|
|
}
|
|
|
|
async function distributeDocument(req, res) {
|
|
try {
|
|
const client = req.userGraphQLClient;
|
|
|
|
const { documentId, bodyshopid } = req.body;
|
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
|
|
|
const distributeResult = await documenso.documents.distribute({
|
|
documentId,
|
|
});
|
|
|
|
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.`,
|
|
useremail: req.user?.email,
|
|
type: 'esig-distribute'
|
|
}
|
|
})
|
|
|
|
res.json({ success: true, distributeResult });
|
|
} catch (error) {
|
|
|
|
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.", message: error.message });
|
|
}
|
|
}
|
|
|
|
async function redistributeDocument(req, res) {
|
|
try {
|
|
const client = req.userGraphQLClient;
|
|
|
|
const { documentId, bodyshopid } = req.body;
|
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
|
|
|
|
|
const document = await documenso.documents.get({
|
|
documentId: parseInt(documentId)
|
|
});
|
|
|
|
const distributeResult = await documenso.documents.redistribute({
|
|
documentId: parseInt(documentId),
|
|
recipients: document.recipients.filter(r => r.signingStatus === "NOT_SIGNED").map(r => r.id)
|
|
});
|
|
|
|
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}) redistributed to recipients.`,
|
|
useremail: req.user?.email,
|
|
type: 'esig-redistribute'
|
|
}
|
|
})
|
|
|
|
res.json({ success: true, distributeResult });
|
|
} catch (error) {
|
|
|
|
logger.log(`esig-redistribute-error`, "ERROR", "esig", "api", {
|
|
message: error.message, stack: error.stack,
|
|
body: req.body
|
|
});
|
|
res.status(500).json({ error: "An error occurred while redistributing the document.", message: error.message });
|
|
}
|
|
}
|
|
|
|
async function deleteDocument(req, res) {
|
|
try {
|
|
const client = req.userGraphQLClient;
|
|
|
|
const { documentId, bodyshopid } = req.body;
|
|
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 documenso = await getDocumensoClient({ bodyshopid, req })
|
|
|
|
const deleteResult = await documenso.documents.delete({
|
|
documentId: (documentId)
|
|
});
|
|
|
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
|
external_document_id: documentId.toString(),
|
|
esig_update: {
|
|
status: "DELETED"
|
|
}
|
|
})
|
|
res.json({ success: true, deleteResult });
|
|
} catch (error) {
|
|
|
|
logger.log(`esig-delete-error`, "ERROR", "esig", "api", {
|
|
message: error.message, stack: error.stack,
|
|
body: req.body
|
|
});
|
|
res.status(500).json({ error: "An error occurred while deleting the document." });
|
|
}
|
|
}
|
|
|
|
async function viewDocument(req, res) {
|
|
try {
|
|
const { documentId, bodyshopid } = req.body;
|
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
|
|
|
const document = await documenso.document.documentDownload({
|
|
documentId: parseInt(documentId)
|
|
});
|
|
res.json({ success: true, document });
|
|
} catch (error) {
|
|
|
|
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;
|
|
const { bodyshop } = req.body;
|
|
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req });
|
|
const result = await createEsignDocumentFromPdf({
|
|
req,
|
|
bodyshop,
|
|
pdfBuffer: fileBuffer,
|
|
esigData
|
|
});
|
|
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
logger.log(`esig-new-error`, "ERROR", "esig", "api", {
|
|
message: error.message, stack: error.stack,
|
|
body: _.omit(req.body, ["bodyshop"]) // bodyshop can be large, so we omit it from the logs
|
|
});
|
|
res.status(error.statusCode || 500).json({ error: "An error occurred while creating the e-sign document.", message: error.message });
|
|
}
|
|
}
|
|
|
|
async function newCustomEsignDocument(req, res) {
|
|
try {
|
|
const bodyshop = parseJsonField(req.body.bodyshop, req.body.bodyshop);
|
|
const esigData = parseJsonField(req.body.esigData, {});
|
|
const uploadedDocument = req.file;
|
|
|
|
if (!uploadedDocument?.buffer) {
|
|
return res.status(400).json({ error: "A PDF document is required." });
|
|
}
|
|
|
|
if (uploadedDocument.mimetype !== "application/pdf") {
|
|
return res.status(400).json({ error: "Only PDF documents can be used for e-signature." });
|
|
}
|
|
|
|
req.body.bodyshop = bodyshop;
|
|
|
|
const fileName = uploadedDocument.originalname?.replace(/\.[^.]+$/, "") || undefined;
|
|
const result = await createEsignDocumentFromPdf({
|
|
req,
|
|
bodyshop,
|
|
pdfBuffer: uploadedDocument.buffer,
|
|
esigData,
|
|
fileName
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
logger.log(`esig-new-custom-error`, "ERROR", "esig", "api", {
|
|
message: error.message, stack: error.stack,
|
|
body: _.omit(req.body, ["bodyshop"]) // bodyshop can be large, so we omit it from the logs
|
|
});
|
|
res.status(error.statusCode || 500).json({ error: "An error occurred while creating the custom e-sign document.", message: error.message });
|
|
}
|
|
}
|
|
|
|
async function RenderTemplate({ req }) {
|
|
const jsrAuth = jsrAuthString()
|
|
const jsreportClient = new jsreport(JSR_SERVER, process.env.JSR_USER, process.env.JSR_PASSWORD);
|
|
const { templateObject, bodyshop } = req.body;
|
|
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}`,
|
|
|
|
recipe: "chrome-pdf",
|
|
...(!ignoreCustomMargins && {
|
|
chrome: {
|
|
marginTop:
|
|
bodyshop.logo_img_path &&
|
|
bodyshop.logo_img_path.headerMargin &&
|
|
bodyshop.logo_img_path.headerMargin > 36
|
|
? bodyshop.logo_img_path.headerMargin
|
|
: "36px",
|
|
marginBottom:
|
|
bodyshop.logo_img_path &&
|
|
bodyshop.logo_img_path.footerMargin &&
|
|
bodyshop.logo_img_path.footerMargin > 50
|
|
? bodyshop.logo_img_path.footerMargin
|
|
: "50px"
|
|
}
|
|
}),
|
|
},
|
|
data: {
|
|
...contextData,
|
|
...templateObject.variables,
|
|
...templateObject.context,
|
|
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
|
|
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
|
|
bodyshop: bodyshop,
|
|
esignature: true,
|
|
filters: templateObject?.filters,
|
|
sorters: templateObject?.sorters,
|
|
offset: bodyshop.timezone, //dayjs().utcOffset(),
|
|
defaultSorters: templateObject?.defaultSorters
|
|
}
|
|
};
|
|
const render = await jsreportClient.render(reportRequest);
|
|
|
|
//Check render object and download. It should be the PDF?
|
|
const pdfBuffer = await render.body()
|
|
return { pdf: pdfBuffer, esigData }
|
|
}
|
|
|
|
const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
|
|
const { bodyshop } = req.body
|
|
|
|
|
|
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(
|
|
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
|
{ headers: { Authorization: jsrAuth } }
|
|
);
|
|
const jsReportEsig = await axios.get(
|
|
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
|
|
{ headers: { Authorization: jsrAuth } }
|
|
);
|
|
|
|
let templateQueryToExecute;
|
|
let esigData;
|
|
let useShopSpecificTemplate = false;
|
|
// let shopSpecificTemplate;
|
|
|
|
if (shopSpecificFolder) {
|
|
let shopSpecificTemplate = jsReportQueries.data.value.find(
|
|
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
|
);
|
|
if (shopSpecificTemplate) {
|
|
useShopSpecificTemplate = true;
|
|
templateQueryToExecute = atob(shopSpecificTemplate.content);
|
|
}
|
|
let shopSpecificEsig = jsReportEsig.data.value.find(
|
|
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
|
);
|
|
if (shopSpecificEsig) {
|
|
esigData = (atob(shopSpecificEsig.content));
|
|
}
|
|
}
|
|
|
|
if (!templateQueryToExecute) {
|
|
const generalTemplate = jsReportQueries.data.value.find((f) => !f.folder);
|
|
useShopSpecificTemplate = false;
|
|
templateQueryToExecute = atob(generalTemplate.content);
|
|
}
|
|
if (!esigData) {
|
|
const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder);
|
|
useShopSpecificTemplate = false;
|
|
if (generalTemplate && generalTemplate.content) {
|
|
esigData = atob(generalTemplate?.content);
|
|
}
|
|
}
|
|
|
|
const client = req.userGraphQLClient;
|
|
|
|
|
|
// In the print center, we will never have sorters or filters.
|
|
// We have no template filters or sorters, so we can just execute the query and return the data
|
|
// if (!hasFilters && !hasSorters && !hasDefaultSorters) {
|
|
let contextData = {};
|
|
if (templateQueryToExecute) {
|
|
const data = await client.request(
|
|
templateQueryToExecute,
|
|
templateObject.variables,
|
|
);
|
|
contextData = data;
|
|
}
|
|
|
|
let parsedEsigData
|
|
try {
|
|
parsedEsigData = esigData ? JSON.parse(esigData) : null;
|
|
} catch (error) {
|
|
logger.log(`esig-data-parse-error`, "ERROR", "esig", "api", {
|
|
message: error.message, stack: error.stack,
|
|
esigData,
|
|
body: req.body
|
|
});
|
|
parsedEsigData = {}
|
|
}
|
|
|
|
return {
|
|
contextData,
|
|
useShopSpecificTemplate,
|
|
shopSpecificFolder,
|
|
esigData: parsedEsigData
|
|
};
|
|
// }
|
|
|
|
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
|
|
};
|
|
|
|
module.exports = {
|
|
newEsignDocument,
|
|
newCustomEsignDocument,
|
|
distributeDocument,
|
|
redistributeDocument,
|
|
deleteDocument,
|
|
viewDocument,
|
|
getDocumensoClient
|
|
}
|