Eisgnature Migrations, webhook handling, and clean up.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<babeledit_project be_version="2.7.1" version="1.2">
|
<babeledit_project version="1.2" be_version="2.7.1">
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
BabelEdit project file
|
BabelEdit project file
|
||||||
@@ -20487,6 +20487,37 @@
|
|||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>esignature</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>actions</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>distribute</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>eula</name>
|
<name>eula</name>
|
||||||
<children>
|
<children>
|
||||||
@@ -37705,6 +37736,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>esignature</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>estimatelines</name>
|
<name>estimatelines</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EmbedUpdateDocumentV1 } from "@documenso/embed-react";
|
import { EmbedUpdateDocumentV1 } from "@documenso/embed-react";
|
||||||
import { Modal } from "antd";
|
import { Modal, notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -7,6 +7,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectEsignature } from "../../redux/modals/modals.selectors";
|
import { selectEsignature } from "../../redux/modals/modals.selectors";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
esignatureModal: selectEsignature,
|
esignatureModal: selectEsignature,
|
||||||
@@ -22,13 +23,14 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { open, context } = esignatureModal;
|
const { open, context } = esignatureModal;
|
||||||
const { token, envelopeId, documentId, jobid } = context;
|
const { token, envelopeId, documentId, jobid } = context;
|
||||||
|
const [distributing, setDistributing] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
title={t("jobs.labels.esignature")}
|
title={t("jobs.labels.esignature")}
|
||||||
onOk={async () => {
|
onOk={async () => {
|
||||||
try {
|
try {
|
||||||
|
setDistributing(true);
|
||||||
const distResult = await axios.post("/esign/distribute", {
|
const distResult = await axios.post("/esign/distribute", {
|
||||||
documentId,
|
documentId,
|
||||||
envelopeId,
|
envelopeId,
|
||||||
@@ -39,7 +41,12 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error distributing document:", error);
|
console.error("Error distributing document:", error);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.distribute_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
setDistributing(false);
|
||||||
}}
|
}}
|
||||||
onCancel={async () => {
|
onCancel={async () => {
|
||||||
try {
|
try {
|
||||||
@@ -51,11 +58,16 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error cancelling document:", error);
|
console.error("Error cancelling document:", error);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.cancel_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
okButtonProps={{ title: "Distribute by Email" }}
|
okButtonProps={{ loading: distributing }}
|
||||||
width="90%"
|
okText={t("esignature.actions.distribute")}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
|
width={800}
|
||||||
>
|
>
|
||||||
<div style={{ height: "600px", width: "100%" }}>
|
<div style={{ height: "600px", width: "100%" }}>
|
||||||
{token ? (
|
{token ? (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import { Button, Card, Col, Row, Tag } from "antd";
|
import { Button, Card, Checkbox, Col, Row, Space, Tag } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -12,6 +12,8 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|||||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -23,6 +25,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobAuditTrail);
|
|||||||
|
|
||||||
export function JobAuditTrail({ bodyshop, jobId }) {
|
export function JobAuditTrail({ bodyshop, jobId }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const notification = useNotification();
|
||||||
const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, {
|
const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||||
variables: { jobid: jobId },
|
variables: { jobid: jobId },
|
||||||
skip: !jobId,
|
skip: !jobId,
|
||||||
@@ -53,6 +56,125 @@ export function JobAuditTrail({ bodyshop, jobId }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
const esigColumns = [
|
||||||
|
{
|
||||||
|
title: t("audit.fields.created_at"),
|
||||||
|
dataIndex: "created_at",
|
||||||
|
key: "created_at",
|
||||||
|
render: (text) => <DateTimeFormatter>{text}</DateTimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.updated_at"),
|
||||||
|
dataIndex: "updated_at",
|
||||||
|
key: "updated_at",
|
||||||
|
render: (text) => <DateTimeFormatter>{text}</DateTimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.title"),
|
||||||
|
dataIndex: "title",
|
||||||
|
key: "title",
|
||||||
|
render: (text) => (
|
||||||
|
<BlurWrapperComponent featureName="audit" bypass>
|
||||||
|
<div>{text}</div>
|
||||||
|
</BlurWrapperComponent>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.external_document_id"),
|
||||||
|
dataIndex: "external_document_id",
|
||||||
|
key: "external_document_id",
|
||||||
|
render: (text) => (
|
||||||
|
<BlurWrapperComponent featureName="audit" bypass>
|
||||||
|
<div>{text}</div>
|
||||||
|
</BlurWrapperComponent>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
render: (text) => (
|
||||||
|
<BlurWrapperComponent featureName="audit" bypass>
|
||||||
|
<div>{text}</div>
|
||||||
|
</BlurWrapperComponent>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.opened"),
|
||||||
|
dataIndex: "opened",
|
||||||
|
key: "opened",
|
||||||
|
render: (text) => <Checkbox checked={text} disabled />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.rejected"),
|
||||||
|
dataIndex: "rejected",
|
||||||
|
key: "rejected",
|
||||||
|
render: (text) => <Checkbox checked={text} disabled />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.completed"),
|
||||||
|
dataIndex: "completed",
|
||||||
|
key: "completed",
|
||||||
|
render: (text) => <Checkbox checked={text} disabled />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.completed_at"),
|
||||||
|
dataIndex: "completed_at",
|
||||||
|
key: "completed_at",
|
||||||
|
render: (text) => <DateTimeFormatter>{text}</DateTimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
render: (_text, record) => (
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
disabled={record.completed_at !== null || record.status === "REJECTED"}
|
||||||
|
onClick={async () => {
|
||||||
|
logImEXEvent("job_esig_delete", {});
|
||||||
|
try {
|
||||||
|
const deleteResult = await axios.post("/esign/delete", {
|
||||||
|
documentId: record.external_document_id
|
||||||
|
});
|
||||||
|
console.log("*** ~ JobAuditTrail ~ deleteResult:", deleteResult);
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting document:", error?.response?.data || error.message);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.delete_error"),
|
||||||
|
description: error?.response?.data?.error || error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("esignature.actions.delete")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => console.log(record)}>{t("esignature.actions.resend")}</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
axios
|
||||||
|
.post("/esign/view", {
|
||||||
|
documentId: record.external_document_id
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
window.open(response.data?.document?.downloadUrl, "_blank");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error viewing document:", error?.response?.data || error.message);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.view_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("esignature.actions.view")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
const emailColumns = [
|
const emailColumns = [
|
||||||
{
|
{
|
||||||
title: t("audit.fields.created"),
|
title: t("audit.fields.created"),
|
||||||
@@ -184,6 +306,17 @@ export function JobAuditTrail({ bodyshop, jobId }) {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title={t("jobs.labels.esignatures")}>
|
||||||
|
<ResponsiveTable
|
||||||
|
loading={loading}
|
||||||
|
columns={esigColumns}
|
||||||
|
mobileColumnKeys={["title", "status"]}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data ? data.esignature_documents : []}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ export const QUERY_AUDIT_TRAIL = gql`
|
|||||||
useremail
|
useremail
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
|
esignature_documents(where: {jobid: {_eq: $jobid}}) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
jobid
|
||||||
|
external_document_id
|
||||||
|
subject
|
||||||
|
message
|
||||||
|
title
|
||||||
|
status
|
||||||
|
recipients
|
||||||
|
completed_at
|
||||||
|
opened
|
||||||
|
completed
|
||||||
|
rejected
|
||||||
|
completed_at
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1239,6 +1239,11 @@
|
|||||||
"unique_employee_number": "You must enter a unique employee number."
|
"unique_employee_number": "You must enter a unique employee number."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esignature": {
|
||||||
|
"actions": {
|
||||||
|
"distribute": "Distribute"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eula": {
|
"eula": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"accept": "Accept EULA"
|
"accept": "Accept EULA"
|
||||||
|
|||||||
@@ -1239,6 +1239,11 @@
|
|||||||
"unique_employee_number": ""
|
"unique_employee_number": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esignature": {
|
||||||
|
"actions": {
|
||||||
|
"distribute": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"eula": {
|
"eula": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"accept": "Accept EULA"
|
"accept": "Accept EULA"
|
||||||
|
|||||||
@@ -1239,6 +1239,11 @@
|
|||||||
"unique_employee_number": ""
|
"unique_employee_number": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esignature": {
|
||||||
|
"actions": {
|
||||||
|
"distribute": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"eula": {
|
"eula": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"accept": "Accept EULA"
|
"accept": "Accept EULA"
|
||||||
|
|||||||
72
documenso/docker-compose.yml
Normal file
72
documenso/docker-compose.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
name: documenso-production
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:?err}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:?err}
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
documenso:
|
||||||
|
image: documenso/documenso:latest
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- PORT=${PORT:-3000}
|
||||||
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
||||||
|
- NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY:?err}
|
||||||
|
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY:?err}
|
||||||
|
- NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID}
|
||||||
|
- NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET}
|
||||||
|
- NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:?err}
|
||||||
|
- NEXT_PRIVATE_INTERNAL_WEBAPP_URL=${NEXT_PRIVATE_INTERNAL_WEBAPP_URL:-http://localhost:$PORT}
|
||||||
|
- NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:?err}
|
||||||
|
- NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL:-${NEXT_PRIVATE_DATABASE_URL}}
|
||||||
|
- NEXT_PUBLIC_UPLOAD_TRANSPORT=${NEXT_PUBLIC_UPLOAD_TRANSPORT:-database}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_ENDPOINT=${NEXT_PRIVATE_UPLOAD_ENDPOINT}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=${NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_REGION=${NEXT_PRIVATE_UPLOAD_REGION}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_BUCKET=${NEXT_PRIVATE_UPLOAD_BUCKET}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=${NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=${NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY}
|
||||||
|
- NEXT_PRIVATE_SMTP_TRANSPORT=${NEXT_PRIVATE_SMTP_TRANSPORT:?err}
|
||||||
|
- NEXT_PRIVATE_SMTP_HOST=${NEXT_PRIVATE_SMTP_HOST}
|
||||||
|
- NEXT_PRIVATE_SMTP_PORT=${NEXT_PRIVATE_SMTP_PORT}
|
||||||
|
- NEXT_PRIVATE_SMTP_USERNAME=${NEXT_PRIVATE_SMTP_USERNAME}
|
||||||
|
- NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD}
|
||||||
|
- NEXT_PRIVATE_SMTP_APIKEY_USER=${NEXT_PRIVATE_SMTP_APIKEY_USER}
|
||||||
|
- NEXT_PRIVATE_SMTP_APIKEY=${NEXT_PRIVATE_SMTP_APIKEY}
|
||||||
|
- NEXT_PRIVATE_SMTP_SECURE=${NEXT_PRIVATE_SMTP_SECURE}
|
||||||
|
- NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=${NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS}
|
||||||
|
- NEXT_PRIVATE_SMTP_FROM_NAME=${NEXT_PRIVATE_SMTP_FROM_NAME:?err}
|
||||||
|
- NEXT_PRIVATE_SMTP_FROM_ADDRESS=${NEXT_PRIVATE_SMTP_FROM_ADDRESS:?err}
|
||||||
|
- NEXT_PRIVATE_SMTP_SERVICE=${NEXT_PRIVATE_SMTP_SERVICE}
|
||||||
|
- NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_API_KEY=${NEXT_PRIVATE_MAILCHANNELS_API_KEY}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=${NEXT_PRIVATE_MAILCHANNELS_ENDPOINT}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=${NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=${NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=${NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY}
|
||||||
|
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
|
||||||
|
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||||
|
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
|
||||||
|
- NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=${NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS}
|
||||||
|
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH:-/opt/documenso/cert.p12}
|
||||||
|
- NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE}
|
||||||
|
- NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=${NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS}
|
||||||
|
ports:
|
||||||
|
- ${PORT:-3000}:${PORT:-3000}
|
||||||
|
volumes:
|
||||||
|
- /opt/documenso/cert.p12:/opt/documenso/cert.p12:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
@@ -2156,10 +2156,12 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
columns:
|
columns:
|
||||||
|
- commission_rates
|
||||||
- created_at
|
- created_at
|
||||||
- employeeid
|
- employeeid
|
||||||
- id
|
- id
|
||||||
- labor_rates
|
- labor_rates
|
||||||
|
- payout_method
|
||||||
- percentage
|
- percentage
|
||||||
- teamid
|
- teamid
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -2167,10 +2169,12 @@
|
|||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
|
- commission_rates
|
||||||
- created_at
|
- created_at
|
||||||
- employeeid
|
- employeeid
|
||||||
- id
|
- id
|
||||||
- labor_rates
|
- labor_rates
|
||||||
|
- payout_method
|
||||||
- percentage
|
- percentage
|
||||||
- teamid
|
- teamid
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -2188,10 +2192,12 @@
|
|||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
|
- commission_rates
|
||||||
- created_at
|
- created_at
|
||||||
- employeeid
|
- employeeid
|
||||||
- id
|
- id
|
||||||
- labor_rates
|
- labor_rates
|
||||||
|
- payout_method
|
||||||
- percentage
|
- percentage
|
||||||
- teamid
|
- teamid
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -2560,6 +2566,101 @@
|
|||||||
_eq: X-Hasura-User-Id
|
_eq: X-Hasura-User-Id
|
||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
|
- table:
|
||||||
|
name: esignature_documents
|
||||||
|
schema: public
|
||||||
|
object_relationships:
|
||||||
|
- name: document
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: documentid
|
||||||
|
- name: job
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: jobid
|
||||||
|
insert_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
columns:
|
||||||
|
- completed
|
||||||
|
- documentid
|
||||||
|
- external_document_id
|
||||||
|
- jobid
|
||||||
|
- message
|
||||||
|
- opened
|
||||||
|
- recipients
|
||||||
|
- rejected
|
||||||
|
- status
|
||||||
|
- subject
|
||||||
|
- title
|
||||||
|
comment: ""
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- completed
|
||||||
|
- completed_at
|
||||||
|
- created_at
|
||||||
|
- documentid
|
||||||
|
- external_document_id
|
||||||
|
- id
|
||||||
|
- jobid
|
||||||
|
- message
|
||||||
|
- opened
|
||||||
|
- recipients
|
||||||
|
- rejected
|
||||||
|
- status
|
||||||
|
- subject
|
||||||
|
- title
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
comment: ""
|
||||||
|
update_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- completed
|
||||||
|
- completed_at
|
||||||
|
- created_at
|
||||||
|
- documentid
|
||||||
|
- external_document_id
|
||||||
|
- message
|
||||||
|
- opened
|
||||||
|
- recipients
|
||||||
|
- rejected
|
||||||
|
- status
|
||||||
|
- subject
|
||||||
|
- title
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
check: null
|
||||||
|
comment: ""
|
||||||
- table:
|
- table:
|
||||||
name: eula_acceptances
|
name: eula_acceptances
|
||||||
schema: public
|
schema: public
|
||||||
@@ -3458,6 +3559,13 @@
|
|||||||
table:
|
table:
|
||||||
name: email_audit_trail
|
name: email_audit_trail
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: esignature_documents
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
name: esignature_documents
|
||||||
|
schema: public
|
||||||
- name: exportlogs
|
- name: exportlogs
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -6506,6 +6614,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- task_name
|
- task_name
|
||||||
@@ -6531,6 +6640,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- task_name
|
- task_name
|
||||||
@@ -6565,6 +6675,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- task_name
|
- task_name
|
||||||
@@ -6748,6 +6859,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -6768,6 +6880,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -6798,6 +6911,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- updated_at
|
- updated_at
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."esignature_documents";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE "public"."esignature_documents" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "external_document_id" text NOT NULL, "jobid" uuid NOT NULL, "status" text NOT NULL, "recipients" jsonb[] NOT NULL, "title" text NOT NULL, "subject" text NOT NULL, "message" text NOT NULL, "viewed" boolean NOT NULL DEFAULT false, "completed" boolean NOT NULL DEFAULT false, "documentid" uuid, "rejected" boolean NOT NULL DEFAULT false, "opened" boolean NOT NULL DEFAULT false, PRIMARY KEY ("id") , FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE restrict ON DELETE restrict);COMMENT ON TABLE "public"."esignature_documents" IS E'Tracking the lifecycle of esignature documents. ';
|
||||||
|
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
_new record;
|
||||||
|
BEGIN
|
||||||
|
_new := NEW;
|
||||||
|
_new."updated_at" = NOW();
|
||||||
|
RETURN _new;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
CREATE TRIGGER "set_public_esignature_documents_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."esignature_documents"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
|
||||||
|
COMMENT ON TRIGGER "set_public_esignature_documents_updated_at" ON "public"."esignature_documents"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."esignature_documents" drop constraint "esignature_documents_documentid_fkey";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
alter table "public"."esignature_documents"
|
||||||
|
add constraint "esignature_documents_documentid_fkey"
|
||||||
|
foreign key ("documentid")
|
||||||
|
references "public"."documents"
|
||||||
|
("id") on update restrict on delete restrict;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "public"."esignature_documents" ALTER COLUMN "recipients" TYPE ARRAY;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "public"."esignature_documents" ALTER COLUMN "recipients" TYPE json[];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."esignature_documents" add column "completed_at" timestamptz
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."esignature_documents" add column "completed_at" timestamptz
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
comment on column "public"."esignature_documents"."viewed" is E'Tracking the lifecycle of esignature documents. ';
|
||||||
|
alter table "public"."esignature_documents" alter column "viewed" set default false;
|
||||||
|
alter table "public"."esignature_documents" alter column "viewed" drop not null;
|
||||||
|
alter table "public"."esignature_documents" add column "viewed" bool;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."esignature_documents" drop column "viewed" cascade;
|
||||||
@@ -10,7 +10,7 @@ const documenso = new Documenso({
|
|||||||
});
|
});
|
||||||
const JSR_SERVER = "https://reports.test.imex.online";
|
const JSR_SERVER = "https://reports.test.imex.online";
|
||||||
const jsreport = require("@jsreport/nodejs-client");
|
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) {
|
async function distributeDocument(req, res) {
|
||||||
@@ -22,8 +22,12 @@ async function distributeDocument(req, res) {
|
|||||||
documentId,
|
documentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
const auditEntry = await client.request(DISTRIBUTE_ESIGNATURE_DOCUMENT, {
|
||||||
obj: {
|
external_document_id: documentId.toString(),
|
||||||
|
esig_update: {
|
||||||
|
status: "SENT"
|
||||||
|
},
|
||||||
|
audit: {
|
||||||
jobid: req.body.jobid,
|
jobid: req.body.jobid,
|
||||||
bodyshopid: req.body.bodyshopid,
|
bodyshopid: req.body.bodyshopid,
|
||||||
operation: `Esignature document with title ${distributeResult.title} (ID: ${documentId}) distributed to recipients.`,
|
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,
|
message: error.message, stack: error.stack,
|
||||||
body: req.body
|
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) {
|
async function deleteDocument(req, res) {
|
||||||
try {
|
try {
|
||||||
|
//TODO: Add in logic to check if doc exists, is deletable etc.
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
const { documentId } = req.body;
|
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({
|
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 });
|
res.json({ success: true, deleteResult });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting document:", error?.data);
|
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) {
|
async function newEsignDocument(req, res) {
|
||||||
try {
|
try {
|
||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
@@ -68,21 +103,18 @@ async function newEsignDocument(req, res) {
|
|||||||
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req })
|
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req })
|
||||||
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
|
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
|
||||||
|
|
||||||
|
|
||||||
//Get the Job data.
|
//Get the Job data.
|
||||||
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid: req.body.jobid });
|
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({
|
const createDocumentResponse = await documenso.documents.create({
|
||||||
payload: {
|
payload: {
|
||||||
title: esigData?.title || `Esign request from ${bodyshop.shopname}`,
|
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.
|
externalId: `${req.body.jobid}|${req.user?.email}`, //Have to pass the uploaded by later on. Limited to 255 chars.
|
||||||
recipients: [
|
recipients,
|
||||||
{
|
|
||||||
email: "allan@imexsystems.ca",//jobData.ownr_ea,
|
|
||||||
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
|
|
||||||
role: "SIGNER",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
meta: {
|
meta: {
|
||||||
timezone: bodyshop.timezone,
|
timezone: bodyshop.timezone,
|
||||||
dateFormat: "MM/dd/yyyy hh:mm a",
|
dateFormat: "MM/dd/yyyy hh:mm a",
|
||||||
@@ -99,7 +131,6 @@ async function newEsignDocument(req, res) {
|
|||||||
documentId: createDocumentResponse.id,
|
documentId: createDocumentResponse.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (esigData?.fields && esigData.fields.length > 0) {
|
if (esigData?.fields && esigData.fields.length > 0) {
|
||||||
try {
|
try {
|
||||||
await documenso.envelopes.fields.createMany({
|
await documenso.envelopes.fields.createMany({
|
||||||
@@ -117,16 +148,23 @@ async function newEsignDocument(req, res) {
|
|||||||
|
|
||||||
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
|
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
|
||||||
|
|
||||||
//add to job audit trail.
|
const auditEntry = await client.request(INSERT_ESIGNATURE_DOCUMENT, {
|
||||||
|
audit: {
|
||||||
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
|
||||||
obj: {
|
|
||||||
jobid: req.body.jobid,
|
jobid: req.body.jobid,
|
||||||
bodyshopid: bodyshop.id,
|
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}`,
|
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,
|
useremail: req.user?.email,
|
||||||
type: 'esig-create'
|
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 = {
|
module.exports = {
|
||||||
newEsignDocument,
|
newEsignDocument,
|
||||||
distributeDocument,
|
distributeDocument,
|
||||||
deleteDocument
|
deleteDocument,
|
||||||
|
viewDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
const { Documenso } = require("@documenso/sdk-typescript");
|
const { Documenso } = require("@documenso/sdk-typescript");
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const logger = require("../utils/logger");
|
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 { uploadFileBuffer } = require("../media/imgproxy-media");
|
||||||
|
const { log } = require("node-persist");
|
||||||
|
|
||||||
const client = require('../graphql-client/graphql-client').client;
|
const client = require('../graphql-client/graphql-client').client;
|
||||||
const documenso = new Documenso({
|
const documenso = new Documenso({
|
||||||
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
|
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
|
||||||
@@ -30,38 +30,56 @@ async function esignWebhook(req, res) {
|
|||||||
body: message
|
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) {
|
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:
|
case webhookTypeEnums.DOCUMENT_CREATED:
|
||||||
//This is largely a throwaway event we know it was 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
|
// Here you can add any additional processing you want to do when a document is created
|
||||||
break;
|
break;
|
||||||
case webhookTypeEnums.DOCUMENT_COMPLETED:
|
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);
|
await handleDocumentCompleted(message.payload);
|
||||||
// Here you can add any additional processing you want to do when a document is completed
|
// Here you can add any additional processing you want to do when a document is completed
|
||||||
break;
|
break;
|
||||||
case webhookTypeEnums.DOCUMENT_SIGNED:
|
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
|
// 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;
|
break;
|
||||||
default:
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
logger.log(`esig-webhook-processed`, "INFO", "redis", "api", { event: message.event, documentId: message.payload?.payload?.id, jobid: message.payload?.payload?.externalId?.split("|")[0] || null });
|
||||||
// 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)
|
res.sendStatus(200)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,25 +87,14 @@ async function esignWebhook(req, res) {
|
|||||||
message: error.message, stack: error.stack,
|
message: error.message, stack: error.stack,
|
||||||
body: req.body
|
body: req.body
|
||||||
});
|
});
|
||||||
// const downloadsDir = path.join(__dirname, '../downloads');
|
res.status(500).json({ message: "Error processing webhook event.", error: error.message });
|
||||||
// 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) {
|
async function handleDocumentCompleted(payload = sampleComplete) {
|
||||||
|
|
||||||
|
|
||||||
//Check if the bodyshop is on image proxy or not
|
|
||||||
try {
|
try {
|
||||||
//Split the external id to get the uploaded user.
|
//Split the external id to get the uploaded user.
|
||||||
const [jobid, uploaded_by] = payload.externalId.split("|");
|
const [jobid, uploaded_by] = payload.externalId.split("|");
|
||||||
|
|
||||||
if (!jobid || !uploaded_by) {
|
if (!jobid || !uploaded_by) {
|
||||||
throw new Error(`Invalid externalId format. Expected "jobid|uploaded_by", got "${payload.externalId}"`);
|
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`;
|
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) {
|
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
|
||||||
//LMS not yet implemented.
|
//TODO:LMS not yet implemented.
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//S3 Upload
|
//S3 Upload
|
||||||
@@ -125,8 +132,15 @@ async function handleDocumentCompleted(payload = sampleComplete) {
|
|||||||
s3Key: key,
|
s3Key: key,
|
||||||
bucket: uploadResult.bucket
|
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,
|
jobid: jobs_by_pk.id,
|
||||||
bodyshopid: jobs_by_pk.bodyshop.id,
|
bodyshopid: jobs_by_pk.bodyshop.id,
|
||||||
operation: `Esignature document with title ${payload.title} (ID: ${payload.documentMeta.id}) has been completed.`,
|
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'
|
type: 'esig-complete'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
//insert the document record with the s3 key and bucket info.
|
//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: {
|
docInput: {
|
||||||
jobid: jobs_by_pk.id,
|
jobid: jobs_by_pk.id,
|
||||||
uploaded_by: uploaded_by,
|
uploaded_by: uploaded_by,
|
||||||
|
|||||||
@@ -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!) {
|
exports.INSERT_ESIG_AUDIT_TRAIL = `mutation INSERT_ESIG_AUDIT_TRAIL($obj: audit_trail_insert_input!) {
|
||||||
insert_audit_trail_one(object: $obj) {
|
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) {
|
insert_documents_one(object: $docInput) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const router = express.Router();
|
|||||||
|
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
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");
|
const { esignWebhook } = require("../esign/webhook");
|
||||||
|
|
||||||
//router.use(validateFirebaseIdTokenMiddleware);
|
//router.use(validateFirebaseIdTokenMiddleware);
|
||||||
@@ -11,6 +11,7 @@ const { esignWebhook } = require("../esign/webhook");
|
|||||||
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
|
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
|
||||||
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
|
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
|
||||||
router.post("/delete", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, deleteDocument);
|
router.post("/delete", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, deleteDocument);
|
||||||
|
router.post("/view", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, viewDocument);
|
||||||
router.post("/webhook", esignWebhook);
|
router.post("/webhook", esignWebhook);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user