IO-2433 Basic completion webhook, S3 upload, audit trail.
This commit is contained in:
@@ -6,19 +6,21 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectEsignature } from "../../redux/modals/modals.selectors";
|
||||
import { EmbedUpdateDocumentV1 } from "@documenso/embed-react";
|
||||
import axios from "axios";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
esignatureModal: selectEsignature
|
||||
esignatureModal: selectEsignature,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("esignature"))
|
||||
});
|
||||
|
||||
export function EsignatureModalContainer({ esignatureModal, toggleModalVisible }) {
|
||||
export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const { open, context } = esignatureModal;
|
||||
const { token, envelopeId, documentId } = context;
|
||||
const { token, envelopeId, documentId, jobid } = context;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -40,7 +42,7 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible }
|
||||
presignToken={token}
|
||||
host="https://stg-app.documenso.com"
|
||||
documentId={documentId}
|
||||
externalId="order-12345"
|
||||
externalId={jobid}
|
||||
className="esignature-embed"
|
||||
onDocumentUpdated={(data) => {
|
||||
console.log("Document updated:", data.documentId);
|
||||
@@ -53,7 +55,12 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible }
|
||||
onClick={async () => {
|
||||
// Add your button click handler logic here
|
||||
try {
|
||||
const distResult = await axios.post("/esign/distribute", { documentId, envelopeId });
|
||||
const distResult = await axios.post("/esign/distribute", {
|
||||
documentId,
|
||||
envelopeId,
|
||||
jobid,
|
||||
bodyshopid: bodyshop.id
|
||||
});
|
||||
console.log("Distribution result:", distResult);
|
||||
} catch (error) {
|
||||
console.error("Error distributing document:", error);
|
||||
|
||||
@@ -64,7 +64,7 @@ export function PrintCenterItemComponent({
|
||||
data: { token, documentId, evnelopeId }
|
||||
} = await axios.post("/esign/new", {
|
||||
name: item.key,
|
||||
variables: { id: id },
|
||||
jobid: id,
|
||||
context,
|
||||
bodyshop,
|
||||
templateObject: {
|
||||
@@ -73,7 +73,7 @@ export function PrintCenterItemComponent({
|
||||
}
|
||||
});
|
||||
|
||||
setEsignatureContext({ context: { token, documentId, evnelopeId }, jobid: id });
|
||||
setEsignatureContext({ context: { token, documentId, evnelopeId, jobid: id } });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
|
||||
@@ -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"
|
||||
// }
|
||||
@@ -2,51 +2,258 @@
|
||||
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 result = await documenso.documents.download({
|
||||
documentId: req.body.payload.id,
|
||||
const message = req.body
|
||||
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
|
||||
event: message.event,
|
||||
body: message
|
||||
});
|
||||
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)
|
||||
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 (err) {
|
||||
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);
|
||||
} 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: {
|
||||
@@ -147,4 +354,35 @@ module.exports = {
|
||||
// },
|
||||
// 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>qy<>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`
|
||||
95
server/esign/webhook.types.ts
Normal file
95
server/esign/webhook.types.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export type WebhookEventType =
|
||||
| "DOCUMENT_CREATED"
|
||||
| "DOCUMENT_SENT"
|
||||
| "DOCUMENT_COMPLETED"
|
||||
| "DOCUMENT_REJECTED"
|
||||
| "DOCUMENT_CANCELLED"
|
||||
| "DOCUMENT_OPENED"
|
||||
| "DOCUMENT_SIGNED";
|
||||
|
||||
export interface AuthOptions {
|
||||
accessAuth: unknown[];
|
||||
actionAuth: unknown[];
|
||||
}
|
||||
|
||||
export interface Recipient {
|
||||
id: number;
|
||||
name: string;
|
||||
role: string;
|
||||
email: string;
|
||||
token?: string | null;
|
||||
signedAt?: string | null;
|
||||
expiresAt?: string | null;
|
||||
documentId?: number;
|
||||
readStatus?: string | null;
|
||||
sendStatus?: string | null;
|
||||
templateId?: number | null;
|
||||
authOptions?: AuthOptions;
|
||||
signingOrder?: number | null;
|
||||
signingStatus?: string | null;
|
||||
rejectionReason?: string | null;
|
||||
documentDeletedAt?: string | null;
|
||||
expirationNotifiedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface EmailSettings {
|
||||
documentDeleted: boolean;
|
||||
documentPending: boolean;
|
||||
recipientSigned: boolean;
|
||||
recipientRemoved: boolean;
|
||||
documentCompleted: boolean;
|
||||
ownerDocumentCompleted: boolean;
|
||||
recipientSigningRequest: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentMeta {
|
||||
id: string;
|
||||
message?: string | null;
|
||||
subject?: string | null;
|
||||
language?: string | null;
|
||||
timezone?: string | null;
|
||||
dateFormat?: string | null;
|
||||
redirectUrl?: string | null;
|
||||
signingOrder?: string | null;
|
||||
emailSettings?: EmailSettings;
|
||||
distributionMethod?: string | null;
|
||||
drawSignatureEnabled?: boolean;
|
||||
typedSignatureEnabled?: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentAuthOptions {
|
||||
globalAccessAuth: unknown[];
|
||||
globalActionAuth: unknown[];
|
||||
}
|
||||
|
||||
export interface DocumentPayload {
|
||||
id: number;
|
||||
title?: string | null;
|
||||
source?: string | null;
|
||||
status?: string | null;
|
||||
teamId?: number | null;
|
||||
userId?: number | null;
|
||||
Recipient?: Recipient[];
|
||||
recipients?: Recipient[];
|
||||
createdAt?: string | null;
|
||||
deletedAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
externalId?: string | null;
|
||||
formValues?: unknown | null;
|
||||
templateId?: number | null;
|
||||
visibility?: string | null;
|
||||
authOptions?: DocumentAuthOptions;
|
||||
completedAt?: string | null;
|
||||
documentMeta?: DocumentMeta | null;
|
||||
}
|
||||
|
||||
export interface WebhookEventPayload {
|
||||
event: WebhookEventType;
|
||||
payload: DocumentPayload;
|
||||
createdAt?: string | null;
|
||||
webhookEndpoint?: string | null;
|
||||
}
|
||||
|
||||
export default WebhookEventPayload;
|
||||
@@ -2251,18 +2251,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
|
||||
|
||||
exports.INSERT_NEW_TRANSITION = (
|
||||
includeOldTransition
|
||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
|
||||
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||
}) {
|
||||
insert_transitions_one(object: $newTransition) {
|
||||
id
|
||||
}
|
||||
${
|
||||
includeOldTransition
|
||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||
${includeOldTransition
|
||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||
affected_rows
|
||||
}`
|
||||
: ""
|
||||
: ""
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -3248,3 +3246,46 @@ exports.SET_JOB_DMS_ID = `mutation SetJobDmsId($id: uuid!, $dms_id: String!, $dm
|
||||
kmin
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
exports.QUERY_JOB_FOR_SIGNATURE = `query QUERY_JOB_FOR_SIGNATURE($jobid: uuid!) {
|
||||
jobs_by_pk(id: $jobid) {
|
||||
id
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_ph1
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
exports.INSERT_ESIG_AUDIT_TRAIL = `mutation INSERT_ESIG_AUDIT_TRAIL($obj: audit_trail_insert_input!) {
|
||||
insert_audit_trail_one(object: $obj) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
exports.QUERY_META_FOR_ESIG_COMPLETION = `query QUERY_META_FOR_ESIG_COMPLETION($jobid: uuid!) {
|
||||
jobs_by_pk(id: $jobid) {
|
||||
id
|
||||
ro_number
|
||||
bodyshop {
|
||||
id
|
||||
uselocalmediaserver
|
||||
localmediatoken
|
||||
localmediaserverhttp
|
||||
localmediaservernetwork
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
exports.INSERT_ESIGNATURE_DOCUMENT = `mutation INSERT_ESIGNATURE_DOCUMENT($docInput: documents_insert_input!) {
|
||||
insert_documents_one(object: $docInput) {
|
||||
id
|
||||
name
|
||||
key
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -94,6 +94,47 @@ const generateSignedUploadUrls = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file buffer directly to S3.
|
||||
* Accepts either `req.file.buffer` (e.g. from multer) or `req.body.buffer` (base64 string).
|
||||
*/
|
||||
const uploadFileBuffer = async ({ key, contentType, buffer }) => {
|
||||
try {
|
||||
|
||||
|
||||
if (!key) {
|
||||
throw new Error("key is required");
|
||||
}
|
||||
if (!buffer) {
|
||||
throw new Error("buffer is required");
|
||||
}
|
||||
|
||||
const isPdf = key.toLowerCase().endsWith(".pdf");
|
||||
const client = new S3Client({ region: InstanceRegion() });
|
||||
|
||||
const putParams = {
|
||||
Bucket: imgproxyDestinationBucket,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
StorageClass: "INTELLIGENT_TIERING"
|
||||
};
|
||||
|
||||
if (contentType) {
|
||||
putParams.ContentType = contentType;
|
||||
} else if (isPdf) {
|
||||
putParams.ContentType = "application/pdf";
|
||||
}
|
||||
|
||||
await client.send(new PutObjectCommand(putParams));
|
||||
|
||||
|
||||
return ({ success: true, key, bucket: imgproxyDestinationBucket });
|
||||
} catch (error) {
|
||||
|
||||
return { success: false, message: error.message, stack: error.stack };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Thumbnail URLS
|
||||
* @param req
|
||||
@@ -500,6 +541,7 @@ const keyStandardize = (doc) => {
|
||||
|
||||
module.exports = {
|
||||
generateSignedUploadUrls,
|
||||
uploadFileBuffer,
|
||||
getThumbnailUrls,
|
||||
getOriginalImageByDocumentId,
|
||||
downloadFiles,
|
||||
|
||||
@@ -8,9 +8,9 @@ const { esignWebhook } = require("../esign/webhook");
|
||||
|
||||
//router.use(validateFirebaseIdTokenMiddleware);
|
||||
|
||||
router.post("/new", withUserGraphQLClientMiddleware, newEsignDocument);
|
||||
router.post("/distribute", withUserGraphQLClientMiddleware, distributeDocument);
|
||||
router.post("/webhook", withUserGraphQLClientMiddleware, esignWebhook);
|
||||
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
|
||||
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
|
||||
router.post("/webhook", esignWebhook);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user