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 DOCUMENSO_SERVER_URL = process.env.DOCUMENSO_SERVER_URL || "https://sign.imex.online/api/v2"; 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 { apiKey } = await getDocumensoConfig({ bodyshopid, req }); return new Documenso({ apiKey, serverURL: DOCUMENSO_SERVER_URL }); } async function getDocumensoConfig({ bodyshopid, req }) { const client = req.userGraphQLClient; const { bodyshops_by_pk: { documenso_api_key } } = await client.request(QUERY_DOCUMENSO_KEY, { bodyshopid }); return { apiKey: documenso_api_key, serverURL: DOCUMENSO_SERVER_URL }; } async function getDocumensoDocument({ apiKey, documentId }) { const { data } = await axios.get(`${DOCUMENSO_SERVER_URL}/document/${encodeURIComponent(documentId)}`, { headers: { Accept: "application/json", Authorization: apiKey } }); return data; } 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 documensoConfig = await getDocumensoConfig({ bodyshopid: bodyshop.id, req }); const documenso = new Documenso({ apiKey: documensoConfig.apiKey, serverURL: documensoConfig.serverURL }); 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 getDocumensoDocument({ apiKey: documensoConfig.apiKey, 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 documensoConfig = await getDocumensoConfig({ bodyshopid, req }); const documenso = new Documenso({ apiKey: documensoConfig.apiKey, serverURL: documensoConfig.serverURL }); const document = await getDocumensoDocument({ apiKey: documensoConfig.apiKey, 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 };