Compare commits

..

80 Commits

Author SHA1 Message Date
Dave Richer
045f36e294 Merged in release/2026-06-05 (pull request #3288)
IO-3722 Remove delivery date for bypass vehicles.
2026-05-28 19:23:09 +00:00
Dave Richer
c7c6dfcd7d Merged in feature/IO-3722-disable-contact-fortellis (pull request #3287)
IO-3722 Remove delivery date for bypass vehicles.
2026-05-28 19:22:53 +00:00
Patrick Fic
c1c0b35c8f IO-3722 Remove delivery date for bypass vehicles. 2026-05-28 11:32:23 -07:00
Dave Richer
c024fdd57b Merged in release/2026-06-05 (pull request #3285)
Release/2026 06 05
2026-05-28 16:56:04 +00:00
Dave Richer
a4ccacf83a Merged in feature/IO-3722-disable-contact-fortellis (pull request #3284)
IO-3722 Remove customer lookup by Vehicle Owner.
2026-05-28 16:55:39 +00:00
Patrick Fic
aa3b303fe9 IO-3722 Remove customer lookup by Vehicle Owner. 2026-05-28 09:53:40 -07:00
Patrick Fic
fdaf50d778 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3282)
Feature/IO-3722 disable contact fortellis
2026-05-27 21:48:17 +00:00
Patrick Fic
468ed23f73 IO-3722 Fix undefined customer ref. 2026-05-27 14:18:31 -07:00
Patrick Fic
322ebd3bc7 Resolve inversed if statement. 2026-05-27 12:46:09 -07:00
Patrick Fic
b887cfed01 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3278)
IO-3722 Add additional await.
2026-05-27 19:41:41 +00:00
Patrick Fic
0f800c5a4c IO-3722 Add additional await. 2026-05-27 12:40:41 -07:00
Dave Richer
6cce92b0fd Merged in release/2026-06-05 (pull request #3276)
IO-3722 Disable contact API calls for Fortellis.
2026-05-27 18:29:33 +00:00
Dave Richer
60ab04cb38 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3275)
IO-3722 Disable contact API calls for Fortellis.
2026-05-27 18:29:14 +00:00
Patrick FIc
345a470731 IO-3722 Disable contact API calls for Fortellis. 2026-05-27 10:31:33 -07:00
Dave Richer
0025e113c6 Merged in release/2026-06-05 (pull request #3273)
feature/IO-3541-Parts-Dispatch-Return-Data - Add Refetech queries to keep data in sync
2026-05-26 16:25:53 +00:00
Dave Richer
dc435b2bb0 Merged in feature/IO-3541-Parts-Dispatch-Return-Data (pull request #3272)
feature/IO-3541-Parts-Dispatch-Return-Data - Add Refetech queries to keep data in sync
2026-05-26 16:25:23 +00:00
Dave
fd72d244e7 feature/IO-3541-Parts-Dispatch-Return-Data - Add Refetech queries to keep data in sync 2026-05-26 12:24:56 -04:00
Dave Richer
87bb472271 Merged in release/2026-06-05 (pull request #3271)
feature/IO-2960-Employee-Email-Info - Fix
2026-05-26 16:18:15 +00:00
Dave Richer
825959880e Merged in feature/IO-2960-Employee-Email-Info (pull request #3270)
feature/IO-2960-Employee-Email-Info - Fix
2026-05-26 16:17:55 +00:00
Dave
c40fea0ec9 feature/IO-2960-Employee-Email-Info - Fix 2026-05-26 12:17:25 -04:00
Dave Richer
ebdf427b58 Merged in release/2026-06-05 (pull request #3269)
feature/IO-3567-New-Job-Line-Tab - Fix
2026-05-26 16:06:29 +00:00
Dave Richer
b3fdd68276 Merged in feature/IO-3567-New-Job-Line-Tab (pull request #3268)
feature/IO-3567-New-Job-Line-Tab - Fix
2026-05-26 16:06:02 +00:00
Dave
30e5027c8c feature/IO-3567-New-Job-Line-Tab - Fix 2026-05-26 12:05:31 -04:00
Dave Richer
3e63c58b9b Merged in release/2026-06-05 (pull request #3267)
release/2026-06-05 - Esignture Banner
2026-05-26 15:49:49 +00:00
Dave
938cef1f6b release/2026-06-05 - Esignture Banner 2026-05-26 11:49:08 -04:00
Dave Richer
7e2df3e341 Merged in release/2026-06-05 (pull request #3266)
release/2026-06-05 - Fix Documenso
2026-05-25 20:43:05 +00:00
Dave
45d095a7a3 release/2026-06-05 - Fix Documenso 2026-05-25 16:42:23 -04:00
Dave Richer
709b6ef1d6 Merged in release/2026-06-05 (pull request #3265)
release/2026-06-05 - Package updates, Esig Fixes, Ant tweak for upgrade
2026-05-25 19:52:15 +00:00
Dave
4e98df6694 release/2026-06-05 - Package updates, Esig Fixes, Ant tweak for upgrade 2026-05-25 15:51:37 -04:00
Dave Richer
b920bb4437 Merged in release/2026-06-05 (pull request #3264)
Release/2026 06 05
2026-05-25 19:06:02 +00:00
Dave Richer
e36a110e81 Merged in feature/IO-3713-Esign-Modal-UI (pull request #3263)
Feature/IO-3713 Esign Modal UI
2026-05-25 19:05:41 +00:00
Dave
719d1b6479 Merge remote-tracking branch 'origin/release/2026-06-05' into feature/IO-3713-Esign-Modal-UI 2026-05-25 15:04:55 -04:00
Dave
29ded5efbf feature/IO-3713-Esign-Modal-UI - Fix 2026-05-25 15:01:46 -04:00
Dave
551e0f0592 feature/IO-3713-Esign-Modal-UI - Fix 2026-05-25 15:00:05 -04:00
Dave Richer
4d299bb226 Merged in release/2026-06-05 (pull request #3262)
feature/IO-3701-Harness-Replacement - Implement
2026-05-25 15:44:29 +00:00
Dave Richer
ae9b68a0bc Merged in feature/IO-3701-Harness-Replacement (pull request #3261)
feature/IO-3701-Harness-Replacement - Implement
2026-05-25 15:44:04 +00:00
Dave Richer
99b65e8186 Merged in release/2026-06-05 (pull request #3260)
feature/IO-3714-Esignature-Lock - Add Lock to Esignatures
2026-05-25 15:27:36 +00:00
Dave Richer
426283ffee Merged in release/2026-05-22 (pull request #3256)
IO-3710 Visual Board Vehicle Color
2026-05-20 23:57:48 +00:00
Dave Richer
4fc86ccaa3 Merged in release/2026-05-22 (pull request #3254)
release/2026-05-22 - Remove uncessary require
2026-05-20 20:12:07 +00:00
Dave Richer
a67946c5a3 Merged in release/2026-05-22 (pull request #3253)
IO-3712 Disable analytics in client side.
2026-05-20 18:11:11 +00:00
Dave Richer
e43923b7a0 Merged in release/2026-05-22 (pull request #3251)
Release/2026 05 22
2026-05-20 16:54:53 +00:00
Dave Richer
e9ef429729 Merged in release/2026-05-22 (pull request #3247)
Release/2026 05 22
2026-05-14 17:56:22 +00:00
Dave Richer
db01ad9155 Merged in release/2026-05-22 (pull request #3245)
Release/2026 05 22
2026-05-13 16:10:49 +00:00
Allan Carr
8bf7fbd1f1 Merged in release/2026-05-22 (pull request #3241)
IO-3691 Job Totals Issues
2026-05-12 15:42:36 +00:00
Dave Richer
c37037ef21 Merged in release/2026-05-22 (pull request #3238)
hotfix/2020hotfix/2026-05-11 - Fix so polling throws error on missing env var (logs error), and does not start poll, vs starting polling and logging error every 10 seconds
2026-05-11 21:26:40 +00:00
Dave Richer
6050aebcd5 Merged in release/2026-05-08 (pull request #3235)
IO-3689 Customer List Restriction
2026-05-08 18:45:38 +00:00
Dave Richer
77d0f5ab38 Merged in release/2026-05-08 (pull request #3233)
feature/IO-3688-Searchable-Referral-Source - Implement (convert button)
2026-05-08 15:43:23 +00:00
Dave Richer
a0692f8c69 Merged in release/2026-05-08 (pull request #3231)
feature/IO-3688-Searchable-Referral-Source - Implement
2026-05-08 14:42:36 +00:00
Dave Richer
4f76aeb06f Merged in release/2026-05-08 (pull request #3229)
feature/IO-3679-Tech-Console-Null-Error - fix
2026-05-07 14:43:08 +00:00
Dave Richer
302a42089f Merged in release/2026-05-08 (pull request #3227)
IO-3686 River city enhancements for AR customers and Contact Code
2026-05-07 14:05:14 +00:00
Dave Richer
906265c4b2 Merged in release/2026-05-08 (pull request #3226)
feature/IO-3687-Grey-Scale-Invisible-text - implement
2026-05-06 20:46:31 +00:00
Dave Richer
388b042037 Merged in release/2026-05-08 (pull request #3223)
Release/2026 05 08
2026-05-06 15:48:38 +00:00
Dave Richer
73eb76a230 Merged in release/2026-05-08 (pull request #3220)
feature/IO-3672-Reynolds-Adjustments-V3 - Expand Export logs for Reynolds
2026-05-05 17:31:56 +00:00
Dave Richer
d5e9b79f75 Merged in release/2026-05-08 (pull request #3218)
feature/IO-3672-Reynolds-Adjustments-V3 - Hide DMS Posting sheet report in reynolds mode.
2026-05-04 21:09:10 +00:00
Dave Richer
56d0c009e2 Merged in release/2026-05-08 (pull request #3216)
feature/IO-3672-Reynolds-Adjustments-V3 - Make sure there is never a scenario where a ROGOG does not have a ROLABOR
2026-05-04 20:33:08 +00:00
Dave Richer
79030f6b36 Merged in release/2026-05-08 (pull request #3214)
feature/IO-3674-Fix-Save-And-New - Fix Save and New so state gets reset on form when starting from a new employee
2026-05-04 20:13:21 +00:00
Dave Richer
5e78cdd8ae Merged in release/2026-05-08 (pull request #3210)
Release/2026 05 08
2026-04-29 16:46:47 +00:00
Dave Richer
8f4ac866f1 Merged in release/2026-05-08 (pull request #3205)
hotfix/2026-04-28 - Add Label, fix exported
2026-04-28 17:52:54 +00:00
Dave Richer
9ad2a53bec Merged in release/2026-05-08 (pull request #3202)
hotfix/2026-04-21 - fix Parts order comments
2026-04-22 16:44:10 +00:00
Dave Richer
6590f8961b Merged in release/2026-05-08 (pull request #3200)
feature/IO-3647-Reynolds-Integration-Phase-2-Optional - Add option to make 'Enhanced Early ROS' optional
2026-04-21 14:52:31 +00:00
Dave Richer
7df71b8f44 Merged in release/2026-05-08 (pull request #3198)
hotfix/2026-04-21 - Fix save dirty state on employees causing prompt, add 'save and new'
2026-04-21 14:29:55 +00:00
Dave Richer
4776b03a21 Merged in release/2026-05-08 (pull request #3194)
hotfix/2026-04-20 - Remove item from Cost centers
2026-04-20 15:41:47 +00:00
Dave Richer
20943f74e9 Merged in release/2026-04-17 (pull request #3190)
feature/IO-3647-Reynolds-Integration-Phase-2 - Enhance early RO with meaningful amounts.
2026-04-13 14:41:37 +00:00
Dave Richer
4af312854e Merged in release/2026-04-17 (pull request #3187)
hotfix/2026-04-10 - Fix Location Identifier in chatter-api
2026-04-10 15:41:08 +00:00
Dave Richer
ff084f6fb8 Merged in release/2026-04-17 (pull request #3185)
feature/IO-3638-Reynolds-OpenSearch - Add Search on DMS id in Reynolds shops
2026-04-09 15:16:48 +00:00
Dave Richer
5c9e4517a6 Merged in release/2026-04-17 (pull request #3183)
Release/2026 04 17
2026-04-08 18:03:10 +00:00
Dave Richer
190217ffce Merged in release/2026-04-17 (pull request #3181)
Release/2026 04 17
2026-04-03 01:56:19 +00:00
Dave Richer
28dc1d4533 Merged in release/2026-04-03 (pull request #3178)
Merged in feature/IO-3637-DMS-ID-Production-Board-Column (pull request #3175)
2026-04-03 01:37:45 +00:00
Allan Carr
a97e03e0b1 Merged in feature/IO-3637-DMS-ID-Production-Board-Column (pull request #3176)
Feature/IO-3637 DMS ID Production Board Column
2026-04-02 23:07:28 +00:00
Dave Richer
e30353cab6 Merged in release/2026-04-03 (pull request #3171)
Release/2026 04 03
2026-03-31 20:19:00 +00:00
Dave Richer
c9b9f67170 Merged in release/2026-04-03 (pull request #3168)
Release/2026 04 03
2026-03-30 19:08:50 +00:00
Dave Richer
4a47f543b2 Merged in release/2026-04-03 (pull request #3164)
IO-3629 PostBatchWip Rtn != 0 error
2026-03-30 15:07:33 +00:00
Dave Richer
3b60aa89f1 Merged in release/2026-04-03 (pull request #3160)
Release/2026 04 03
2026-03-27 18:48:51 +00:00
Dave Richer
20d2572087 Merged in release/2026-04-03 (pull request #3157)
Release/2026 04 03
2026-03-25 22:36:49 +00:00
Dave Richer
ac4c09af60 Merged in release/2026-04-03 (pull request #3154)
Release/2026 04 03
2026-03-24 17:50:29 +00:00
Dave Richer
6a60af9dfe Merged in release/2026-04-03 (pull request #3150)
Release/2026 04 03
2026-03-23 17:05:19 +00:00
Dave Richer
dfb6f02864 Merged in release/2026-04-03 (pull request #3148)
Fix RR
2026-03-20 18:56:28 +00:00
Dave Richer
48bb494e0f Merged in release/2026-04-03 (pull request #3146)
IO-3515 Add shopname to bill ai feedback.
2026-03-20 18:16:04 +00:00
Dave Richer
9b74cba56b Merged in release/2026-04-03 (pull request #3144)
Release/2026 04 03
2026-03-19 22:44:56 +00:00
Dave Richer
6fc8124268 Merged in release/2026-04-03 (pull request #3141)
Release/2026 04 03
2026-03-19 18:47:24 +00:00
26 changed files with 2964 additions and 3675 deletions

1
.gitignore vendored
View File

@@ -157,3 +157,4 @@ docker_data
.terraform
terraform.tfvars
terraform.exe

2891
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,61 +8,61 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@amplitude/analytics-browser": "^2.38.0",
"@amplitude/analytics-browser": "^2.42.4",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.6",
"@apollo/client": "^4.2.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@documenso/embed-react": "^0.5.1",
"@documenso/embed-react": "^0.6.1",
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.1.0",
"@firebase/analytics": "^0.10.21",
"@firebase/app": "^0.14.10",
"@firebase/auth": "^1.12.2",
"@firebase/firestore": "^4.13.0",
"@firebase/messaging": "^0.12.25",
"@fingerprintjs/fingerprintjs": "^5.2.0",
"@firebase/analytics": "^0.10.22",
"@firebase/app": "^0.14.12",
"@firebase/auth": "^1.13.1",
"@firebase/firestore": "^4.14.1",
"@firebase/messaging": "^0.12.26",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.3.5",
"@sentry/react": "^10.47.0",
"@reduxjs/toolkit": "^2.12.0",
"@sentry/cli": "^3.4.3",
"@sentry/react": "^10.53.1",
"@sentry/vite-plugin": "^4.9.1",
"@tanem/react-nprogress": "^5.0.63",
"antd": "^6.3.5",
"antd": "^6.4.3",
"apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1",
"axios": "^1.14.0",
"axios": "^1.16.1",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.20",
"dayjs-business-days2": "^1.3.3",
"dinero.js": "^1.9.1",
"dotenv": "^17.3.1",
"dotenv": "^17.4.2",
"env-cmd": "^11.0.0",
"exifr": "^7.1.3",
"graphql": "^16.13.2",
"graphql": "^16.14.0",
"graphql-ws": "^6.0.8",
"i18next": "^25.10.10",
"i18next-browser-languagedetector": "^8.2.1",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.41",
"libphonenumber-js": "^1.13.3",
"lightningcss": "^1.32.0",
"logrocket": "^12.1.0",
"logrocket": "^12.1.1",
"markerjs2": "^2.32.7",
"memoize-one": "^6.0.0",
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.71",
"posthog-js": "^1.364.4",
"posthog-js": "^1.376.0",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
"react": "^19.2.4",
"react": "^19.2.6",
"react-big-calendar": "^1.19.4",
"react-color": "^2.19.3",
"react-cookie": "^8.1.0",
"react-dom": "^19.2.4",
"react-cookie": "^8.1.2",
"react-dom": "^19.2.6",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "^2.2.3",
"react-i18next": "^16.6.6",
@@ -72,22 +72,22 @@
"react-number-format": "^5.4.5",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.62",
"react-redux": "^9.2.0",
"react-redux": "^9.3.0",
"react-resizable": "^3.1.3",
"react-router-dom": "^7.13.2",
"react-router-dom": "^7.15.1",
"react-sticky": "^6.0.3",
"react-virtuoso": "^4.18.3",
"react-virtuoso": "^4.18.7",
"recharts": "^3.8.1",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.4.2",
"redux-saga": "^1.5.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"reselect": "^5.2.0",
"rxjs": "^7.8.2",
"sass": "^1.98.0",
"sass": "^1.100.0",
"socket.io-client": "^4.8.3",
"styled-components": "^6.3.12",
"styled-components": "^6.4.2",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^5.2.0"
},
@@ -137,14 +137,14 @@
"@rollup/rollup-linux-x64-gnu": "4.6.1"
},
"devDependencies": {
"@ant-design/icons": "^6.1.1",
"@ant-design/icons": "^6.2.3",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.28.5",
"@dotenvx/dotenvx": "^1.59.1",
"@babel/preset-react": "^7.29.7",
"@dotenvx/dotenvx": "^1.68.1",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.39.2",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.60.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -156,21 +156,21 @@
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"globals": "^17.4.0",
"globals": "^17.6.0",
"jsdom": "^28.1.0",
"memfs": "^4.57.1",
"memfs": "^4.57.2",
"os-browserify": "^0.3.0",
"playwright": "^1.58.2",
"playwright": "^1.60.0",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^7.3.1",
"vite-plugin-babel": "^1.6.0",
"vite-plugin-babel": "^1.7.3",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.26.0",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-node-polyfills": "^0.28.0",
"vite-plugin-pwa": "^1.3.0",
"vite-plugin-style-import": "^2.0.0",
"vitest": "^4.1.2",
"workbox-window": "^7.4.0"
"vitest": "^4.1.7",
"workbox-window": "^7.4.1"
}
}

View File

@@ -512,7 +512,52 @@
.esignature-embed {
width: 100%;
height: 100%;
border-width: 0;
}
display: block;
width: 100%;
height: 100%;
border-width: 0;
}
.esignature-modal {
.ant-modal {
top: 16px;
max-width: calc(100vw - 32px);
padding-bottom: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
max-height: calc(100vh - 32px);
}
.ant-modal-body {
flex: 1 1 auto;
min-height: 0;
}
}
.esignature-modal-frame {
width: 100%;
height: calc(100vh - 150px);
min-height: 320px;
overflow: hidden;
}
@media (max-width: 768px), (max-height: 560px) {
.esignature-modal {
.ant-modal {
top: 8px;
max-width: calc(100vw - 16px);
}
.ant-modal-content {
max-height: calc(100vh - 16px);
}
}
.esignature-modal-frame {
height: calc(100vh - 132px);
min-height: 0;
}
}

View File

@@ -37,6 +37,7 @@ const defaultTheme = (isDarkMode) => ({
isDarkMode
),
colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
colorShadow: isDarkMode ? "rgba(0, 0, 0, 0.7)" : "#000000",
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
}
});

View File

@@ -36,6 +36,7 @@ export function BillEnterModalLinesComponent({
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const firstFieldRefs = useRef({});
const lineDescriptionRefs = useRef({});
const CONTROL_HEIGHT = 32;
@@ -94,6 +95,23 @@ export function BillEnterModalLinesComponent({
});
};
const focusLineDescription = (index) => {
const lineDescription = lineDescriptionRefs.current[index];
if (typeof lineDescription?.focus === "function") {
lineDescription.focus({ preventScroll: true });
return;
}
lineDescription?.resizableTextArea?.textArea?.focus?.({ preventScroll: true });
};
const focusJobLineSelect = (index) => {
window.setTimeout(() => {
firstFieldRefs.current[index]?.focus?.({ preventScroll: true });
}, 0);
};
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
const autofillActualCost = (index) => {
if (bodyshop.accountingconfig?.disableBillCostCalculation) return;
@@ -195,6 +213,12 @@ export function BillEnterModalLinesComponent({
minHeight: `${CONTROL_HEIGHT}px`
}}
allowRemoved={form.getFieldValue("is_credit_memo") || false}
onInputKeyDown={(event) => {
if (event.key !== "Tab" || event.shiftKey || event.defaultPrevented) return;
event.preventDefault();
focusLineDescription(index);
}}
onSelect={(value, opt) => {
// IMPORTANT:
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
@@ -221,6 +245,7 @@ export function BillEnterModalLinesComponent({
};
})
});
focusJobLineSelect(index);
}}
/>
)
@@ -236,7 +261,16 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.line_desc"),
rules: [{ required: true }]
}),
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
formInput: (record, index) => (
<Input.TextArea
ref={(el) => {
lineDescriptionRefs.current[index] = el;
}}
disabled={disabled}
autoSize
tabIndex={0}
/>
)
},
{

View File

@@ -26,6 +26,7 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
const { open, context } = esignatureModal;
const { token, envelopeId, documentId, jobid } = context;
const [distributing, setDistributing] = useState(false);
const hasToken = Boolean(token);
if (!hasDocumensoApiKey(bodyshop)) {
return null;
@@ -39,6 +40,10 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
rome: t("jobs.labels.esignature_rome")
})}
onOk={async () => {
if (!hasToken) {
return;
}
try {
setDistributing(true);
await axios.post("/esign/distribute", {
@@ -58,6 +63,11 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
setDistributing(false);
}}
onCancel={async () => {
if (!hasToken) {
toggleModalVisible();
return;
}
try {
await axios.post("/esign/delete", {
documentId,
@@ -73,13 +83,15 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
});
}
}}
okButtonProps={{ loading: distributing }}
okButtonProps={{ loading: distributing, style: hasToken ? undefined : { display: "none" } }}
okText={t("esignature.actions.distribute")}
destroyOnHidden
width={"80%"}
width="calc(100vw - 32px)"
wrapClassName="esignature-modal"
styles={{ body: { overflow: "hidden", padding: 0 } }}
>
<div style={{ height: "80vh", width: "100%" }}>
{token ? (
<div className="esignature-modal-frame">
{hasToken ? (
<EmbedUpdateDocumentV1
presignToken={token}
host="https://sign.imex.online"

View File

@@ -63,7 +63,9 @@ export function JobLineDispatchButton({
}
}
//joblineids: selectedLines.map((l) => l.id),
}
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
awaitRefetchQueries: true
});
if (result.errors) {
console.log("🚀 ~ handleConvert ~ result.errors:", result.errors);

View File

@@ -1,5 +1,6 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
import { Card, Col, Input, Row, Space, Typography, Tooltip } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import { Alert, Button, Card, Col, Input, Row, Space, Typography, Tooltip } from "antd";
import _ from "lodash";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -17,6 +18,7 @@ import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-spee
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { hasDocumensoApiKey } from "../../utils/esignature.js";
import useLocalStorage from "../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
@@ -28,6 +30,10 @@ const mapDispatchToProps = () => ({});
export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technician }) {
const [search, setSearch] = useState("");
const [esignatureBannerDismissed, setEsignatureBannerDismissed] = useLocalStorage(
"print_center_esignature_banner_dismissed",
false
);
const { id: jobId, job } = printCenterModal.context;
const tempList = TemplateList("job", {});
const { t } = useTranslation();
@@ -42,6 +48,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
const dmsMode = getDmsMode(bodyshop, "off");
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
const showEsignatureBanner = !esignatureEnabled && !esignatureBannerDismissed;
const Templates = !hasDMSKey
? Object.keys(tempList)
@@ -92,6 +99,23 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
return (
<div>
{showEsignatureBanner && (
<Alert
action={
<Button
aria-label={t("general.actions.close")}
icon={<CloseOutlined />}
onClick={() => setEsignatureBannerDismissed(true)}
size="small"
type="text"
/>
}
banner
title={t("printcenter.banners.esignature_promo")}
type="info"
className="print-center-esignature-banner"
/>
)}
<Row gutter={[16, 16]}>
<Col lg={8} md={12} sm={24}>
<PrintCenterSpeedPrint jobId={jobId} />

View File

@@ -5,3 +5,7 @@
padding: 0;
}
}
.print-center-esignature-banner {
margin-bottom: 16px;
}

View File

@@ -12,11 +12,11 @@ import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
CHECK_EMPLOYEE_EMAIL,
CHECK_EMPLOYEE_NUMBER,
DELETE_VACATION,
INSERT_EMPLOYEES,
QUERY_EMPLOYEE_BY_ID,
QUERY_USERS_BY_EMAIL,
UPDATE_EMPLOYEE
} from "../../graphql/employees.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -174,9 +174,10 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
const handleFinish = async (values) => {
const submitAction = saveAndResetSubmitAction();
const userEmail = typeof values.user_email === "string" ? values.user_email.trim() : values.user_email;
const normalizedValues = {
...values,
user_email: values.user_email === "" ? null : values.user_email
user_email: userEmail === "" ? null : userEmail
};
if (search.employeeId && search.employeeId !== "new") {
@@ -491,18 +492,29 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
rules={[
({ getFieldValue }) => ({
async validator(rule, value) {
const user_email = getFieldValue("user_email");
const user_email = typeof value === "string" ? value.trim() : getFieldValue("user_email");
if (user_email && value) {
const response = await client.query({
query: QUERY_USERS_BY_EMAIL,
query: CHECK_EMPLOYEE_EMAIL,
variables: {
email: user_email
email: user_email,
shopId: bodyshop.id
}
});
if (response.data.users.length === 1) {
return Promise.resolve();
const matchingEmployees = response.data.employees_aggregate.nodes;
const currentEmployeeId = form.getFieldValue("id") ?? search.employeeId;
if (
response.data.employees_aggregate.aggregate.count === 0 ||
matchingEmployees.every((employee) => employee.id === currentEmployeeId)
) {
return Promise.resolve();
}
return Promise.reject(t("employees.validation.unique_user_email"));
}
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
} else {

View File

@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useEffect } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
CHECK_EMPLOYEE_EMAIL,
DELETE_VACATION,
INSERT_EMPLOYEES,
QUERY_EMPLOYEE_BY_ID,
@@ -16,6 +17,7 @@ const updateEmployeeMock = vi.fn();
const deleteVacationMock = vi.fn();
const useQueryMock = vi.fn();
const useMutationMock = vi.fn();
const apolloClientQueryMock = vi.fn();
const navigateMock = vi.fn();
const notification = {
error: vi.fn(),
@@ -87,6 +89,10 @@ vi.mock("react-i18next", () => ({
return "Employee number must be unique";
}
if (key === "employees.validation.unique_user_email") {
return "User email already assigned";
}
if (key === "bodyshop.validation.useremailmustexist") {
return "User email must exist";
}
@@ -203,18 +209,20 @@ describe("ShopEmployeesFormComponent", () => {
return [vi.fn()];
});
useApolloClient.mockReturnValue({
query: vi.fn().mockResolvedValue({
data: {
employees_aggregate: {
aggregate: {
count: 0
},
nodes: []
apolloClientQueryMock.mockResolvedValue({
data: {
employees_aggregate: {
aggregate: {
count: 0
},
users: []
}
})
nodes: []
},
users: []
}
});
useApolloClient.mockReturnValue({
query: apolloClientQueryMock
});
insertEmployeesMock.mockResolvedValue({
@@ -356,4 +364,59 @@ describe("ShopEmployeesFormComponent", () => {
title: "Saved"
});
});
it("blocks saving when the user email belongs to another employee in the shop", async () => {
apolloClientQueryMock.mockImplementation(({ query }) => {
if (query === CHECK_EMPLOYEE_EMAIL) {
return Promise.resolve({
data: {
users: [{ email: "jamie@example.com" }],
employees_aggregate: {
aggregate: {
count: 1
},
nodes: [{ id: "other-employee" }]
}
}
});
}
return Promise.resolve({
data: {
employees_aggregate: {
aggregate: {
count: 0
},
nodes: []
},
users: []
}
});
});
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
target: { value: "Jamie" }
});
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
target: { value: "Rivera" }
});
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
target: { value: "42" }
});
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
target: { value: "1234" }
});
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
target: { value: "2026-04-20" }
});
fireEvent.change(screen.getByRole("textbox", { name: "User Email" }), {
target: { value: "jamie@example.com" }
});
fireEvent.click(screen.getByRole("button", { name: "Save Employee" }));
expect(await screen.findByText("User email already assigned")).toBeInTheDocument();
expect(insertEmployeesMock).not.toHaveBeenCalled();
expect(notification.success).not.toHaveBeenCalled();
});
});

View File

@@ -157,36 +157,36 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Col>
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
ClosingPeriod.treatment === "on" && (
<Col xs={24} sm={12} xl={8}>
<Form.Item
key="ClosingPeriod"
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
</Col>
)}
<Col xs={24} sm={12} xl={8}>
<Form.Item
key="ClosingPeriod"
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
</Col>
)}
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
ADPPayroll.treatment === "on" && (
<Col xs={24} sm={12} xl={8}>
<Form.Item
key="companyCode"
name={["accountingconfig", "companyCode"]}
label={t("bodyshop.fields.companycode")}
>
<Input />
</Form.Item>
</Col>
)}
<Col xs={24} sm={12} xl={8}>
<Form.Item
key="companyCode"
name={["accountingconfig", "companyCode"]}
label={t("bodyshop.fields.companycode")}
>
<Input />
</Form.Item>
</Col>
)}
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
ADPPayroll.treatment === "on" && (
<Col xs={24} sm={12} xl={8}>
<Col xs={24} sm={12} xl={8}>
<Form.Item key="batchID" name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
<Input />
</Form.Item>
</Col>
)}
<Input />
</Form.Item>
</Col>
)}
{HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && (
<>
<Col xs={24} sm={12} xl={8}>
@@ -512,6 +512,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
>
<InputNumber min={0} max={100} suffix="%" />
</Form.Item>
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("bodyshop.fields.dms.disablecontact")}
valuePropName="checked"
name={["cdk_configuration", "disablecontact"]}
>
<Switch />
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.disablecontactvehiclecreation")}

View File

@@ -49,6 +49,22 @@ export const CHECK_EMPLOYEE_NUMBER = gql`
}
`;
export const CHECK_EMPLOYEE_EMAIL = gql`
query CHECK_EMPLOYEE_EMAIL($email: String!, $shopId: uuid!) {
users(where: { email: { _ilike: $email } }) {
email
}
employees_aggregate(where: { user_email: { _ilike: $email }, shopid: { _eq: $shopId } }) {
aggregate {
count
}
nodes {
id
}
}
}
`;
export const QUERY_ACTIVE_EMPLOYEES = gql`
query QUERY_ACTIVE_EMPLOYEES {
employees(where: { active: { _eq: true } }) {

View File

@@ -1,5 +1,4 @@
import FingerprintJS from "@fingerprintjs/fingerprintjs";
//import { setUserId, setUserProperties } from "@firebase/analytics";
import {
checkActionCode,
confirmPasswordReset,
@@ -9,11 +8,9 @@ import {
} from "@firebase/auth";
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
import { getToken } from "@firebase/messaging";
// import * as Sentry from "@sentry/react";
import { notification } from "antd";
import axios from "axios";
import i18next from "i18next";
//import LogRocket from "logrocket";
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
import {
auth,
@@ -48,9 +45,13 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
//import posthog from "posthog-js";
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
//import { setUserId, setUserProperties } from "@firebase/analytics";
//import * as Sentry from "@sentry/react";
//import LogRocket from "logrocket";
//import posthog from "posthog-js";
const fpPromise = FingerprintJS.load();
export function* onEmailSignInStart() {

View File

@@ -370,6 +370,7 @@
"cashierid": "Cashier ID",
"default_journal": "Default Journal",
"disablebillwip": "Disable bill WIP for A/P Posting",
"disablecontact": "Disable Contact Updates/Creation",
"disablecontactvehiclecreation": "Disable Contact & Vehicle Updates/Creation",
"dms_acctnumber": "DMS Account #",
"dms_control_override": "Static Control # Override",
@@ -1350,7 +1351,8 @@
"vacationadded": "Employee vacation added."
},
"validation": {
"unique_employee_number": "You must enter a unique employee number."
"unique_employee_number": "You must enter a unique employee number.",
"unique_user_email": "This email is already assigned to another employee."
}
},
"esignature": {
@@ -3053,6 +3055,9 @@
"appointments": {
"appointment_confirmation": "Appointment Confirmation"
},
"banners": {
"esignature_promo": "Tired of getting paper signatures? Try E-Signatures today. Contact support to add this feature."
},
"bills": {
"inhouse_invoice": "In House Invoice"
},

View File

@@ -370,6 +370,7 @@
"cashierid": "",
"default_journal": "",
"disablebillwip": "",
"disablecontact": "",
"disablecontactvehiclecreation": "",
"dms_acctnumber": "",
"dms_control_override": "",
@@ -1350,7 +1351,8 @@
"vacationadded": ""
},
"validation": {
"unique_employee_number": ""
"unique_employee_number": "",
"unique_user_email": "Este correo electrónico ya está asignado a otro empleado."
}
},
"esignature": {
@@ -3053,6 +3055,9 @@
"appointments": {
"appointment_confirmation": ""
},
"banners": {
"esignature_promo": "¿Cansado de obtener firmas en papel? Prueba las firmas electrónicas hoy. Contacta a ventas para agregar esta función."
},
"bills": {
"inhouse_invoice": ""
},

View File

@@ -370,6 +370,7 @@
"cashierid": "",
"default_journal": "",
"disablebillwip": "",
"disablecontact": "",
"disablecontactvehiclecreation": "",
"dms_acctnumber": "",
"dms_control_override": "",
@@ -1350,7 +1351,8 @@
"vacationadded": ""
},
"validation": {
"unique_employee_number": ""
"unique_employee_number": "",
"unique_user_email": "Cette adresse courriel est déjà assignée à un autre employé."
}
},
"esignature": {
@@ -3053,6 +3055,9 @@
"appointments": {
"appointment_confirmation": ""
},
"banners": {
"esignature_promo": "Vous en avez assez des signatures papier? Essayez les signatures électroniques dès aujourd'hui. Communiquez avec les ventes pour ajouter cette fonction."
},
"bills": {
"inhouse_invoice": ""
},

View File

@@ -237,9 +237,7 @@ export default defineConfig(({ command, mode }) => {
lodash: ["lodash"],
"@sentry/react": ["@sentry/react"],
"feature-flags": ["src/feature-flags/splitio-react-replacement.jsx"],
logrocket: ["logrocket"],
firebase: [
"@firebase/analytics",
"@firebase/app",
"@firebase/firestore",
"@firebase/auth",

View File

@@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/aws" {
version = "6.38.0"
constraints = "~> 6.0"
hashes = [
"h1:IMf41BcW9huOeVcrt6XjQqadYR2xD8zkUpGLLERJ4NM=",
"h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=",
"zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed",
"zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522",
@@ -28,6 +29,7 @@ provider "registry.terraform.io/hashicorp/random" {
version = "3.8.1"
constraints = "~> 3.6"
hashes = [
"h1:osH3aBqEARwOz3VBJKdpFKJJCNIdgRC6k8vPojkLmlY=",
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",

View File

@@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.14.3",
"serial": 105,
"terraform_version": "1.15.4",
"serial": 111,
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
"outputs": {
"application_url": {
@@ -21,7 +21,7 @@
"type": "string"
},
"postgres_engine_version": {
"value": "17.9",
"value": "17.10",
"type": "string"
},
"secrets_manager_secret_name": {
@@ -118,7 +118,7 @@
"filter": null,
"has_major_target": null,
"has_minor_target": null,
"id": "17.9",
"id": "17.10",
"include_all": null,
"latest": true,
"parameter_group_family": "postgres17",
@@ -144,15 +144,15 @@
"supports_parallel_query": false,
"supports_read_replica": true,
"valid_major_targets": [
"18.3"
"18.4"
],
"valid_minor_targets": [],
"valid_upgrade_targets": [
"18.3"
"18.4"
],
"version": "17.9",
"version_actual": "17.9",
"version_description": "PostgreSQL 17.9-R1"
"version": "17.10",
"version_actual": "17.10",
"version_description": "PostgreSQL 17.10-R1"
},
"sensitive_attributes": [],
"identity_schema_version": 0
@@ -1085,7 +1085,7 @@
"endpoint": "documenso-postgres.cfo5pnykioqq.ca-central-1.rds.amazonaws.com:5432",
"engine": "postgres",
"engine_lifecycle_support": "open-source-rds-extended-support",
"engine_version": "17.9",
"engine_version": "17.10",
"engine_version_actual": "17.9",
"final_snapshot_identifier": "documenso-final-03443461",
"hosted_zone_id": "Z1JG78A3UK1DU3",
@@ -1096,7 +1096,7 @@
"instance_class": "db.t4g.micro",
"iops": 3000,
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
"latest_restorable_time": "2026-05-01T17:49:36Z",
"latest_restorable_time": "2026-05-25T20:16:55Z",
"license_model": "postgresql-license",
"listener_endpoint": [],
"maintenance_window": "tue:03:10-tue:03:40",
@@ -1384,7 +1384,7 @@
"Application": "documenso",
"ManagedBy": "Terraform"
},
"task_definition": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:8",
"task_definition": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:9",
"timeouts": null,
"triggers": {},
"volume_configuration": [],
@@ -1451,9 +1451,9 @@
{
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:8",
"arn": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:9",
"arn_without_revision": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task",
"container_definitions": "[{\"environment\":[{\"name\":\"NEXT_PRIVATE_INTERNAL_WEBAPP_URL\",\"value\":\"http://127.0.0.1:3000\"},{\"name\":\"NEXT_PRIVATE_SMTP_HOST\",\"value\":\"email-smtp.ca-central-1.amazonaws.com\"},{\"name\":\"NEXT_PRIVATE_SMTP_PORT\",\"value\":\"587\"},{\"name\":\"NEXT_PRIVATE_SMTP_SECURE\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_SMTP_TRANSPORT\",\"value\":\"smtp-auth\"},{\"name\":\"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_BUCKET\",\"value\":\"documenso-714144183158-ca-central-1\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_REGION\",\"value\":\"ca-central-1\"},{\"name\":\"NEXT_PUBLIC_DISABLE_SIGNUP\",\"value\":\"true\"},{\"name\":\"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT\",\"value\":\"10\"},{\"name\":\"NEXT_PUBLIC_UPLOAD_TRANSPORT\",\"value\":\"s3\"},{\"name\":\"NEXT_PUBLIC_WEBAPP_URL\",\"value\":\"https://sign.imex.online\"},{\"name\":\"PORT\",\"value\":\"3000\"}],\"essential\":true,\"image\":\"documenso/documenso:2.10.0\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/documenso\",\"awslogs-region\":\"ca-central-1\",\"awslogs-stream-prefix\":\"documenso\"}},\"mountPoints\":[],\"name\":\"documenso\",\"portMappings\":[{\"containerPort\":3000,\"hostPort\":3000,\"protocol\":\"tcp\"}],\"secrets\":[{\"name\":\"NEXTAUTH_SECRET\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXTAUTH_SECRET::\"},{\"name\":\"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS::\"},{\"name\":\"NEXT_PRIVATE_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DIRECT_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DIRECT_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_PASSPHRASE\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_PASSPHRASE::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_ADDRESS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_ADDRESS::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_NAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_NAME::\"},{\"name\":\"NEXT_PRIVATE_SMTP_PASSWORD\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_PASSWORD::\"},{\"name\":\"NEXT_PRIVATE_SMTP_USERNAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_USERNAME::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY::\"}],\"systemControls\":[],\"volumesFrom\":[]}]",
"container_definitions": "[{\"environment\":[{\"name\":\"NEXT_PRIVATE_INTERNAL_WEBAPP_URL\",\"value\":\"http://127.0.0.1:3000\"},{\"name\":\"NEXT_PRIVATE_SMTP_HOST\",\"value\":\"email-smtp.ca-central-1.amazonaws.com\"},{\"name\":\"NEXT_PRIVATE_SMTP_PORT\",\"value\":\"587\"},{\"name\":\"NEXT_PRIVATE_SMTP_SECURE\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_SMTP_TRANSPORT\",\"value\":\"smtp-auth\"},{\"name\":\"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_BUCKET\",\"value\":\"documenso-714144183158-ca-central-1\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_REGION\",\"value\":\"ca-central-1\"},{\"name\":\"NEXT_PUBLIC_DISABLE_SIGNUP\",\"value\":\"true\"},{\"name\":\"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT\",\"value\":\"10\"},{\"name\":\"NEXT_PUBLIC_UPLOAD_TRANSPORT\",\"value\":\"s3\"},{\"name\":\"NEXT_PUBLIC_WEBAPP_URL\",\"value\":\"https://sign.imex.online\"},{\"name\":\"PORT\",\"value\":\"3000\"}],\"essential\":true,\"image\":\"documenso/documenso:2.11.0\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/documenso\",\"awslogs-region\":\"ca-central-1\",\"awslogs-stream-prefix\":\"documenso\"}},\"mountPoints\":[],\"name\":\"documenso\",\"portMappings\":[{\"containerPort\":3000,\"hostPort\":3000,\"protocol\":\"tcp\"}],\"secrets\":[{\"name\":\"NEXTAUTH_SECRET\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXTAUTH_SECRET::\"},{\"name\":\"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS::\"},{\"name\":\"NEXT_PRIVATE_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DIRECT_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DIRECT_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_PASSPHRASE\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_PASSPHRASE::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_ADDRESS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_ADDRESS::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_NAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_NAME::\"},{\"name\":\"NEXT_PRIVATE_SMTP_PASSWORD\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_PASSWORD::\"},{\"name\":\"NEXT_PRIVATE_SMTP_USERNAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_USERNAME::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY::\"}],\"systemControls\":[],\"volumesFrom\":[]}]",
"cpu": "512",
"enable_fault_injection": false,
"ephemeral_storage": [],
@@ -1470,7 +1470,7 @@
"requires_compatibilities": [
"FARGATE"
],
"revision": 8,
"revision": 9,
"runtime_platform": [],
"skip_destroy": false,
"tags": {
@@ -1498,7 +1498,7 @@
"account_id": "714144183158",
"family": "documenso-task",
"region": "ca-central-1",
"revision": 8
"revision": 9
},
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [

View File

@@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.14.3",
"serial": 101,
"terraform_version": "1.15.4",
"serial": 105,
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
"outputs": {
"application_url": {
@@ -1096,7 +1096,7 @@
"instance_class": "db.t4g.micro",
"iops": 3000,
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
"latest_restorable_time": "2026-05-01T15:49:30Z",
"latest_restorable_time": "2026-05-01T17:49:36Z",
"license_model": "postgresql-license",
"listener_endpoint": [],
"maintenance_window": "tue:03:10-tue:03:40",
@@ -3551,7 +3551,7 @@
],
"description": "WAF protection for Documenso",
"id": "04577153-2a1a-462c-94b8-b0a1804755bb",
"lock_token": "e71f2816-492c-4afc-acc2-3700795c2657",
"lock_token": "417061f1-deea-4ac2-b932-9bea49265444",
"name": "documenso-web-acl",
"name_prefix": "",
"region": "ca-central-1",
@@ -3693,7 +3693,24 @@
{
"managed_rule_group_configs": [],
"name": "AWSManagedRulesCommonRuleSet",
"rule_action_override": [],
"rule_action_override": [
{
"action_to_use": [
{
"allow": [],
"block": [],
"captcha": [],
"challenge": [],
"count": [
{
"custom_request_handling": []
}
]
}
],
"name": "SizeRestrictions_BODY"
}
],
"scope_down_statement": [],
"vendor_name": "AWS",
"version": ""

2166
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,63 +19,63 @@
"feature-flags:export-harness": "node scripts/export-harness-feature-flags.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.1020.0",
"@aws-sdk/client-elasticache": "^3.1020.0",
"@aws-sdk/client-s3": "^3.1020.0",
"@aws-sdk/client-secrets-manager": "^3.1020.0",
"@aws-sdk/client-ses": "^3.1020.0",
"@aws-sdk/client-sqs": "^3.1020.0",
"@aws-sdk/client-textract": "^3.1020.0",
"@aws-sdk/credential-provider-node": "^3.972.28",
"@aws-sdk/lib-storage": "^3.1020.0",
"@aws-sdk/s3-request-presigner": "^3.1020.0",
"@documenso/sdk-typescript": "^0.8.0",
"@jsreport/nodejs-client": "^4.1.0",
"@aws-sdk/client-cloudwatch-logs": "^3.1053.0",
"@aws-sdk/client-elasticache": "^3.1053.0",
"@aws-sdk/client-s3": "^3.1053.0",
"@aws-sdk/client-secrets-manager": "^3.1053.0",
"@aws-sdk/client-ses": "^3.1053.0",
"@aws-sdk/client-sqs": "^3.1053.0",
"@aws-sdk/client-textract": "^3.1053.0",
"@aws-sdk/credential-provider-node": "^3.972.44",
"@aws-sdk/lib-storage": "^3.1053.0",
"@aws-sdk/s3-request-presigner": "^3.1053.0",
"@documenso/sdk-typescript": "^0.8.1",
"@jsreport/nodejs-client": "^4.1.1",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.1",
"aws4": "^1.13.2",
"axios": "^1.14.0",
"axios": "^1.16.1",
"axios-curlirize": "^2.0.0",
"better-queue": "^3.8.12",
"bullmq": "^5.71.1",
"bullmq": "^5.77.3",
"chart.js": "^4.5.1",
"cloudinary": "^2.9.0",
"cloudinary": "^2.10.0",
"compression": "^1.8.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"crisp-status-reporter": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^17.3.1",
"dotenv": "^17.4.2",
"express": "^4.21.1",
"fast-xml-parser": "^5.5.9",
"firebase-admin": "^13.7.0",
"fuse.js": "^7.1.0",
"graphql": "^16.13.2",
"fast-xml-parser": "^5.8.0",
"firebase-admin": "^13.10.0",
"fuse.js": "^7.3.0",
"graphql": "^16.14.0",
"graphql-request": "^6.1.0",
"intuit-oauth": "^4.2.2",
"intuit-oauth": "^4.2.3",
"ioredis": "^5.10.1",
"json-2-csv": "^5.5.10",
"jsonwebtoken": "^9.0.3",
"juice": "^11.1.1",
"lodash": "^4.17.23",
"lodash": "^4.18.1",
"moment": "^2.30.1",
"moment-timezone": "^0.6.1",
"moment-timezone": "^0.6.2",
"multer": "^2.1.1",
"mustache": "^4.2.0",
"node-persist": "^4.0.4",
"nodemailer": "^6.10.0",
"normalize-url": "^9.0.0",
"normalize-url": "^9.0.1",
"pdf-lib": "^1.17.1",
"phone": "^3.1.71",
"query-string": "7.1.3",
"recursive-diff": "^1.0.9",
"rimraf": "^6.1.3",
"skia-canvas": "^3.0.8",
"soap": "^1.8.0",
"soap": "^1.9.3",
"socket.io": "^4.8.3",
"socket.io-adapter": "^2.5.6",
"socket.io-adapter": "^2.5.7",
"ssh2-sftp-client": "^11.0.0",
"twilio": "^5.13.1",
"uuid": "^11.1.0",
@@ -90,11 +90,11 @@
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.4.0",
"globals": "^17.6.0",
"mock-require": "^3.0.3",
"p-limit": "^3.1.0",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"supertest": "^7.2.2",
"vitest": "^4.1.2"
"vitest": "^4.1.7"
}
}

View File

@@ -1,488 +1,526 @@
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.
//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 {
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 (value === undefined || value === null) {
return fallback;
}
if (typeof value !== "string") {
return value;
}
if (typeof value !== "string") {
return value;
}
try {
return JSON.parse(value);
} catch {
return fallback;
}
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
function getDefaultEsignData({ esigData, bodyshop, fileName }) {
const fallbackTitle = fileName || `Esign request from ${bodyshop.shopname}`;
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}.`
};
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;
const error = new Error(message);
error.statusCode = statusCode;
return error;
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(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;
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();
const ownerEmail = jobData?.ownr_ea?.trim();
if (!ownerEmail) {
throw createClientError("Job owner email is required before sending an e-signature request.");
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"
}
if (!isValidEmail(ownerEmail)) {
throw createClientError(`Job owner email "${ownerEmail}" is not valid.`);
}
return [
{
email: ownerEmail,
name: getJobOwnerName(jobData, ownerEmail),
role: "SIGNER"
}
];
];
}
async function getDocumensoClient({ bodyshopid, req }) {
const client = req.userGraphQLClient;
const { bodyshops_by_pk: { documenso_api_key } } = await client.request(QUERY_DOCUMENSO_KEY, { bodyshopid });
return new Documenso({
apiKey: documenso_api_key,//Done on a by team basis,
serverURL: "https://sign.imex.online/api/v2",
});
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 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 { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid });
const recipients = getJobOwnerRecipients(jobData);
const documenso = await getDocumensoClient({ bodyshopid: bodyshop.id, req })
const 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 createDocumentResponse = await documenso.documents.create({
payload: {
title: resolvedEsigData.title,
externalId: `${jobid}|${req.user?.email}`,
recipients,
meta: {
timezone: bodyshop.timezone,
dateFormat: "MM/dd/yyyy hh:mm a",
language: "en",
subject: resolvedEsigData.subject,
message: resolvedEsigData.message
}
},
file: fileBlob
});
const documentResult = await documenso.documents.get({
documentId: createDocumentResponse.id,
});
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
});
}
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({});
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,
}
});
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
};
return {
token: presignToken.token,
documentId: createDocumentResponse.id,
envelopeId: createDocumentResponse.envelopeId
};
}
async function distributeDocument(req, res) {
try {
const client = req.userGraphQLClient;
try {
const client = req.userGraphQLClient;
const { documentId, bodyshopid } = req.body;
const documenso = await getDocumensoClient({ bodyshopid, req })
const { documentId, bodyshopid } = req.body;
const documenso = await getDocumensoClient({ bodyshopid, req });
const distributeResult = await documenso.documents.distribute({
documentId,
});
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'
}
})
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 });
}
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;
try {
const client = req.userGraphQLClient;
const { documentId, bodyshopid } = req.body;
const documenso = await getDocumensoClient({ bodyshopid, req })
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 document = await documenso.documents.get({
documentId: parseInt(documentId)
});
const distributeResult = await documenso.documents.redistribute({
documentId: parseInt(documentId),
recipients: document.recipients.filter((r) => r.signingStatus === "NOT_SIGNED").map((r) => r.id)
});
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"
}
});
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 });
}
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;
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() });
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." });
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 })
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 });
}
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
});
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 });
}
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;
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);
if (!uploadedDocument?.buffer) {
return res.status(400).json({ error: "A PDF document is required." });
}
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 });
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 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}`,
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
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"
}
};
const render = await jsreportClient.render(reportRequest);
})
},
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 }
//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 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 folders = await axios.get(`${JSR_SERVER}/odata/folders`, {
headers: { Authorization: jsrAuth }
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
});
const shopSpecificFolder = folders.data.value.find((f) => f.name === bodyshop.imexshopid);
parsedEsigData = {};
}
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 } }
);
return {
contextData,
useShopSpecificTemplate,
shopSpecificFolder,
esigData: parsedEsigData
};
// }
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);
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
};
module.exports = {
newEsignDocument,
newCustomEsignDocument,
distributeDocument,
redistributeDocument,
deleteDocument,
viewDocument,
getDocumensoClient
}
newEsignDocument,
newCustomEsignDocument,
distributeDocument,
redistributeDocument,
deleteDocument,
viewDocument,
getDocumensoClient
};

View File

@@ -15,7 +15,7 @@ const _ = require("lodash");
const moment = require("moment-timezone");
const replaceSpecialRegex = /[^a-zA-Z0-9 ]+/g;
const bypassCustomerId = "bypass";
// Helper function to handle FortellisApiError logging
function handleFortellisApiError(socket, error, functionName, additionalDetails = {}) {
if (error instanceof FortellisApiError) {
@@ -95,7 +95,8 @@ async function FortellisJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
defaultFortellisTTL
);
let DMSVehCustomer;
let DMSVehCustomerFromVehicle;
//let DMSVehCustomer;
if (!DMSVid.newId) {
CreateFortellisLogEvent(socket, "DEBUG", `{2.1} Querying the Vehicle using the DMSVid: ${DMSVid.vehiclesVehId}`);
const DMSVeh = await QueryDmsVehicleById({ socket, redisHelpers, JobData, DMSVid });
@@ -106,46 +107,66 @@ async function FortellisJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
DMSVeh,
defaultFortellisTTL
);
DMSVehCustomerFromVehicle = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
const DMSVehCustomerFromVehicle =
DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
// //Add in contact bypass for Fortellis.
// if (!JobData.bodyshop.cdk_configuration.disablecontact) {
// const DMSVehCustomerFromVehicle =
// DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
if (DMSVehCustomerFromVehicle?.id && DMSVehCustomerFromVehicle.id.value) {
CreateFortellisLogEvent(
socket,
"DEBUG",
`{2.2} Querying the Customer using the ID from DMSVeh: ${DMSVehCustomerFromVehicle.id.value}`
);
DMSVehCustomer = await QueryDmsCustomerById({
socket,
redisHelpers,
JobData,
CustomerId: DMSVehCustomerFromVehicle.id.value
});
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSVehCustomer,
DMSVehCustomer,
defaultFortellisTTL
);
}
// if (DMSVehCustomerFromVehicle?.id && DMSVehCustomerFromVehicle.id.value) {
// CreateFortellisLogEvent(
// socket,
// "DEBUG",
// `{2.2} Querying the Customer using the ID from DMSVeh: ${DMSVehCustomerFromVehicle.id.value}`
// );
// DMSVehCustomer = await QueryDmsCustomerById({
// socket,
// redisHelpers,
// JobData,
// CustomerId: DMSVehCustomerFromVehicle.id.value
// });
// await setSessionTransactionData(
// socket.id,
// getTransactionType(jobid),
// FortellisCacheEnums.DMSVehCustomer,
// DMSVehCustomer,
// defaultFortellisTTL
// );
// }
// }
}
CreateFortellisLogEvent(socket, "DEBUG", `{2.3} Querying the Customer using the name.`);
if (JobData.bodyshop.cdk_configuration.disablecontact) {
//Just go straight to posting.
await FortellisSelectedCustomer({ socket, redisHelpers, selectedCustomerId: bypassCustomerId, jobid });
} else {
const DMSCustList = await QueryDmsCustomerByName({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCustList,
DMSCustList,
defaultFortellisTTL
);
const DMSCustList = await QueryDmsCustomerByName({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCustList,
DMSCustList,
defaultFortellisTTL
);
socket.emit("fortellis-select-customer", [
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
...DMSCustList
]);
socket.emit("fortellis-select-customer",
//Removed to save one one API call while disputing with fortellis.
// [
// // ...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
// ...DMSCustList
// ]
DMSVehCustomerFromVehicle ?
DMSCustList.map(c => {
//if customer id is the same as the current assigned owner on the vehicle id, set it as vinowner true. )
if (DMSVehCustomerFromVehicle?.id?.value === c.customerId) {
return { ...c, vinOwner: true }
} else {
return c
}
}) : DMSCustList
);
}
} catch (error) {
CreateFortellisLogEvent(socket, "ERROR", `Error in FortellisJobExport - ${error} `, {
error: error.message,
@@ -218,36 +239,40 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
});
return;
}
//Bypass only the customer creation. We still need to create the vehicle and update it to post the service history later on.
let DMSCust;
if (selectedCustomerId) {
CreateFortellisLogEvent(socket, "DEBUG", `{3.1} Querying the Customer using Customer ID: ${selectedCustomerId}`);
if (!JobData.bodyshop.cdk_configuration.disablecontact) {
if (selectedCustomerId) {
CreateFortellisLogEvent(socket, "DEBUG", `{3.1} Querying the Customer using Customer ID: ${selectedCustomerId}`);
//Get cust list from Redis. Return the item
const DMSCustList =
(await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSCustList)) || [];
const existingCustomerInDMSCustList = DMSCustList.find((c) => c.customerId === selectedCustomerId);
DMSCust = existingCustomerInDMSCustList || {
customerId: selectedCustomerId //This is the fall back in case it is the generic customer.
};
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCust,
DMSCust,
defaultFortellisTTL
);
} else {
CreateFortellisLogEvent(socket, "DEBUG", `{3.2} Creating new customer.`);
const DMSCustomerInsertResponse = await InsertDmsCustomer({ socket, redisHelpers, JobData });
DMSCust = { customerId: DMSCustomerInsertResponse.data };
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCust,
DMSCust,
defaultFortellisTTL
);
//Get cust list from Redis. Return the item
const DMSCustList =
(await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSCustList)) || [];
const existingCustomerInDMSCustList = DMSCustList.find((c) => c.customerId === selectedCustomerId);
DMSCust = existingCustomerInDMSCustList || {
customerId: selectedCustomerId //This is the fall back in case it is the generic customer.
};
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCust,
DMSCust,
defaultFortellisTTL
);
} else {
CreateFortellisLogEvent(socket, "DEBUG", `{3.2} Creating new customer.`);
const DMSCustomerInsertResponse = await InsertDmsCustomer({ socket, redisHelpers, JobData });
DMSCust = { customerId: DMSCustomerInsertResponse.data };
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCust,
DMSCust,
defaultFortellisTTL
);
}
}else{
DMSCust = { customerId: bypassCustomerId };
}
let DMSVeh;
@@ -258,8 +283,12 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
DMSVeh = await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSVeh);
CreateFortellisLogEvent(socket, "DEBUG", `{4.3} Updating Existing Vehicle to associate to owner.`);
//If it's a bypass scenario, skip this all.
//Check to see if the vehicle needs to be updated - i.e. the owner is not the selected customer.
if (!DMSVeh?.owners.find((o) => o.id.value === DMSCust.customerId && o.id.assigningPartyId === "CURRENT")) {
if (
selectedCustomerId !== bypassCustomerId &&
!DMSVeh?.owners.find((o) => o.id.value === DMSCust.customerId && o.id.assigningPartyId === "CURRENT")
) {
DMSVeh = await UpdateDmsVehicle({
socket,
redisHelpers,
@@ -782,12 +811,14 @@ async function InsertDmsVehicle({ socket, redisHelpers, JobData, txEnvelope, DMS
// "chassis": "",
// "color": "",
// "dealerBodyStyle": "",
deliveryDate:
txEnvelope.dms_unsold === true
? ""
: moment()
...(DMSCust?.customerId !== bypassCustomerId && {
deliveryDate:
txEnvelope.dms_unsold === true
? ""
: moment()
// .tz(JobData.bodyshop.timezone)
.format("YYYY-MM-DD"),
}),
// "deliveryMileage": 4,
// "doorsQuantity": 4,
// "engineNumber": "",
@@ -902,14 +933,17 @@ async function InsertDmsVehicle({ socket, redisHelpers, JobData, txEnvelope, DMS
// "warrantyExpDate": "2015-01-12",
// "wheelbase": ""
},
owners: [
{
id: {
assigningPartyId: "CURRENT",
value: DMSCust.customerId
// Owners is not required. Exclude it if we are bypassing.
...(DMSCust?.customerId !== bypassCustomerId && {
owners: [
{
id: {
assigningPartyId: "CURRENT",
value: DMSCust.customerId
}
}
}
]
]
})
//"inventoryAccount": "237"
}
});
@@ -1009,12 +1043,14 @@ async function UpdateDmsVehicle({ socket, redisHelpers, JobData, DMSVeh, DMSCust
modelAbrev: txEnvelope.dms_model
}
: {}),
deliveryDate:
txEnvelope.dms_unsold === true
? ""
: moment(DMSVehToSend.vehicle.deliveryDate)
...(DMSCust?.customerId !== bypassCustomerId && {
deliveryDate:
txEnvelope.dms_unsold === true
? ""
: moment(DMSVehToSend.vehicle.deliveryDate)
//.tz(JobData.bodyshop.timezone)
.toISOString()
})
},
owners: ids
}