feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint - Remove old attempt at Reynolds Integration in favor of new library.

This commit is contained in:
Dave
2025-10-29 10:42:32 -04:00
parent 319f3220ed
commit e06f0f9918
36 changed files with 2468 additions and 3736 deletions

377
client/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.2.1",
"hasInstallScript": true,
"dependencies": {
"@amplitude/analytics-browser": "^2.27.0",
"@amplitude/analytics-browser": "^2.29.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^3.13.9",
"@emotion/is-prop-valid": "^1.4.0",
@@ -20,21 +20,21 @@
"@firebase/firestore": "^4.9.2",
"@firebase/messaging": "^0.12.22",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.9.1",
"@sentry/cli": "^2.56.1",
"@reduxjs/toolkit": "^2.9.2",
"@sentry/cli": "^2.57.0",
"@sentry/react": "^9.43.0",
"@sentry/vite-plugin": "^4.4.0",
"@sentry/vite-plugin": "^4.6.0",
"@splitsoftware/splitio-react": "^2.5.0",
"@tanem/react-nprogress": "^5.0.56",
"antd": "^5.27.5",
"antd": "^5.27.6",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.4.0",
"autosize": "^6.0.1",
"axios": "^1.12.2",
"axios": "^1.13.1",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.18",
"dayjs-business-days2": "^1.3.0",
"dayjs-business-days2": "^1.3.1",
"dinero.js": "^1.9.1",
"dotenv": "^17.2.3",
"env-cmd": "^10.1.0",
@@ -43,7 +43,7 @@
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.24",
"libphonenumber-js": "^1.12.25",
"lightningcss": "^1.30.2",
"logrocket": "^9.0.2",
"markerjs2": "^2.32.7",
@@ -51,7 +51,7 @@
"normalize-url": "^8.1.0",
"object-hash": "^3.0.0",
"phone": "^3.1.67",
"posthog-js": "^1.276.0",
"posthog-js": "^1.281.0",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
@@ -79,7 +79,7 @@
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-saga": "^1.4.2",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.93.2",
@@ -93,31 +93,31 @@
"devDependencies": {
"@ant-design/icons": "^6.1.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.27.1",
"@babel/preset-react": "^7.28.5",
"@dotenvx/dotenvx": "^1.51.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.38.0",
"@playwright/test": "^1.56.1",
"@sentry/webpack-plugin": "^4.4.0",
"@sentry/webpack-plugin": "^4.6.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.6.0",
"browserslist": "^4.26.3",
"browserslist": "^4.27.0",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.6.2",
"eslint": "^9.38.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"memfs": "^4.49.0",
"memfs": "^4.50.0",
"os-browserify": "^0.3.0",
"playwright": "^1.56.1",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^7.1.10",
"vite": "^7.1.12",
"vite-plugin-babel": "^1.3.2",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.24.0",
@@ -141,16 +141,17 @@
"license": "MIT"
},
"node_modules/@amplitude/analytics-browser": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.27.0.tgz",
"integrity": "sha512-1LBCLmnr7aUpLtOp64lpr8GzN3vPKM0fwiM/7tWJ9XU9/GKA+k3CUSjI8OdERKrw2yVywujoAVQo4anGZXYIDA==",
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.29.0.tgz",
"integrity": "sha512-e+9tdTisSjj//BhMK4Ej8rhldfhcZXurxsfqns2DyXjf+bxLQwO32ZTIbfQMy+CAiRwVRc2nuZXyfFCpW2DgMA==",
"license": "MIT",
"dependencies": {
"@amplitude/analytics-core": "^2.28.0",
"@amplitude/plugin-autocapture-browser": "^1.15.3",
"@amplitude/plugin-network-capture-browser": "^1.6.9",
"@amplitude/plugin-page-view-tracking-browser": "^2.5.3",
"@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.31",
"@amplitude/analytics-core": "^2.30.0",
"@amplitude/plugin-autocapture-browser": "^1.16.2",
"@amplitude/plugin-network-capture-browser": "^1.6.11",
"@amplitude/plugin-page-url-enrichment-browser": "^0.5.0",
"@amplitude/plugin-page-view-tracking-browser": "^2.5.5",
"@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.33",
"tslib": "^2.4.1"
}
},
@@ -161,9 +162,9 @@
"license": "MIT"
},
"node_modules/@amplitude/analytics-core": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.28.0.tgz",
"integrity": "sha512-Wj/xUHhiHk2xH0/lp5IgHlzyKJOIb/WkpWP8W66wf3EaLJwX0AtPWMEb557BHbYBG/KXonp6ob9DyD0xbW38dg==",
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.30.0.tgz",
"integrity": "sha512-oWz13sQmfCRa9prfYcURPVNHQQOf0/rDEK7PjBi0m0nKXs+QaHu/Tq2y4F6f4K66YDsT7zYYO4DpkS+QJM3/Sw==",
"license": "MIT",
"dependencies": {
"@amplitude/analytics-connector": "^1.6.4",
@@ -171,34 +172,44 @@
}
},
"node_modules/@amplitude/plugin-autocapture-browser": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.15.3.tgz",
"integrity": "sha512-WPYw81fFdTzUxEzM3/lTalsxLNGDKFklGnDaHoLFB4LxL5XfIyC+lBnOls+9zjt7dyV0qvq5YzunbFNT2ysR8g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.16.2.tgz",
"integrity": "sha512-0ariOP5g8xsPlKUrC91DviG9nKuaC4hYPHu5OHpUcU/LdWiKG1k+w5qawUq+KIsyTCAFRbTHn+XoJPjhm6Mbcg==",
"license": "MIT",
"dependencies": {
"@amplitude/analytics-core": "^2.28.0",
"@amplitude/analytics-core": "^2.30.0",
"rxjs": "^7.8.1",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/plugin-network-capture-browser": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.6.9.tgz",
"integrity": "sha512-dBp0FiXGwreFfEZvWBqe+VbbwSRsljRwdYdtSQvBeYriBWZp7g3rD02xmt8zjlWxMpQTAE0wLjAi7E01NclBXg==",
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.6.11.tgz",
"integrity": "sha512-kGbSZ5omr9842kzBN/Wx31kq59RcyclEhpsX2lDxkuHn4el+nhylztx2mNYJyEmeNmdV68MfA1IFkA//ZJzebA==",
"license": "MIT",
"dependencies": {
"@amplitude/analytics-core": "^2.28.0",
"@amplitude/analytics-core": "^2.30.0",
"rxjs": "^7.8.1",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/plugin-page-view-tracking-browser": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.5.3.tgz",
"integrity": "sha512-zqdZ01mScGHwvxoCLiz+qClA7sbryozeX2BmRt4mxkk9qkukKuMs2xGijgUDqsrWX0UbkYczFZhPbhm+4Jti9g==",
"node_modules/@amplitude/plugin-page-url-enrichment-browser": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.5.0.tgz",
"integrity": "sha512-MJOCHQa3H5t2khPuGwgGX36tuA7ohDwzXyuPVZbY6fN3qRT9LAE686VuvQSXH9365yHMcWp2vPkdn8Ju3i9IUw==",
"license": "MIT",
"dependencies": {
"@amplitude/analytics-core": "^2.28.0",
"@amplitude/analytics-core": "^2.30.0",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/plugin-page-view-tracking-browser": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.5.5.tgz",
"integrity": "sha512-pSeYW3YjdFdJEA8WFQbWwaez4q1hwr4alg+Z7y1DU0hoQ91ADxCrFDQ2zoTSyBZfu/paWx1Gds1v4sYjsgh5Bw==",
"license": "MIT",
"dependencies": {
"@amplitude/analytics-core": "^2.30.0",
"tslib": "^2.4.1"
}
},
@@ -2185,15 +2196,15 @@
}
},
"node_modules/@babel/preset-react": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz",
"integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz",
"integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-transform-react-display-name": "^7.27.1",
"@babel/plugin-transform-react-display-name": "^7.28.0",
"@babel/plugin-transform-react-jsx": "^7.27.1",
"@babel/plugin-transform-react-jsx-development": "^7.27.1",
"@babel/plugin-transform-react-pure-annotations": "^7.27.1"
@@ -3419,9 +3430,9 @@
}
},
"node_modules/@posthog/core": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz",
"integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.4.0.tgz",
"integrity": "sha512-jmW8/I//YOHAfjzokqas+Qtc2T57Ux8d2uIJu7FLcMGxywckHsl6od59CD18jtUzKToQdjQhV6Y3429qj+KeNw==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
@@ -3664,17 +3675,17 @@
}
},
"node_modules/@redux-saga/core": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz",
"integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.4.2.tgz",
"integrity": "sha512-nIMLGKo6jV6Wc1sqtVQs1iqbB3Kq20udB/u9XEaZQisT6YZ0NRB8+4L6WqD/E+YziYutd27NJbG8EWUPkb7c6Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@redux-saga/deferred": "^1.2.1",
"@redux-saga/delay-p": "^1.2.1",
"@redux-saga/is": "^1.1.3",
"@redux-saga/symbols": "^1.1.3",
"@redux-saga/types": "^1.2.1",
"@babel/runtime": "^7.28.4",
"@redux-saga/deferred": "^1.3.1",
"@redux-saga/delay-p": "^1.3.1",
"@redux-saga/is": "^1.2.1",
"@redux-saga/symbols": "^1.2.1",
"@redux-saga/types": "^1.3.1",
"typescript-tuple": "^2.2.1"
},
"funding": {
@@ -3683,46 +3694,46 @@
}
},
"node_modules/@redux-saga/deferred": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz",
"integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.3.1.tgz",
"integrity": "sha512-0YZ4DUivWojXBqLB/TmuRRpDDz7tyq1I0AuDV7qi01XlLhM5m51W7+xYtIckH5U2cMlv9eAuicsfRAi1XHpXIg==",
"license": "MIT"
},
"node_modules/@redux-saga/delay-p": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz",
"integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.3.1.tgz",
"integrity": "sha512-597I7L5MXbD/1i3EmcaOOjL/5suxJD7p5tnbV1PiWnE28c2cYiIHqmSMK2s7us2/UrhOL2KTNBiD0qBg6KnImg==",
"license": "MIT",
"dependencies": {
"@redux-saga/symbols": "^1.1.3"
"@redux-saga/symbols": "^1.2.1"
}
},
"node_modules/@redux-saga/is": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz",
"integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.2.1.tgz",
"integrity": "sha512-x3aWtX3GmQfEvn8dh0ovPbsXgK9JjpiR24wKztpGbZP8JZUWWvUgKrvnWZ/T/4iphOBftyVc9VrIwhAnsM+OFA==",
"license": "MIT",
"dependencies": {
"@redux-saga/symbols": "^1.1.3",
"@redux-saga/types": "^1.2.1"
"@redux-saga/symbols": "^1.2.1",
"@redux-saga/types": "^1.3.1"
}
},
"node_modules/@redux-saga/symbols": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz",
"integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.2.1.tgz",
"integrity": "sha512-3dh+uDvpBXi7EUp/eO+N7eFM4xKaU4yuGBXc50KnZGzIrR/vlvkTFQsX13zsY8PB6sCFYAgROfPSRUj8331QSA==",
"license": "MIT"
},
"node_modules/@redux-saga/types": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz",
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.3.1.tgz",
"integrity": "sha512-YRCrJdhQLobGIQ8Cj1sta3nn6DrZDTSUnrIYhS2e5V590BmfVDleKoAquclAiKSBKWJwmuXTb+b4BL6rSHnahw==",
"license": "MIT"
},
"node_modules/@reduxjs/toolkit": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.1.tgz",
"integrity": "sha512-sETJ3qO72y7L7WiR5K54UFLT3jRzAtqeBPVO15xC3bGA6kDqCH8m/v7BKCPH4czydXzz/1lPEGLvew7GjOO3Qw==",
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
"integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
@@ -4385,9 +4396,9 @@
}
},
"node_modules/@sentry/babel-plugin-component-annotate": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.4.0.tgz",
"integrity": "sha512-Pzjpn9MZg6yR61ThJgOoD28dLNCj457O0/t8d276K+Bzf8iOZKbrNO4sltp1vUB1yqhV+ulvIZO8xu8ABohtsg==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.0.tgz",
"integrity": "sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -4410,14 +4421,14 @@
}
},
"node_modules/@sentry/bundler-plugin-core": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.4.0.tgz",
"integrity": "sha512-WTGhgwxzyolzOg0sudULK0rRgLndtsEiBt4QwltKW/WYArMtFyf286aZx19uQ+rD+bSx3Il81SD23nqDOTtnzg==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.0.tgz",
"integrity": "sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.18.5",
"@sentry/babel-plugin-component-annotate": "4.4.0",
"@sentry/cli": "^2.51.0",
"@sentry/babel-plugin-component-annotate": "4.6.0",
"@sentry/cli": "^2.57.0",
"dotenv": "^16.3.1",
"find-up": "^5.0.0",
"glob": "^9.3.2",
@@ -4441,9 +4452,9 @@
}
},
"node_modules/@sentry/cli": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.56.1.tgz",
"integrity": "sha512-VDAIg+gmjNtJS5VUZQMDSK9RaKC9hYQi3PoXpNa+owNfQNk60bCi8z8jkbWRcKbNGn3V51WqvrQAqLoNAdPc9w==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.57.0.tgz",
"integrity": "sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4460,20 +4471,20 @@
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.56.1",
"@sentry/cli-linux-arm": "2.56.1",
"@sentry/cli-linux-arm64": "2.56.1",
"@sentry/cli-linux-i686": "2.56.1",
"@sentry/cli-linux-x64": "2.56.1",
"@sentry/cli-win32-arm64": "2.56.1",
"@sentry/cli-win32-i686": "2.56.1",
"@sentry/cli-win32-x64": "2.56.1"
"@sentry/cli-darwin": "2.57.0",
"@sentry/cli-linux-arm": "2.57.0",
"@sentry/cli-linux-arm64": "2.57.0",
"@sentry/cli-linux-i686": "2.57.0",
"@sentry/cli-linux-x64": "2.57.0",
"@sentry/cli-win32-arm64": "2.57.0",
"@sentry/cli-win32-i686": "2.57.0",
"@sentry/cli-win32-x64": "2.57.0"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.56.1.tgz",
"integrity": "sha512-zfhT8MrvB5x/xRdIVGwg+sG0Cx3i0G6RH2zCrdQ/moWn8TfkwsM0O1k/AxpwbpcRfAHCkVb04CU/yKciKwg2KA==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.57.0.tgz",
"integrity": "sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==",
"license": "BSD-3-Clause",
"optional": true,
"os": [
@@ -4484,9 +4495,9 @@
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.56.1.tgz",
"integrity": "sha512-fNB/Ng11HrkGOSEIDg+fc3zfTCV7q6kJddp6ndK3QlYFsCffRSnclaX1SMp+mqxdWkHqe1kkp85OY8G/x5uAWw==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.57.0.tgz",
"integrity": "sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==",
"cpu": [
"arm"
],
@@ -4502,9 +4513,9 @@
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.56.1.tgz",
"integrity": "sha512-AypXIwZvOMJb9RgjI/98hTAd06FcOjqjIm6G9IR0OI4pJCOcaAXz9NKXdJqxpZd7phSMJnD+Bx/8iYOUPeY73A==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.57.0.tgz",
"integrity": "sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==",
"cpu": [
"arm64"
],
@@ -4520,9 +4531,9 @@
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.56.1.tgz",
"integrity": "sha512-vnH+WJEsUq7Lf7xc9udzE/M4hoDXXsniFFYr/7BvdnXtCQlNNaWFMXHbEDYAql3baIlHkWoG8cEHWuB/YKyniw==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.57.0.tgz",
"integrity": "sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==",
"cpu": [
"x86",
"ia32"
@@ -4539,9 +4550,9 @@
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.56.1.tgz",
"integrity": "sha512-3/BlKe5Vdnia36MeovghHJD8lbcum5TFIxLp+PSfH2sVb09+5Jo0L95oRTI2JkD8Fs+QNssvTqTxJj5eIo/n+A==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.57.0.tgz",
"integrity": "sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==",
"cpu": [
"x64"
],
@@ -4557,9 +4568,9 @@
}
},
"node_modules/@sentry/cli-win32-arm64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.56.1.tgz",
"integrity": "sha512-Gg8RV7CV7Tz4fiR1EN1Af5AVhJsnEXiZvfvfQXI4lp51MKAhcxZIMtEfg9HaWsn3Dm/wgwYBinyeywfWbTXYDg==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.57.0.tgz",
"integrity": "sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==",
"cpu": [
"arm64"
],
@@ -4573,9 +4584,9 @@
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.56.1.tgz",
"integrity": "sha512-6u6a060yC3i76Ze1apqgWr5luQSyhuD5ND84eWfh/UbddsEa42UHjoVHOiBwmpZqf/hvNZAtzLnE4NCvU4zOMg==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.57.0.tgz",
"integrity": "sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==",
"cpu": [
"x86",
"ia32"
@@ -4590,9 +4601,9 @@
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.56.1.tgz",
"integrity": "sha512-11cdflajBrDWlRZqI9MOu7ok2vnPzFjKmbU3YvBYWQapNE+HHAsWdsRL/u/P1RmU62vj7Y42iSUcj6x1SNrdPw==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.57.0.tgz",
"integrity": "sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==",
"cpu": [
"x64"
],
@@ -4653,12 +4664,12 @@
}
},
"node_modules/@sentry/vite-plugin": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.4.0.tgz",
"integrity": "sha512-sOq1xJj5URIa/c4fSJomjOjp7l0ljk4WWRjol6ERwJ5wntOKDrw5Y7T1ZbyiDGD8/ndzQnn4Od03Z+jSvpqwog==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.6.0.tgz",
"integrity": "sha512-fMR2d+EHwbzBa0S1fp45SNUTProxmyFBp+DeBWWQOSP9IU6AH6ea2rqrpMAnp/skkcdW4z4LSRrOEpMZ5rWXLw==",
"license": "MIT",
"dependencies": {
"@sentry/bundler-plugin-core": "4.4.0",
"@sentry/bundler-plugin-core": "4.6.0",
"unplugin": "1.0.1"
},
"engines": {
@@ -4666,13 +4677,13 @@
}
},
"node_modules/@sentry/webpack-plugin": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.4.0.tgz",
"integrity": "sha512-s9Js4v++pbZaKu6ddG1LSXbSKfM71UxkS6PzmOWj4HyTHdiZr+469tbdanTJwz8XO87neFAP1mteuo1Cur3iHg==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.6.0.tgz",
"integrity": "sha512-i9Yy2kXCbFKlRST09fV1HsI0naJAfeXxoiUPyh5iCgSo2w7ZwEUlk0tJhupnHZzfSa3OSg01+vVNeeyLYM4tdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sentry/bundler-plugin-core": "4.4.0",
"@sentry/bundler-plugin-core": "4.6.0",
"unplugin": "1.0.1",
"uuid": "^9.0.0"
},
@@ -5481,9 +5492,9 @@
}
},
"node_modules/antd": {
"version": "5.27.5",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.27.5.tgz",
"integrity": "sha512-Ehd9mqtHvJ1clon1yJ/1BTV6eX/3SH2YXZZPTHUk8XdzXFwUioI+Lht47s+MaHIUBY77RnZrmtKwwR+VVu0l7A==",
"version": "5.27.6",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.27.6.tgz",
"integrity": "sha512-70HrjVbzDXvtiUQ5MP1XdNudr/wGAk9Ivaemk6f36yrAeJurJSmZ8KngOIilolLRHdGuNc6/Vk+4T1OZpSjpag==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.2.1",
@@ -5896,9 +5907,9 @@
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -6027,9 +6038,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz",
"integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==",
"version": "2.8.21",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz",
"integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -6272,9 +6283,9 @@
}
},
"node_modules/browserslist": {
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
"funding": [
{
"type": "opencollective",
@@ -6291,11 +6302,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
"electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
"electron-to-chromium": "^1.5.238",
"node-releases": "^2.0.26",
"update-browserslist-db": "^1.1.4"
},
"bin": {
"browserslist": "cli.js"
@@ -6472,9 +6483,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001748",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
"integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
"version": "1.0.30001751",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"funding": [
{
"type": "opencollective",
@@ -7246,12 +7257,12 @@
"license": "MIT"
},
"node_modules/dayjs-business-days2": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.0.tgz",
"integrity": "sha512-OgDBnsNmlk9+vmRQaP4yFisXs29WDk0ItUUctIagmO6OIoxhf4vArTov5i+G4vjT9Sz8NXOLMLrOVP0X0lG/Hw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.1.tgz",
"integrity": "sha512-zn5Athg9GGjuW7MJGz9qZKgpf1HzkK/jvHnUaR1P1TvDBmQ5Dc3auRbQJxkOcgmuK0XM+QG/Nxxe10IMHjw+4w==",
"license": "MIT",
"dependencies": {
"dayjs": "^1.11.13"
"dayjs": "^1.11.18"
}
},
"node_modules/debug": {
@@ -7605,9 +7616,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.231",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.231.tgz",
"integrity": "sha512-cyl6vqZGkEBnz/PmvFHn/u9G/hbo+FF2CNAOXriG87QOeLsUdifCZ9UbHNscE9wGdrC8XstNMli0CbQnZQ+fkA==",
"version": "1.5.243",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.243.tgz",
"integrity": "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==",
"license": "ISC"
},
"node_modules/elliptic": {
@@ -10350,9 +10361,9 @@
}
},
"node_modules/libphonenumber-js": {
"version": "1.12.24",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.24.tgz",
"integrity": "sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==",
"version": "1.12.25",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.25.tgz",
"integrity": "sha512-u90tUu/SEF8b+RaDKCoW7ZNFDakyBtFlX1ex3J+VH+ElWes/UaitJLt/w4jGu8uAE41lltV/s+kMVtywcMEg7g==",
"license": "MIT"
},
"node_modules/lightningcss": {
@@ -10768,9 +10779,9 @@
}
},
"node_modules/memfs": {
"version": "4.49.0",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.49.0.tgz",
"integrity": "sha512-L9uC9vGuc4xFybbdOpRLoOAOq1YEBBsocCs5NVW32DfU+CZWWIn3OVF+lB8Gp4ttBVSMazwrTrjv8ussX/e3VQ==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.50.0.tgz",
"integrity": "sha512-N0LUYQMUA1yS5tJKmMtU9yprPm6ZIg24yr/OVv/7t6q0kKDIho4cBbXRi1XKttUmNYDYgF/q45qrKE/UhGO0CA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -11529,9 +11540,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.23",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
"integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
"license": "MIT"
},
"node_modules/node-stdlib-browser": {
@@ -12333,28 +12344,16 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.276.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.276.0.tgz",
"integrity": "sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw==",
"version": "1.281.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.281.0.tgz",
"integrity": "sha512-t3sAlgVozpU1W1ppiF5zLG6eBRPUs0hmtxN8R1V7P0qZFmnECshAAk2cBxCsxEanadT3iUpS8Z7crBytATqWQQ==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@posthog/core": "1.3.0",
"@posthog/core": "1.4.0",
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.4"
},
"peerDependencies": {
"@rrweb/types": "2.0.0-alpha.17",
"rrweb-snapshot": "2.0.0-alpha.17"
},
"peerDependenciesMeta": {
"@rrweb/types": {
"optional": true
},
"rrweb-snapshot": {
"optional": true
}
}
},
"node_modules/posthog-js/node_modules/core-js": {
@@ -13839,12 +13838,12 @@
}
},
"node_modules/redux-saga": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz",
"integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz",
"integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==",
"license": "MIT",
"dependencies": {
"@redux-saga/core": "^1.3.0"
"@redux-saga/core": "^1.4.2"
}
},
"node_modules/redux-state-sync": {
@@ -16172,9 +16171,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"funding": [
{
"type": "opencollective",
@@ -16356,9 +16355,9 @@
}
},
"node_modules/vite": {
"version": "7.1.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -8,7 +8,7 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@amplitude/analytics-browser": "^2.27.0",
"@amplitude/analytics-browser": "^2.29.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^3.13.9",
"@emotion/is-prop-valid": "^1.4.0",
@@ -19,21 +19,21 @@
"@firebase/firestore": "^4.9.2",
"@firebase/messaging": "^0.12.22",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.9.1",
"@sentry/cli": "^2.56.1",
"@reduxjs/toolkit": "^2.9.2",
"@sentry/cli": "^2.57.0",
"@sentry/react": "^9.43.0",
"@sentry/vite-plugin": "^4.4.0",
"@sentry/vite-plugin": "^4.6.0",
"@splitsoftware/splitio-react": "^2.5.0",
"@tanem/react-nprogress": "^5.0.56",
"antd": "^5.27.5",
"antd": "^5.27.6",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.4.0",
"autosize": "^6.0.1",
"axios": "^1.12.2",
"axios": "^1.13.1",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.18",
"dayjs-business-days2": "^1.3.0",
"dayjs-business-days2": "^1.3.1",
"dinero.js": "^1.9.1",
"dotenv": "^17.2.3",
"env-cmd": "^10.1.0",
@@ -42,7 +42,7 @@
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.24",
"libphonenumber-js": "^1.12.25",
"lightningcss": "^1.30.2",
"logrocket": "^9.0.2",
"markerjs2": "^2.32.7",
@@ -50,7 +50,7 @@
"normalize-url": "^8.1.0",
"object-hash": "^3.0.0",
"phone": "^3.1.67",
"posthog-js": "^1.276.0",
"posthog-js": "^1.281.0",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
@@ -78,7 +78,7 @@
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-saga": "^1.4.2",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.93.2",
@@ -135,31 +135,31 @@
"devDependencies": {
"@ant-design/icons": "^6.1.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.27.1",
"@babel/preset-react": "^7.28.5",
"@dotenvx/dotenvx": "^1.51.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.38.0",
"@playwright/test": "^1.56.1",
"@sentry/webpack-plugin": "^4.4.0",
"@sentry/webpack-plugin": "^4.6.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.6.0",
"browserslist": "^4.26.3",
"browserslist": "^4.27.0",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.6.2",
"eslint": "^9.38.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"memfs": "^4.49.0",
"memfs": "^4.50.0",
"os-browserify": "^0.3.0",
"playwright": "^1.56.1",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^7.1.10",
"vite": "^7.1.12",
"vite-plugin-babel": "^1.3.2",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.24.0",

1349
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,30 +18,30 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.913.0",
"@aws-sdk/client-elasticache": "^3.913.0",
"@aws-sdk/client-s3": "^3.913.0",
"@aws-sdk/client-secrets-manager": "^3.913.0",
"@aws-sdk/client-ses": "^3.913.0",
"@aws-sdk/credential-provider-node": "^3.913.0",
"@aws-sdk/lib-storage": "^3.913.0",
"@aws-sdk/s3-request-presigner": "^3.913.0",
"@aws-sdk/client-cloudwatch-logs": "^3.919.0",
"@aws-sdk/client-elasticache": "^3.919.0",
"@aws-sdk/client-s3": "^3.919.0",
"@aws-sdk/client-secrets-manager": "^3.919.0",
"@aws-sdk/client-ses": "^3.919.0",
"@aws-sdk/credential-provider-node": "^3.919.0",
"@aws-sdk/lib-storage": "^3.919.0",
"@aws-sdk/s3-request-presigner": "^3.919.0",
"@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.12.2",
"axios": "^1.13.1",
"axios-curlirize": "^2.0.0",
"better-queue": "^3.8.12",
"bullmq": "^5.61.0",
"bullmq": "^5.62.0",
"chart.js": "^4.5.1",
"cloudinary": "^2.7.0",
"cloudinary": "^2.8.0",
"compression": "^1.8.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"crisp-status-reporter": "^1.2.2",
"dd-trace": "^5.72.0",
"dd-trace": "^5.74.0",
"dinero.js": "^1.9.1",
"dotenv": "^17.2.3",
"express": "^4.21.1",
@@ -50,8 +50,8 @@
"graphql": "^16.11.0",
"graphql-request": "^6.1.0",
"intuit-oauth": "^4.2.1",
"ioredis": "^5.8.1",
"json-2-csv": "^5.5.9",
"ioredis": "^5.8.2",
"json-2-csv": "^5.5.10",
"jsonwebtoken": "^9.0.2",
"juice": "^11.0.3",
"lodash": "^4.17.21",
@@ -66,11 +66,11 @@
"recursive-diff": "^1.0.9",
"rimraf": "^6.0.1",
"skia-canvas": "^3.0.8",
"soap": "^1.5.0",
"soap": "^1.6.0",
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^11.0.0",
"twilio": "^5.10.3",
"twilio": "^5.10.4",
"uuid": "^11.1.0",
"winston": "^3.18.3",
"winston-cloudwatch": "^6.3.0",

View File

@@ -123,7 +123,7 @@ const applyRoutes = ({ app }) => {
app.use("/payroll", require("./server/routes/payrollRoutes"));
app.use("/sso", require("./server/routes/ssoRoutes"));
app.use("/integrations", require("./server/routes/intergrationRoutes"));
app.use("/rr", require("./server/rr/rrRoutes"));
app.use("/rr", require("./server/rr"));
// Default route for forbidden access
app.get("/", (req, res) => {

1
server/rr/index.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require("./rrRoutes");

1
server/rr/lib/index.cjs Normal file

File diff suppressed because one or more lines are too long

1
server/rr/lib/index.mjs Normal file

File diff suppressed because one or more lines are too long

3
server/rr/lib/types.cjs Normal file
View File

@@ -0,0 +1,3 @@
'use strict';
// CJS access point for JSDoc typedefs
module.exports = require('./types.js');

144
server/rr/lib/types.js Normal file
View File

@@ -0,0 +1,144 @@
/**
* @typedef {Object} RRClientConfig
* @property {string} baseUrl Base URL of the Rome endpoint
* @property {string} username WS-Security username
* @property {string} password WS-Security password
* @property {'Text'|'Digest'} [wssePasswordType] Password type (defaults to Text)
* @property {number} [timeoutMs] Request timeout in milliseconds
* @property {{max?: number}} [retries] Retry configuration
* @property {{ info:Function, warn:Function, error:Function, debug:Function }} [logger] Custom logger implementation
*/
/** Dealer/store routing (required each call). */
export const _routingDoc = null;
/**
* @typedef {Object} Routing
* @property {string} dealerNumber
* @property {string} [storeNumber]
* @property {string} [areaNumber]
*/
/** Optional envelope overrides; BODId/CreationDateTime auto-generated if omitted. */
export const _envelopeDoc = null;
/**
* @typedef {Object} EnvelopeOptions
* @property {string} [bodId]
* @property {string|Date} [creationDateTime]
* @property {{ component?: string, task?: string, referenceId?: string }} [sender]
*/
/** Per-call options */
export const _callOptionsDoc = null;
/**
* @typedef {Object} CallOptions
* @property {Routing} routing
* @property {EnvelopeOptions} [envelope]
*/
/** @template T @typedef {Object} RRResult { @property {boolean} success @property {T} [data] @property {any} parsed @property {{request:string,response:string}} xml @property {{transaction?:any,roRecord?:any}} [statusBlocks] @property {any} [applicationArea] } */
/** @typedef {Object} CombinedSearchName { @property {string} [fname] @property {string} [lname] @property {string} [mname] @property {string} [name] } */
/** @typedef {Object} CombinedSearchQuery
* @property {'phone'|'license'|'vin'|'name'|'nameRecId'|'stkNo'} kind Search kind (only one criterion allowed)
* @property {string|number|{phone:string}} [phone] Phone number criterion
* @property {string|number|{license:string}} [license] License plate criterion
* @property {string|number|{vin:string}} [vin] VIN criterion
* @property {CombinedSearchName} [name] Customer name criterion
* @property {string|number|{custId:string}|{nameRecId:string}} [nameRecId] Name record ID criterion
* @property {string|number|{stkNo:string}} [stkNo] Stock number criterion
* @property {number} [maxResults] Max results (capped at 50)
* @property {string} [make] Vehicle make filter
* @property {string|number} [model] Vehicle model filter
* @property {string|number} [year] Vehicle year filter
} */
/** @typedef {Object} CombinedSearchVehicleDetail { @property {string} [LicNo] } */
/** @typedef {Object} CombinedSearchVehicle { @property {string} [Vin] @property {string} [VehicleMake] @property {string|number} [VehicleYr] @property {string} [MdlNo] @property {string} [ModelDesc] @property {string} [Carline] @property {string} [ExtClrDesc] @property {string} [IntClrDesc] @property {string} [MakeName] @property {CombinedSearchVehicleDetail} [VehicleDetail] } */
/** @typedef {Object} CombinedSearchVehicleWarranty { @property {string} [ContractNumber] @property {string} [ExpirationDate] @property {string|number} [ExpirationMileage] } */
/** @typedef {Object} CombinedSearchAdvisorContactInfo { @property {string|number} [NameRecId] } */
/** @typedef {Object} CombinedSearchAdvisor { @property {CombinedSearchAdvisorContactInfo} [ContactInfo] } */
/** @typedef {Object} CombinedSearchVehicleServInfo { @property {string|number} [CustomerNo] @property {string|number} [SalesmanNo] @property {string|number} [InServiceDate] @property {string|number} [Mileage] @property {string} [TeamCode] @property {CombinedSearchVehicleWarranty} [VehExtWarranty] @property {CombinedSearchAdvisor} [Advisor] @property {string[]} [VehServComments] } */
/** @typedef {Object} CombinedSearchServVehicle { @property {CombinedSearchVehicle} [Vehicle] @property {CombinedSearchVehicleServInfo} [VehicleServInfo] } */
/** @typedef {Object} CombinedSearchNameId { @property {string|number} [NameRecId] @property {'I'|'B'} [IBFlag] @property {Object} [IndName] @property {Object} [BusName] } */
/** @typedef {Object} CombinedSearchNameContactId { @property {CombinedSearchNameId} [NameId] @property {Object[]} [Address] @property {Object[]} [ContactOptions] @property {Object[]} [Phone] @property {Object[]} [Email] } */
/** @typedef {Object} CombinedSearchMessage { @property {string|number} [MessageNo] @property {string} [Text] } */
/** @typedef {Object} CombinedSearchBlock { @property {CombinedSearchNameContactId} [NameContactId] @property {CombinedSearchServVehicle[]} [ServVehicle] @property {CombinedSearchMessage[]} [Message] } */
/** @typedef {Object} CustomerAddress { @property {'P'|'B'|'M'|'S'|'D'} [type] @property {string} line1 @property {string} [line2] @property {string} [city] @property {string} [state] @property {string} [postalCode] @property {string} [county] @property {string} [country] } */
/** @typedef {Object} CustomerPhone { @property {'H'|'W'|'M'|'C'|'F'} [type] @property {string|number} number @property {string|number} [extension] } */
/** @typedef {Object} CustomerEmail { @property {string} address } */
/** @typedef {Object} CustomerBirthDate { @property {'P'|'S'} [type] @property {string} date } */
/** @typedef {Object} CustomerSSN { @property {'P'|'S'} [type] @property {string} ssn } */
/** @typedef {Object} CustomerDriver { @property {'P'|'S'} [type] @property {string} licenseNumber @property {string} [licenseState] @property {string} [licenseExpDate] } */
/** @typedef {Object} CustomerChild { @property {string} name } */
/** @typedef {Object} CustomerPersonal { @property {'M'|'F'|'U'} [gender] @property {string} [otherName] @property {string} [anniversaryDate] @property {string} [employerName] @property {string} [employerPhone] @property {string} [occupation] @property {string} [optOut] @property {string} [optOutUse] @property {CustomerBirthDate[]} [birthDates] @property {CustomerSSN[]} [ssns] @property {CustomerDriver} [driver] @property {CustomerChild[]} [children] } */
/** @typedef {Object} CustomerDmsFollowup { @property {string} type @property {string} value } */
/** @typedef {Object} CustomerDmsInfo { @property {string} [taxExemptNum] @property {string} [salesTerritory] @property {string} [deliveryRoute] @property {string} [salesmanNum] @property {string} [lastContactMethod] @property {CustomerDmsFollowup[]} [followups] } */
/** @typedef {Object} InsertCustomerPayload
* @property {'I'|'B'} [ibFlag] Individual/Business flag (auto-inferred if omitted)
* @property {'R'|'W'|'I'|'Retail'|'Wholesale'|'Internal'} [customerType] Customer type
* @property {string} [createdBy] Username or identifier creating the record
* @property {string} [customerName] Business name (alias for lastName when business)
* @property {string} [lastName] Last name or business name
* @property {string} [firstName] First name (required for individuals)
* @property {string} [midName] Middle name
* @property {string} [salut] Salutation
* @property {string} [suffix] Suffix
* @property {CustomerAddress[]} [addresses] Addresses list (line1 required per address)
* @property {CustomerPhone[]} [phones] Phone numbers list (number required per phone)
* @property {CustomerEmail[]} [emails] Email list (first entry used)
* @property {CustomerPersonal} [personal] Personal details
* @property {CustomerDmsInfo} [dms] DMS-specific supplemental info
*/
/** @typedef {InsertCustomerPayload & { nameRecId: string|number }} UpdateCustomerPayload */
/** @typedef {Object} CustomerResponseData { @property {string|undefined} dmsRecKey @property {string|undefined} status @property {string|undefined} statusCode } */
/** @typedef {Object} ServiceVehicleDetail { @property {string} [licNo] } */
/** @typedef {Object} ServiceVehicleWarranty { @property {string} [contractNumber] @property {string} [expirationDate] @property {string|number} [expirationMileage] } */
/** @typedef {Object} ServiceVehicleAdvisorContactInfo { @property {string|number} nameRecId } */
/** @typedef {Object} ServiceVehicleServInfo { @property {string|number} customerNo @property {string|number} [salesmanNo] @property {string|number} [inServiceDate] @property {string|number} [mileage] @property {string} [teamCode] @property {ServiceVehicleWarranty} [vehExtWarranty] @property {{contactInfo?: ServiceVehicleAdvisorContactInfo}} [advisor] } */
/** @typedef {Object} InsertServiceVehiclePayload { @property {string} vin @property {string} [modelDesc] @property {string} [carline] @property {string} [extClrDesc] @property {string} [intClrDesc] @property {string} [trimDesc] @property {string} [bodyStyle] @property {string} [engineDesc] @property {string} [transDesc] @property {string|number} [year] @property {string|number} [odometer] @property {string} [odometerUnits] @property {ServiceVehicleDetail} [vehicleDetail] @property {ServiceVehicleServInfo} vehicleServInfo } */
/** @typedef {Object} ServiceVehicleResponseData { @property {string|undefined} status @property {string|undefined} statusCode } */
/** @typedef {Object} RepairOrderEstimate { @property {string|number} [parts] @property {string|number} [labor] @property {string|number} [total] @property {string} [estimateType] } */
/** @typedef {Object} RepairOrderTax { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string} [taxCode] @property {string|number} [txblGrossAmt] @property {string|number} [grossTaxAmt] } */
/** @typedef {Object} RepairOrderLaborBill { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string|number} [jobTotalHrs] @property {string|number} [billTime] @property {string|number} [billRate] } */
/** @typedef {Object} RepairOrderLaborCCC { @property {string} [cause] @property {string} [complaint] @property {string} [correction] } */
/** @typedef {Object} RepairOrderLaborAmount { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string} [amtType] @property {string|number} [custPrice] @property {string|number} [totalAmt] } */
/** @typedef {Object} RepairOrderLaborOp { @property {string} [opCode] @property {string|number} [jobNo] @property {'T'|'N'} [custTxblNtxblFlag] @property {'T'|'N'} [warrTxblNtxblFlag] @property {'T'|'N'} [intrTxblNtxblFlag] @property {string} [custPayTypeFlag] @property {string} [warrPayTypeFlag] @property {string} [intrPayTypeFlag] @property {string} [vlrCode] @property {RepairOrderLaborBill} [bill] @property {RepairOrderLaborCCC} [ccc] @property {RepairOrderLaborAmount} [amount] } */
/** @typedef {Object} RepairOrderLabor { @property {RepairOrderLaborOp[]} [ops] } */
/** @typedef {Object} RepairOrderPartLine { @property {string} [partNo] @property {string} [partNoDesc] @property {string|number} [partQty] @property {string|number} [sale] @property {string|number} [cost] @property {string} [addDeleteFlag] } */
/** @typedef {Object} RepairOrderPartJob { @property {string} [opCode] @property {string|number} [jobNo] @property {RepairOrderPartLine[]} [lines] } */
/** @typedef {Object} RepairOrderPart { @property {RepairOrderPartJob[]} [jobs] } */
/** @typedef {Object} RepairOrderGGLineAmount { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string} [amtType] @property {string|number} [custPrice] @property {string|number} [dlrCost] } */
/** @typedef {Object} RepairOrderGGLine { @property {string} [breakOut] @property {'G'|'P'|'S'|'F'} [itemType] @property {string} [itemDesc] @property {string|number} [custQty] @property {string|number} [warrQty] @property {string|number} [intrQty] @property {'T'|'N'} [custTxblNtxblFlag] @property {'T'|'N'} [warrTxblNtxblFlag] @property {'T'|'N'} [intrTxblNtxblFlag] @property {string} [custPayTypeFlag] @property {string} [warrPayTypeFlag] @property {string} [intrPayTypeFlag] @property {RepairOrderGGLineAmount} [amount] } */
/** @typedef {Object} RepairOrderGGOp { @property {string} [opCode] @property {string|number} [jobNo] @property {RepairOrderGGLine[]} [lines] } */
/** @typedef {Object} RepairOrderGG { @property {string|number} [roNo] @property {RepairOrderGGOp[]} [ops] } */
/** @typedef {Object} RepairOrderMiscLine { @property {string} [miscCode] @property {'T'|'N'} [custTxblNtxblFlag] @property {'T'|'N'} [warrTxblNtxblFlag] @property {'T'|'N'} [intrTxblNtxblFlag] @property {string} [custPayTypeFlag] @property {string} [warrPayTypeFlag] @property {string} [intrPayTypeFlag] @property {string|number} [codeAmt] } */
/** @typedef {Object} RepairOrderMiscOp { @property {string} [opCode] @property {string|number} [jobNo] @property {RepairOrderMiscLine[]} [lines] } */
/** @typedef {Object} RepairOrderMisc { @property {string|number} [roNo] @property {RepairOrderMiscOp[]} [ops] } */
/** @typedef {Object} CreateRepairOrderPayload
* @property {string|number} customerNo Customer number (CustNo)
* @property {string|number} departmentType Department type (DeptType)
* @property {string} vin Vehicle VIN (Vin)
* @property {string|number} outsdRoNo External RO identifier
* @property {string|number} [advisorNo] Advisor number
* @property {string|number} [tagNo] Tag number
* @property {string|number} [mileageIn] Mileage in value
* @property {string} [roComment] Repair order comment
* @property {RepairOrderEstimate} [estimate] Repair order estimate block
* @property {RepairOrderTax} [tax] Tax block
* @property {RepairOrderLabor} [rolabor] Labor operations
* @property {RepairOrderPart} [ropart] Parts jobs/lines
* @property {RepairOrderGG} [rogg] General goods lines
* @property {RepairOrderMisc} [romisc] Miscellaneous charges
*/
/** @typedef {Object} UpdateRepairOrderPayload { @property {'Y'|'N'} finalUpdate @property {string|number} outsdRoNo @property {string|number} [roNo] @property {string|number} [customerNo] @property {string|number} [tagNo] @property {string|number} [departmentType] @property {string} [vin] @property {string|number} [mileageIn] @property {string|number} [mileageOut] @property {string} [roComment] @property {RepairOrderEstimate} [estimate] @property {RepairOrderTax} [tax] @property {RepairOrderLabor} [rolabor] @property {RepairOrderPart} [ropart] @property {RepairOrderGG} [rogg] @property {RepairOrderMisc} [romisc] } */
/** @typedef {Object} RepairOrderData { @property {string|undefined} status @property {string|undefined} date @property {string|undefined} time @property {string|undefined} outsdRoNo @property {string|undefined} dmsRoNo @property {string|undefined} errorMessage } */
/** @typedef {Object} GetAdvisorsParams { @property {'S'|'P'|'B'|'SERVICE'|'PARTS'|'BODY'|'BODYSHOP'|'BODY SHOP'} department @property {string|number} [advisorNumber] @property {number} [maxResults] } */
/** @typedef {Object} AdvisorRow { @property {string|number|undefined} advisorId @property {string|undefined} firstName @property {string|undefined} lastName @property {'S'|'P'|'B'|undefined} department } */
/** @typedef {Object} GetPartsParams { @property {string|number} roNumber } */
/** @typedef {Object} PartRow { @property {string|undefined} partNumber @property {string|undefined} partDescription @property {string|number|undefined} quantityOrdered @property {string|number|undefined} quantityShipped @property {string|number|undefined} price @property {string|number|undefined} cost @property {string|undefined} processedFlag @property {string|undefined} addOrDelete } */
// Marker exports (no runtime impact)
export const _extendedTypesDoc = null;

1035
server/rr/lib/types/types.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
const { getRRConfigForBodyshop } = require("./rr-config");
const { RrApiError } = require("./rr-error");
/**
* Extracts bodyshopId from body, job, or header and loads RR config.
* @returns {Promise<{ bodyshopId: string, config: any }>}
*/
async function resolveRRConfigHttp(req) {
const body = req?.body || {};
const fromBody = body.bodyshopId;
const fromJob = body.job && (body.job.shopid || body.job.bodyshopId);
const fromHeader = typeof req.get === "function" ? req.get("x-bodyshop-id") : undefined;
const bodyshopId = fromBody || fromJob || fromHeader;
if (!bodyshopId) {
throw new RrApiError(
"Missing bodyshopId (expected in body.bodyshopId, body.job.shopid/bodyshopId, or x-bodyshop-id header)",
"BAD_REQUEST"
);
}
const config = await getRRConfigForBodyshop(bodyshopId);
if (!config?.dealerNumber) {
throw new RrApiError(`RR config not found for bodyshopId=${bodyshopId} (missing dealerNumber)`, "NOT_CONFIGURED");
}
return { bodyshopId, config };
}
module.exports = { resolveRRConfigHttp };

25
server/rr/rr-client.js Normal file
View File

@@ -0,0 +1,25 @@
const { RRClient } = require("./lib/index.cjs");
/**
* Build an RR client using env credentials.
* @param {{logger?: {debug?:Function,info?:Function,warn?:Function,error?:Function}}} opts
*/
function makeRRClient({ logger } = {}) {
const baseUrl = process.env.RR_BASE_URL;
const username = process.env.RR_USERNAME;
const password = process.env.RR_PASSWORD;
if (!baseUrl || !username || !password) {
throw new Error("RR creds missing (RR_BASE_URL, RR_USERNAME, RR_PASSWORD).");
}
return new RRClient({
baseUrl,
username,
password,
logger
// retries: { max: 3 }, // optional: override retry policy
});
}
module.exports = { makeRRClient };

View File

@@ -1,81 +1,63 @@
// server/rr/rr-config.js
/**
* Loads per-bodyshop RR routing from Hasura.
* Expected table fields (adapt if your schema differs):
* - bodyshops.id = $bodyshopId
* - bodyshops.rr_dealerid (string)
* - bodyshops.rr_configuration JSON { storeNumber?, branchNumber? }
*
* Requires env:
* HASURA_GRAPHQL_ENDPOINT, HASURA_ADMIN_SECRET
*/
const { GraphQLClient, gql } = require("graphql-request");
/**
* Fetch the bodyshop row (dealer + per-shop RR json).
* No fallback to env for dealer/store/branch.
*/
async function fetchBodyshopRRRow(bodyshopId) {
if (!bodyshopId) throw new Error("Missing bodyshopId for RR config.");
const HASURA_URL = process.env.HASURA_GRAPHQL_ENDPOINT || process.env.HASURA_URL;
const HASURA_SECRET = process.env.HASURA_ADMIN_SECRET || process.env.HASURA_GRAPHQL_ADMIN_SECRET;
const endpoint = process.env.GRAPHQL_ENDPOINT;
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT env var is required.");
if (!HASURA_URL || !HASURA_SECRET) {
// Warn loudly at startup; you can hard fail if you prefer
console.warn("[RR] HASURA env not set (HASURA_GRAPHQL_ENDPOINT / HASURA_ADMIN_SECRET).");
}
const headers = {};
if (process.env.HASURA_ADMIN_SECRET) {
headers["x-hasura-admin-secret"] = process.env.HASURA_ADMIN_SECRET;
} else if (process.env.GRAPHQL_BEARER) {
headers["authorization"] = `Bearer ${process.env.GRAPHQL_BEARER}`;
}
const client = HASURA_URL
? new GraphQLClient(HASURA_URL, {
headers: { "x-hasura-admin-secret": HASURA_SECRET }
})
: null;
const client = new GraphQLClient(endpoint, { headers });
const Q = gql`
query BodyshopRR($id: uuid!) {
const Q_BODYSHOP_RR = gql`
query RR_Config($id: uuid!) {
bodyshops_by_pk(id: $id) {
id
rr_dealerid
rr_configuration
}
}
`;
const { bodyshops_by_pk: bs } = await client.request(Q, { id: bodyshopId });
if (!bs) throw new Error("Bodyshop not found.");
if (!bs.rr_dealerid) throw new Error("Bodyshop is not configured for RR (missing rr_dealerid).");
let cfgJson = bs.rr_configuration || {};
if (typeof cfgJson === "string") {
try {
cfgJson = JSON.parse(cfgJson);
} catch {
cfgJson = {};
}
}
return {
dealerNumber: bs.rr_dealerid,
storeNumber: cfgJson.storeNumber,
branchNumber: cfgJson.branchNumber
};
}
`;
/**
* Build the full RR config object used by downstream code.
* - Dealer/Store/Branch: from DB (required)
* - Transport/BaseURL/creds/PPSysId: from env (deployment-wide)
* @param {string} bodyshopId
* @returns {Promise<{dealerNumber:string, storeNumber?:string, areaNumber?:string}>}
*/
async function getRRConfigForBodyshop(bodyshopId) {
const { dealerNumber, storeNumber, branchNumber } = await fetchBodyshopRRRow(bodyshopId);
if (!client) throw new Error("Hasura client not configured.");
const rrTransport = (process.env.RR_TRANSPORT || "STAR").toUpperCase();
return {
// Per-bodyshop (DB)
dealerNumber,
storeNumber,
branchNumber,
const data = await client.request(Q_BODYSHOP_RR, { id: bodyshopId });
const row = data?.bodyshops_by_pk;
if (!row) throw new Error(`Bodyshop not found: ${bodyshopId}`);
// Duplicate snake_case for legacy call-sites that expect it
dealer_number: dealerNumber,
store_number: storeNumber,
branch_number: branchNumber,
const dealerNumber = row.rr_dealerid;
// Deployment-wide (env)
baseUrl: process.env.RR_BASE_URL,
username: process.env.RR_USERNAME,
password: process.env.RR_PASSWORD,
ppsysId: process.env.RR_PPSYSID,
rrTransport
};
const cfg = row.rr_configuration || {};
if (!dealerNumber) {
throw new Error(`RR not configured for bodyshop ${bodyshopId} (missing rr_dealerid).`);
}
// The RR client expects "areaNumber" (Rome "branch")
const storeNumber = cfg.storeNumber || cfg.store_no || cfg.store || null;
const areaNumber = cfg.branchNumber || cfg.branch_no || cfg.branch || null;
return { dealerNumber, storeNumber, areaNumber };
}
module.exports = { getRRConfigForBodyshop };

View File

@@ -1,77 +0,0 @@
/**
* STAR-only constants for Reynolds & Reynolds (Rome/RCI)
* Used by rr-helpers.js to build and send SOAP requests.
*
* IMPORTANT:
* - Only rr-test.js should fall back to ENV for dealer/store/branch.
* - All runtime code (sockets/routes/jobs) must pass per-bodyshop
* values from the database (see rr-config.js#getRRConfigForBodyshop).
*/
exports.RR_NS = Object.freeze({
SOAP_ENV: "http://schemas.xmlsoap.org/soap/envelope/",
SOAP_ENC: "http://schemas.xmlsoap.org/soap/encoding/",
XSD: "http://www.w3.org/2001/XMLSchema",
XSI: "http://www.w3.org/2001/XMLSchema-instance",
WSSE: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
WSU: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
STAR_TRANSPORT: "http://www.starstandards.org/webservices/2005/10/transport",
STAR_BUSINESS: "http://www.starstandards.org/STAR"
});
const RR_STAR_SOAP_ACTION = "http://www.starstandards.org/webservices/2005/10/transport/ProcessMessage";
exports.RR_SOAP_ACTION = RR_STAR_SOAP_ACTION;
const RR_SOAP_HEADERS = {
"Content-Type": "text/xml; charset=utf-8",
SOAPAction: RR_STAR_SOAP_ACTION
};
// Export if other modules need default STAR headers
exports.RR_SOAP_HEADERS = RR_SOAP_HEADERS;
// All STAR-supported actions (mapped to Mustache templates)
exports.RR_ACTIONS = Object.freeze({
CombinedSearch: { template: "CombinedSearch" },
GetAdvisors: { template: "GetAdvisors" },
GetParts: { template: "GetParts" },
InsertCustomer: { template: "InsertCustomer" },
InsertServiceVehicle: { template: "InsertServiceVehicle" },
CreateRepairOrder: { template: "CreateRepairOrder" },
UpdateCustomer: { template: "UpdateCustomer" },
UpdateRepairOrder: { template: "UpdateRepairOrder" }
});
/**
* Base config loader (environment-driven)
*
* ⚠️ Policy:
* - Only rr-test.js should rely on the ENV values for dealer/store/branch.
* - All other call sites must inject per-bodyshop values from DB.
*/
exports.getBaseRRConfig = function getBaseRRConfig() {
return {
baseUrl: process.env.RR_BASE_URL,
username: process.env.RR_USERNAME,
password: process.env.RR_PASSWORD,
ppsysId: process.env.RR_PPSYSID, // optional legacy identifier
// ❗ These are ONLY for rr-test.js fallback.
dealerNumber: process.env.RR_DEALER_NUMBER,
storeNumber: process.env.RR_STORE_NUMBER,
branchNumber: process.env.RR_BRANCH_NUMBER || "01",
wssePasswordType: process.env.RR_WSSE_PASSWORD_TYPE || "Text",
timeout: Number(process.env.RR_TIMEOUT_MS || 30000)
};
};
/**
* Normalize dealer/store/branch field names (camelCase vs snake_case).
* Safe to use in helpers to tolerate mixed callers during migration.
*/
exports.normalizeRRDealerFields = function normalizeRRDealerFields(cfg = {}) {
const dealerNumber = cfg.dealerNumber ?? cfg.dealer_number;
const storeNumber = cfg.storeNumber ?? cfg.store_number;
const branchNumber = cfg.branchNumber ?? cfg.branch_number;
return { dealerNumber, storeNumber, branchNumber };
};

View File

@@ -1,99 +1,17 @@
/**
* @file rr-customer.js
* @description Rome (Reynolds & Reynolds) Customer Insert / Update integration.
* Maps internal customer objects to Rome XML schemas and executes RCI calls.
*/
const { withClient } = require("../rr/withClient");
const { MakeRRCall } = require("./rr-helpers");
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
const RRLogger = require("./rr-logger");
const { RrApiError } = require("./rr-error");
/**
* Insert a new customer into Rome.
* @param {Socket} socket - WebSocket connection for logging context
* @param {Object} customer - Hasura customer record
* @param {Object} bodyshopConfig - DMS configuration
* @returns {Promise<Object>} result
*/
async function insertCustomer(socket, customer, bodyshopConfig) {
const action = "InsertCustomer";
const template = "InsertCustomer";
try {
RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
const data = mapCustomerInsert(customer, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig,
jobid: customer.id
async function insertCustomer({ bodyshopId, payload }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.insertCustomer(payload, { routing });
return res;
});
RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
return {
success: true,
dms: "Rome",
action,
customerId: customer.id,
xml: resultXml
};
} catch (error) {
RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
message: error.message,
stack: error.stack
});
throw new RrApiError(`RR InsertCustomer failed: ${error.message}`, "INSERT_CUSTOMER_ERROR");
}
}
/**
* Update an existing customer in Rome.
* @param {Socket} socket
* @param {Object} customer
* @param {Object} bodyshopConfig
* @returns {Promise<Object>}
*/
async function updateCustomer(socket, customer, bodyshopConfig) {
const action = "UpdateCustomer";
const template = "UpdateCustomer";
try {
RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
const data = mapCustomerUpdate(customer, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig,
jobid: customer.id
async function updateCustomer({ bodyshopId, payload }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.updateCustomer(payload, { routing });
return res;
});
RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
return {
success: true,
dms: "Rome",
action,
customerId: customer.id,
xml: resultXml
};
} catch (error) {
RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
message: error.message,
stack: error.stack
});
throw new RrApiError(`RR UpdateCustomer failed: ${error.message}`, "UPDATE_CUSTOMER_ERROR");
}
}
module.exports = {
insertCustomer,
updateCustomer
};
module.exports = { insertCustomer, updateCustomer };

View File

@@ -1,48 +1,11 @@
/**
* @file rr-error.js
* @description Custom error types for the Reynolds & Reynolds (Rome) integration.
*/
/**
* Base RR API Error class — always structured with a message and a code.
*/
class RrApiError extends Error {
/**
* @param {string} message - Human-readable message
* @param {string} [code="RR_ERROR"] - Short machine-readable error code
* @param {Object} [details] - Optional structured metadata
*/
constructor(message, code = "RR_ERROR", details = {}) {
constructor(message, code, meta) {
super(message);
this.name = "RrApiError";
this.code = code;
this.details = details;
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
details: this.details
};
this.code = code || "RR_API_ERROR";
if (meta) this.meta = meta;
Error.captureStackTrace?.(this, RrApiError);
}
}
/**
* Helper to normalize thrown errors into a consistent RrApiError instance.
*/
function toRrError(err, defaultCode = "RR_ERROR") {
if (!err) return new RrApiError("Unknown RR error", defaultCode);
if (err instanceof RrApiError) return err;
if (typeof err === "string") return new RrApiError(err, defaultCode);
const msg = err.message || "Unspecified RR error";
const code = err.code || defaultCode;
const details = err.details || {};
return new RrApiError(msg, code, details);
}
module.exports = {
RrApiError,
toRrError
};
module.exports = { RrApiError };

View File

@@ -1,351 +0,0 @@
/**
* STAR-only SOAP transport + template rendering for Reynolds & Reynolds (Rome/RCI).
* - Renders Mustache STAR business templates (rey_*Req rooted with STAR ns)
* - Builds STAR SOAP envelope (ProcessMessage/payload/content + ApplicationArea)
* - Posts to RCI endpoint with STAR SOAPAction (full URI)
* - Parses XML response (faults + STAR payload result)
*/
"use strict";
const fs = require("fs/promises");
const path = require("path");
const axios = require("axios");
const mustache = require("mustache");
const { XMLParser } = require("fast-xml-parser");
const RRLogger = require("./rr-logger");
const {
RR_ACTIONS,
RR_SOAP_HEADERS,
RR_SOAP_ACTION,
RR_NS,
getBaseRRConfig,
normalizeRRDealerFields
} = require("./rr-constants");
const { RrApiError } = require("./rr-error");
const xmlFormatter = require("xml-formatter");
/**
* Collapse Mustache-induced whitespace and pretty print.
* - strips inner XML decl
* - removes lines that are only whitespace
* - collapses inter-tag whitespace
* - formats with consistent indentation
*/
function prettyPrintXml(xml) {
let s = xml;
// strip any inner XML declaration
s = s.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
// remove lines that are only whitespace
s = s.replace(/^[\t ]*(?:\r?\n)/gm, "");
// collapse whitespace strictly between tags (not inside text nodes)
s = s.replace(/>\s+</g, "><");
// final pretty print
return xmlFormatter(s, {
indentation: " ",
collapseContent: true, // keep short elements on one line
lineSeparator: "\n",
strictMode: false
});
}
// ---------- Public action map (compat with rr-test.js) ----------
const RRActions = Object.fromEntries(Object.entries(RR_ACTIONS).map(([k]) => [k, { action: k }]));
// ---------- Template cache ----------
const templateCache = new Map();
async function loadTemplate(templateName) {
if (templateCache.has(templateName)) return templateCache.get(templateName);
const filePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
const tpl = await fs.readFile(filePath, "utf8");
templateCache.set(templateName, tpl);
return tpl;
}
async function renderXmlTemplate(templateName, data) {
const tpl = await loadTemplate(templateName);
// Render and strip any XML declaration to keep a single root element for the BOD
const rendered = mustache.render(tpl, { STAR_NS: RR_NS.STAR_BUSINESS, ...(data || {}) });
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
}
/**
* Resolve RR config for STAR transport.
*
* Policy:
* - Base (transport) settings (baseUrl, username, password, ppsysId, wssePasswordType, timeout) come from env.
* - Dealer identifiers (dealer/store/branch) MUST be provided by the caller (DB-driven).
* - We DO NOT fall back to env for dealer/store/branch here. Only rr-test.js is allowed to do that.
*/
async function resolveRRConfig(_socket, bodyshopConfig) {
const baseEnv = getBaseRRConfig();
const { dealerNumber, storeNumber, branchNumber } = normalizeRRDealerFields(bodyshopConfig || {});
if (!dealerNumber || !storeNumber || !branchNumber) {
throw new Error(
"Missing dealer/store/branch in RR config. These must be loaded from the database (no env fallback here)."
);
}
return {
baseUrl: bodyshopConfig?.baseUrl || baseEnv.baseUrl,
username: bodyshopConfig?.username || baseEnv.username,
password: bodyshopConfig?.password || baseEnv.password,
ppsysId: bodyshopConfig?.ppsysId || baseEnv.ppsysId,
wssePasswordType: bodyshopConfig?.wssePasswordType || baseEnv.wssePasswordType || "Text",
timeout: baseEnv.timeout,
// canonical identifiers (DB-driven only)
dealerNumber,
storeNumber,
branchNumber
};
}
// ---------- Response parsing ----------
function parseRRResponse(xml) {
const parser = new XMLParser({
ignoreAttributes: false,
removeNSPrefix: true
});
const doc = parser.parse(xml);
// Envelope/Body
const body =
doc?.Envelope?.Body ||
doc?.["soapenv:Envelope"]?.["soapenv:Body"] ||
doc?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
doc?.["S:Envelope"]?.["S:Body"] ||
doc?.Body ||
doc;
// SOAP Fault?
const fault = body?.Fault || body?.["soap:Fault"];
if (fault) {
return {
success: false,
code: fault.faultcode || "SOAP_FAULT",
message: fault.faultstring || "Unknown SOAP Fault",
raw: xml
};
}
// STAR transport path: ProcessMessage/payload/content
const processMessage = body?.ProcessMessage || body?.["ns0:ProcessMessage"] || body?.["ProcessMessageResponse"];
if (processMessage?.payload?.content) {
const content = processMessage.payload.content;
if (content && typeof content === "object") {
const keys = Object.keys(content).filter((k) => k !== "@_id");
const respKey = keys.find((k) => /Resp$/.test(k)) || (keys[0] === "ApplicationArea" && keys[1]) || keys[0];
const respNode = respKey ? content[respKey] : content;
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || respNode?.StatusCode || "OK";
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || respNode?.StatusMessage || null;
return {
success: ["OK", "Success"].includes(String(resultCode)),
code: resultCode,
message: resultMessage,
raw: xml,
parsed: respNode
};
}
}
// Fallback: first element under Body (just in case)
const keys = body && typeof body === "object" ? Object.keys(body) : [];
const respNode = keys.length ? body[keys[0]] : body;
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || "OK";
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || null;
return {
success: resultCode === "OK" || resultCode === "Success",
code: resultCode,
message: resultMessage,
raw: xml,
parsed: respNode
};
}
// ---------- STAR envelope helpers ----------
function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, Destination }) {
// Strip any inner XML declaration (idempotent)
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
const appArea = `
<ApplicationArea>
<CreationDateTime>${CreationDateTime}</CreationDateTime>
<BODId>${BODId}</BODId>
<Sender>
${Sender?.Component ? `<Component>${Sender.Component}</Component>` : ""}
${Sender?.Task ? `<Task>${Sender.Task}</Task>` : ""}
${Sender?.ReferenceId ? `<ReferenceId>${Sender.ReferenceId}</ReferenceId>` : ""}
</Sender>
<Destination>
<DestinationNameCode>RR</DestinationNameCode>
${Destination?.DealerNumber ? `<DealerNumber>${Destination.DealerNumber}</DealerNumber>` : ""}
${Destination?.StoreNumber ? `<StoreNumber>${Destination.StoreNumber}</StoreNumber>` : ""}
${Destination?.AreaNumber ? `<AreaNumber>${Destination.AreaNumber}</AreaNumber>` : ""}
</Destination>
</ApplicationArea>`.trim();
// Inject right after the opening tag of the root element
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
return xml;
}
const TASK_BY_ACTION = {
CreateRepairOrder: { Task: "BSMRO", ReferenceId: "Insert" },
UpdateRepairOrder: { Task: "BSMRO", ReferenceId: "Update" },
InsertCustomer: { Task: "CU", ReferenceId: "Insert" },
UpdateCustomer: { Task: "CU", ReferenceId: "Update" },
InsertServiceVehicle: { Task: "SV", ReferenceId: "Insert" }
};
async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}, action) {
const now = new Date().toISOString();
// Derive sensible defaults for Sender from action, unless caller provided explicit values
const senderDefaults =
appArea?.Sender ||
(action && TASK_BY_ACTION[action] ? { Component: "Rome", ...TASK_BY_ACTION[action] } : { Component: "Rome" });
const payloadWithAppArea = wrapWithApplicationArea(innerBusinessXml, {
CreationDateTime: appArea.CreationDateTime || now,
BODId: appArea.BODId || `BOD-${Date.now()}`,
Sender: senderDefaults,
Destination: appArea.Destination || {
DealerNumber: creds.dealerNumber,
StoreNumber: String(creds.storeNumber ?? "").padStart(2, "0"),
AreaNumber: String(creds.branchNumber || "01").padStart(2, "0")
}
});
return `<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenc="${RR_NS.SOAP_ENC}" xmlns:soapenv="${RR_NS.SOAP_ENV}" xmlns:xsd="${RR_NS.XSD}" xmlns:xsi="${RR_NS.XSI}">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="${RR_NS.WSSE}">
<wsse:UsernameToken>
<wsse:Username>${creds.username}</wsse:Username>
<wsse:Password>${creds.password}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<ProcessMessage xmlns="${RR_NS.STAR_TRANSPORT}">
<payload xmlns:soap="${RR_NS.SOAP_ENV}" xmlns:xsi="${RR_NS.XSI}" xmlns:xsd="${RR_NS.XSD}" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wsse="${RR_NS.WSSE}" xmlns:wsu="${RR_NS.WSU}" xmlns="${RR_NS.STAR_TRANSPORT}">
<content id="content0">
${payloadWithAppArea}
</content>
</payload>
</ProcessMessage>
</soapenv:Body>
</soapenv:Envelope>`;
}
// ---------- Main transport (STAR only) ----------
async function MakeRRCall({
action,
body,
appArea, // <-- allow explicit ApplicationArea overrides at the top level
socket,
dealerConfig, // required in runtime code; rr-test.js can still pass env-inflated cfg
retries = 1,
jobid
}) {
if (!action || !RR_ACTIONS[action]) {
throw new Error(`Invalid RR action: ${action}`);
}
// Prefer explicit dealerConfig from caller; otherwise enforce DB-provided config via resolveRRConfig
const cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
const baseUrl = cfg.baseUrl;
if (!baseUrl) throw new Error("Missing RR base URL");
// Render STAR business body
const templateName = body?.template || action;
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
// Build STAR envelope (use explicit appArea if provided; else accept body.appArea; else derive from action)
const selectedAppArea = appArea || body?.appArea || {};
const envelope = await buildStarEnvelope(renderedBusiness, cfg, selectedAppArea, action);
const formattedEnvelope = prettyPrintXml(envelope);
// Guardrails
if (!formattedEnvelope.includes("<ProcessMessage") || !formattedEnvelope.includes("<ApplicationArea>")) {
throw new Error("STAR envelope malformed: missing ProcessMessage/ApplicationArea");
}
const headers = { ...RR_SOAP_HEADERS, SOAPAction: RR_SOAP_ACTION };
RRLogger(socket, "debug", `Sending RR SOAP request`, {
action,
soapAction: RR_SOAP_ACTION,
endpoint: baseUrl,
jobid,
mode: "STAR"
});
try {
const { data: responseXml } = await axios.post(baseUrl, formattedEnvelope, {
headers,
timeout: cfg.timeout
// Some RCI tenants require Basic in addition to WSSE
// auth: { username: cfg.username, password: cfg.password }
});
const parsed = parseRRResponse(responseXml);
if (!parsed.success) {
RRLogger(socket, "error", `RR ${action} failed`, {
code: parsed.code,
message: parsed.message
});
throw new RrApiError(parsed.message || `RR ${action} failed`, parsed.code || "RR_ERROR");
}
RRLogger(socket, "info", `RR ${action} success`, {
result: parsed.code,
message: parsed.message
});
return responseXml;
} catch (err) {
if (retries > 0) {
RRLogger(socket, "warn", `Retrying RR ${action} (${retries - 1} left)`, {
error: err.message
});
return MakeRRCall({
action,
body,
appArea: selectedAppArea,
socket,
dealerConfig: cfg,
retries: retries - 1,
jobid
});
}
RRLogger(socket, "error", `RR ${action} failed permanently`, { error: err.message });
throw err;
}
}
module.exports = {
MakeRRCall,
renderXmlTemplate,
resolveRRConfig,
parseRRResponse,
buildStarEnvelope,
RRActions
};

View File

@@ -1,158 +1,68 @@
/**
* @file rr-job-export.js
* @description End-to-end export of a Hasura "job" to Reynolds & Reynolds (Rome).
* Orchestrates Customer (insert/update), optional Vehicle insert, and RO (create/update),
* mirroring behavior of PBS/Fortellis exporters for parity.
*/
const { withClient } = require("./withClient");
const RRLogger = require("./rr-logger");
const { RrApiError } = require("./rr-error");
async function exportJobToRR({ bodyshopId, job, logger }) {
return withClient(bodyshopId, logger, async (client, routing) => {
// 1) Upsert Customer
const custPayload = mapJobToCustomer(job);
const custRes = job.customer?.nameRecId
? await client.updateCustomer(custPayload, { routing })
: await client.insertCustomer(custPayload, { routing });
const customerApi = require("./rr-customer");
const roApi = require("./rr-repair-orders");
const { MakeRRCall } = require("./rr-helpers"); // for optional vehicle insert
const { mapServiceVehicle } = require("./rr-mappers");
const customerNo = custRes?.data?.dmsRecKey || job.customer?.customerNo;
if (!customerNo) throw new Error("Failed to resolve customerNo from RR response.");
/**
* Decide if we should CREATE or UPDATE an entity in Rome based on external IDs
*/
function decideAction({ customer, vehicle, job }) {
const hasCustId = !!(customer?.external_id || customer?.rr_customer_id);
const hasVehId = !!(vehicle?.external_id || vehicle?.rr_vehicle_id);
const hasRoId = !!(job?.external_id || job?.rr_repair_order_id || job?.dms_repair_order_id);
// 2) Ensure Service Vehicle (optional, if VIN present)
if (job?.vehicle?.vin) {
await client.insertServiceVehicle(
{
vin: job.vehicle.vin,
vehicleServInfo: { customerNo }
},
{ routing }
);
}
// 3) Create RO
const roHeader = {
customerNo,
departmentType: "B",
vin: job?.vehicle?.vin,
outsdRoNo: job?.roExternal || job?.id,
advisorNo: job?.advisorNo,
mileageIn: job?.mileageIn
};
const roBody = mapJobToRO(job); // extend if you want lines/tax/etc
const roRes = await client.createRepairOrder({ ...roHeader, ...roBody }, { routing });
return roRes?.data;
});
}
function mapJobToCustomer(job) {
const c = job?.customer || {};
return {
customerAction: hasCustId ? "update" : "insert",
vehicleAction: hasVehId ? "skip" : "insert", // Rome often generates vehicle IDs on RO create; we insert only if we have enough data and no id
repairOrderAction: hasRoId ? "update" : "create"
nameRecId: c.nameRecId,
firstName: c.firstName || c.given_name,
lastName: c.lastName || c.family_name,
phone: c.phone || c.mobile,
email: c.email,
address: {
line1: c.address1,
line2: c.address2,
city: c.city,
state: c.province || c.state,
postalCode: c.postal || c.zip
}
};
}
/**
* Normalize a stage result to a consistent structure.
*/
function stageOk(name, extra = {}) {
return { stage: name, success: true, ...extra };
}
function stageFail(name, error) {
return { stage: name, success: false, error: error?.message || String(error) };
}
/**
* Export a job into Rome (Customer → Vehicle → RepairOrder).
* @param {Socket} socket - logging context (may be null in batch)
* @param {Object} job - Hasura job object (must include customer, vehicle, lines, totals)
* @param {Object} bodyshopConfig - per-shop RR config (dealer/store/branch + creds)
* @param {Object} options - { insertVehicleIfMissing: boolean }
* @returns {Promise<Object>} normalized result
*/
async function exportJobToRR(socket, job, bodyshopConfig, options = {}) {
const { customer = {}, vehicle = {} } = job || {};
const { insertVehicleIfMissing = true } = options;
const actions = decideAction({ customer, vehicle, job });
const stages = [];
const summary = {
dms: "Rome",
jobid: job?.id,
ro_action: actions.repairOrderAction,
customer_action: actions.customerAction,
vehicle_action: insertVehicleIfMissing ? actions.vehicleAction : "skip"
function mapJobToRO(job) {
return {
// rolabor: [...],
// roparts: [...],
// estimate: {...},
// tax: {...}
};
RRLogger(socket, "info", `RR Export start`, summary);
// ---- 1) Customer ----
try {
if (actions.customerAction === "insert") {
const res = await customerApi.insertCustomer(socket, customer, bodyshopConfig);
stages.push(stageOk("customer.insert"));
summary.customer_xml = res.xml;
} else {
const res = await customerApi.updateCustomer(socket, customer, bodyshopConfig);
stages.push(stageOk("customer.update"));
summary.customer_xml = res.xml;
}
} catch (error) {
stages.push(stageFail(`customer.${actions.customerAction}`, error));
RRLogger(socket, "error", `RR customer ${actions.customerAction} failed`, {
jobid: job?.id,
error: error.message
});
throw new RrApiError(`Customer ${actions.customerAction} failed: ${error.message}`, "RR_CUSTOMER_ERROR");
}
// ---- 2) Vehicle (optional explicit insert) ----
if (insertVehicleIfMissing && actions.vehicleAction === "insert") {
try {
// Only insert when we have at least VIN or plate+state/year
const hasMinimumIdentity = !!(vehicle?.vin || (vehicle?.license_plate && vehicle?.license_state));
if (hasMinimumIdentity) {
const data = mapServiceVehicle(vehicle, customer, bodyshopConfig);
const xml = await MakeRRCall({
action: "InsertServiceVehicle",
body: { template: "InsertServiceVehicle", data },
socket,
dealerConfig: bodyshopConfig,
jobid: job?.id
});
stages.push(stageOk("vehicle.insert"));
summary.vehicle_xml = xml;
} else {
stages.push(stageOk("vehicle.skip", { reason: "insufficient_identity" }));
}
} catch (error) {
stages.push(stageFail("vehicle.insert", error));
RRLogger(socket, "error", `RR vehicle insert failed`, {
jobid: job?.id,
error: error.message
});
// Non-fatal for the overall export — many flows let RO creation create/associate vehicle.
}
} else {
stages.push(stageOk("vehicle.skip", { reason: actions.vehicleAction === "skip" ? "already_has_id" : "disabled" }));
}
// ---- 3) Repair Order ----
try {
let res;
if (actions.repairOrderAction === "create") {
res = await roApi.createRepairOrder(socket, job, bodyshopConfig);
stages.push(stageOk("ro.create"));
} else {
res = await roApi.updateRepairOrder(socket, job, bodyshopConfig);
stages.push(stageOk("ro.update"));
}
summary.ro_xml = res.xml;
} catch (error) {
stages.push(stageFail(`ro.${actions.repairOrderAction}`, error));
RRLogger(socket, "error", `RR RO ${actions.repairOrderAction} failed`, {
jobid: job?.id,
error: error.message
});
throw new RrApiError(`RepairOrder ${actions.repairOrderAction} failed: ${error.message}`, "RR_RO_ERROR");
}
const result = {
success: true,
...summary,
stages
};
RRLogger(socket, "info", `RR Export finished`, {
jobid: job?.id,
result: {
success: result.success,
customer_action: summary.customer_action,
vehicle_action: summary.vehicle_action,
ro_action: summary.ro_action
}
});
return result;
}
module.exports = {
exportJobToRR
};
module.exports = { exportJobToRR };

View File

@@ -1,55 +1,16 @@
/**
* @file rr-logger.js
* @description Structured logger for Reynolds & Reynolds (Rome) integration.
* Mirrors PBS/Fortellis log shape for consistent log parsing.
*/
const logger = require("../utils/logger");
const util = require("util");
const dayjs = require("dayjs");
/**
* @typedef {Object} LogContext
* @property {string} [jobid]
* @property {string} [action]
* @property {string} [stage]
* @property {string} [endpoint]
* @property {Object} [meta]
*/
/**
* Emit a structured log event to console, Socket.IO, or upstream logger.
* @param {Socket|null} socket - Optional socket for WsLogger passthrough
* @param {"info"|"debug"|"warn"|"error"} level
* @param {string} message - Primary log message
* @param {LogContext|any} [context]
*/
function RRLogger(socket, level, message, context = {}) {
const logEvent = {
source: "RR",
level,
timestamp: dayjs().toISOString(),
message,
...context
};
// Console log (stdout/stderr)
const serialized = `[RR] ${logEvent.timestamp} [${level.toUpperCase()}] ${message}`;
if (level === "error" || level === "warn") {
console.error(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
} else {
console.log(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
}
// Optional: forward to WsLogger (if your socket is configured that way)
function RRLogger(socket) {
return function log(level = "info", message = "", ctx = {}) {
// Console
const fn = logger.logger[level] || logger.log;
fn(`[RR] ${new Date().toISOString()} [${level.toUpperCase()}] ${message}`, ctx);
try {
if (socket && typeof socket.emit === "function") {
socket.emit("rr-log-event", logEvent);
} else if (global.WsLogger && typeof global.WsLogger.createLogEvent === "function") {
global.WsLogger.createLogEvent(socket, level.toUpperCase(), message, context.jobid, context);
}
} catch (e) {
console.error("[RRLogger] forwarding error", e.message);
socket?.emit?.("RR:LOG", { level, message, ctx, ts: Date.now() });
} catch {
/* ignore */
}
};
}
module.exports = RRLogger;

View File

@@ -1,136 +1,24 @@
/**
* @file rr-lookup.js
* @description Rome (Reynolds & Reynolds) lookup operations — Advisors, Parts, and CombinedSearch
*/
const { withClient } = require("./withClient");
const { MakeRRCall, parseRRResponse } = require("./rr-helpers");
const { mapAdvisorLookup, mapPartsLookup, mapCombinedSearch } = require("./rr-mappers");
const RRLogger = require("./rr-logger");
const { RrApiError } = require("./rr-error");
/**
* Get a list of service advisors from Rome.
*/
async function getAdvisors(socket, criteria = {}, bodyshopConfig) {
const action = "GetAdvisors";
const template = "GetAdvisors";
try {
RRLogger(socket, "info", `Starting RR ${action} lookup`);
const data = mapAdvisorLookup(criteria, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig
async function getAdvisors({ bodyshopId, ...criteria }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.getAdvisors(criteria, { routing });
return res;
});
const parsed = parseRRResponse(resultXml);
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
const advisors = parsed.parsed?.Advisors?.Advisor || parsed.parsed?.AdvisorList?.Advisor || [];
const advisorList = Array.isArray(advisors) ? advisors : [advisors];
RRLogger(socket, "debug", `${action} lookup returned ${advisorList.length} advisors`);
return { success: true, dms: "Rome", action, advisors: advisorList };
} catch (error) {
RRLogger(socket, "error", `Error in ${action} lookup`, {
message: error.message,
stack: error.stack
});
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_ADVISORS_ERROR");
}
}
/**
* Get parts information from Rome.
*/
async function getParts(socket, criteria = {}, bodyshopConfig) {
const action = "GetParts";
const template = "GetParts";
try {
RRLogger(socket, "info", `Starting RR ${action} lookup`);
const data = mapPartsLookup(criteria, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig
async function getParts({ bodyshopId, ...criteria }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.getParts(criteria, { routing });
return res;
});
const parsed = parseRRResponse(resultXml);
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
const parts = parsed.parsed?.Parts?.Part || parsed.parsed?.PartList?.Part || [];
const partList = Array.isArray(parts) ? parts : [parts];
RRLogger(socket, "debug", `${action} lookup returned ${partList.length} parts`);
return { success: true, dms: "Rome", action, parts: partList };
} catch (error) {
RRLogger(socket, "error", `Error in ${action} lookup`, {
message: error.message,
stack: error.stack
});
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_PARTS_ERROR");
}
}
/**
* Perform a combined customer / vehicle / company search.
* Equivalent to Rome CombinedSearchRq / Resp.
* @param {Socket} socket
* @param {Object} criteria - { VIN, LicensePlate, CustomerName, Phone, Email }
* @param {Object} bodyshopConfig
* @returns {Promise<Object>} { customers, vehicles, companies }
*/
async function combinedSearch(socket, criteria = {}, bodyshopConfig) {
const action = "CombinedSearch";
const template = "CombinedSearch";
try {
RRLogger(socket, "info", `Starting RR ${action} request`);
const data = mapCombinedSearch(criteria, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig
async function combinedSearch({ bodyshopId, ...query }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.combinedSearch(query, { routing });
return res;
});
const parsed = parseRRResponse(resultXml);
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
const customers = parsed.parsed?.Customers?.Customer || [];
const vehicles = parsed.parsed?.Vehicles?.ServiceVehicle || [];
const companies = parsed.parsed?.Companies?.Company || [];
const result = {
customers: Array.isArray(customers) ? customers : [customers],
vehicles: Array.isArray(vehicles) ? vehicles : [vehicles],
companies: Array.isArray(companies) ? companies : [companies]
};
RRLogger(
socket,
"debug",
`${action} returned ${result.customers.length} customers, ${result.vehicles.length} vehicles, ${result.companies.length} companies`
);
return { success: true, dms: "Rome", action, ...result };
} catch (error) {
RRLogger(socket, "error", `Error in ${action}`, {
message: error.message,
stack: error.stack
});
throw new RrApiError(`RR ${action} failed: ${error.message}`, "COMBINED_SEARCH_ERROR");
}
}
module.exports = {
getAdvisors,
getParts,
combinedSearch
};
module.exports = { getAdvisors, getParts, combinedSearch };

View File

@@ -1,428 +0,0 @@
/**
* @file rr-mappers.js
* @description Maps internal ImEX (Hasura) entities into Rome (Reynolds & Reynolds) XML structures.
* Each function returns a plain JS object that matches Mustache templates in xml-templates/.
*/
const dayjs = require("dayjs");
const { normalizeRRDealerFields } = require("./rr-constants");
/**
* Utility: formats date/time to R&Rs preferred format (ISO or yyyy-MM-dd).
*/
const formatDate = (val) => {
if (!val) return undefined;
return dayjs(val).format("YYYY-MM-DD");
};
/**
* Utility: safely pick numeric values and stringify for XML.
*/
const num = (val) => (val != null ? String(val) : undefined);
const toBoolStr = (v) => (v === true ? "true" : v === false ? "false" : undefined);
const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined && v !== null && v !== "");
/**
* Pull canonical Dealer/Store/Branch fields from cfg (tolerate snake_case during migration).
* Enforces DB-provided values upstream (no env fallback here).
*/
function getDSB(cfg) {
const { dealerNumber, storeNumber, branchNumber } = normalizeRRDealerFields(cfg || {});
return { dealerNumber, storeNumber, branchNumber };
}
/**
* Normalize an address-like object to the template's <Address> block.
*/
function mapAddress(addr) {
if (!addr) return undefined;
const out = {
Line1: addr.line1,
Line2: addr.line2,
City: addr.city,
State: addr.state,
PostalCode: addr.postal_code || addr.postalCode,
Country: addr.country
};
return hasAny(out) ? out : undefined;
}
//
// ===================== CUSTOMER =====================
//
/**
* Map internal customer record to Rome CustomerInsertRq.
*/
function mapCustomerInsert(src) {
const name = src.company_name?.trim() || [src.first_name, src.last_name].filter(Boolean).join(" ").trim();
return {
CustomerNumber: src.external_id, // optional
CustomerType: src.type === "BUSINESS" ? "BUSINESS" : "RETAIL",
CustomerName: name,
DisplayName: src.display_name || name,
Language: src.language || "EN",
TaxExempt: src.tax_exempt ? "Y" : "N",
Active: src.active ? "Y" : "N",
Addresses: (src.addresses || []).map((a) => ({
Type: a.type || "P",
Line1: a.line1,
Line2: a.line2,
City: a.city,
State: a.state,
PostalCode: a.postal_code,
Country: a.country
})),
Phones: (src.phones || []).map((p) => ({
Type: p.type || "H",
Number: p.number,
Extension: p.extension,
Preferred: p.preferred ? "Y" : "N"
})),
Emails: (src.emails || []).map((e) => ({
Type: e.type || "W",
Address: e.address,
Preferred: e.preferred ? "Y" : "N"
}))
};
}
/**
* Map internal customer record to Rome CustomerUpdateRq.
*/
function mapCustomerUpdate(customer, bodyshopConfig) {
if (!customer) return {};
return {
...mapCustomerInsert(customer, bodyshopConfig),
RequestId: `CUST-UPDATE-${customer.id}`
};
}
//
// ===================== VEHICLE =====================
//
/**
* Map vehicle to Rome ServiceVehicleAddRq.
*/
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
if (!vehicle) return {};
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `VEH-${vehicle.id}`,
CustomerId: ownerCustomer?.external_id,
VIN: vehicle.vin,
UnitNumber: vehicle.unit_number,
StockNumber: vehicle.stock_number,
Year: num(vehicle.year),
Make: vehicle.make,
Model: vehicle.model,
Trim: vehicle.trim,
BodyStyle: vehicle.body_style,
Transmission: vehicle.transmission,
Engine: vehicle.engine,
FuelType: vehicle.fuel_type,
DriveType: vehicle.drive_type,
Color: vehicle.color,
LicensePlate: vehicle.license_plate,
LicenseState: vehicle.license_state,
Odometer: num(vehicle.odometer),
OdometerUnits: vehicle.odometer_units || "KM",
InServiceDate: formatDate(vehicle.in_service_date),
Insurance: vehicle.insurance
? {
CompanyName: vehicle.insurance.company,
PolicyNumber: vehicle.insurance.policy,
ExpirationDate: formatDate(vehicle.insurance.expiration_date)
}
: undefined,
Warranty: vehicle.warranty
? {
WarrantyCompany: vehicle.warranty.company,
WarrantyNumber: vehicle.warranty.number,
WarrantyType: vehicle.warranty.type,
ExpirationDate: formatDate(vehicle.warranty.expiration_date)
}
: undefined,
VehicleNotes: vehicle.notes?.length ? { Items: vehicle.notes.map((n) => n.text || n) } : undefined
};
}
//
// ===================== REPAIR ORDER =====================
//
/**
* Map internal job to Rome RepairOrderInsertRq.
* NOTE: The CreateRepairOrder.xml template expects *flat* fields for Customer and ServiceVehicle
* (no {{#Customer}} or {{#ServiceVehicle}} sections). Therefore, we flatten those values here.
*/
function mapRepairOrderCreate(job, bodyshopConfig) {
if (!job) return {};
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
const cust = job.customer || {};
const veh = job.vehicle || {};
// Prefer a concrete address on the customer, fall back to job-level
const customerAddress = cust.address || job.customer_address || job.address || undefined;
return {
// Routing/meta we keep available for logging or other templates
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `RO-${job.id}`,
Environment: process.env.NODE_ENV,
// Header fields
RepairOrderNumber: job.ro_number,
DmsRepairOrderId: job.external_id,
OpenDate: formatDate(job.open_date),
PromisedDate: formatDate(job.promised_date),
CloseDate: formatDate(job.close_date),
ServiceAdvisorId: job.advisor_id,
TechnicianId: job.technician_id,
Department: job.department,
ProfitCenter: job.profit_center,
ROType: job.ro_type,
Status: job.status,
IsBodyShop: "true",
DRPFlag: toBoolStr(!!job.drp_flag) || "false",
// Customer block is FLAT (template does not use {{#Customer}} section)
CustomerId: cust.external_id,
CustomerName: cust.full_name || [cust.first_name, cust.last_name].filter(Boolean).join(" ").trim() || undefined,
PhoneNumber: cust.phone,
EmailAddress: cust.email,
Address: mapAddress(customerAddress),
// ServiceVehicle block is FLAT (template does not use {{#ServiceVehicle}} section)
VehicleId: veh.external_id,
VIN: veh.vin,
LicensePlate: veh.license_plate,
Year: num(veh.year),
Make: veh.make,
Model: veh.model,
Odometer: num(veh.odometer),
Color: veh.color,
// Lines
JobLines: (job.joblines || []).map((l, i) => ({
Sequence: i + 1,
ParentSequence: l.parent_sequence,
LineType: l.line_type,
Category: l.category,
OpCode: l.op_code,
Description: l.description,
LaborHours: num(l.labor_hours),
LaborRate: num(l.labor_rate),
PartNumber: l.part_number,
PartDescription: l.part_description,
Quantity: num(l.quantity),
UnitPrice: num(l.unit_price),
ExtendedPrice: num(l.extended_price),
DiscountAmount: num(l.discount_amount),
TaxCode: l.tax_code,
GLAccount: l.gl_account,
ControlNumber: l.control_number,
Taxes: l.taxes?.length
? {
Items: l.taxes.map((t) => ({
Code: t.code,
Amount: num(t.amount),
Rate: num(t.rate)
}))
}
: undefined
})),
// Totals
Totals: hasAny({
Currency: job.currency || "CAD",
LaborTotal: job.totals?.labor,
PartsTotal: job.totals?.parts,
MiscTotal: job.totals?.misc,
DiscountTotal: job.totals?.discount,
TaxTotal: job.totals?.tax,
GrandTotal: job.totals?.grand
})
? {
Currency: job.currency || "CAD",
LaborTotal: num(job.totals?.labor),
PartsTotal: num(job.totals?.parts),
MiscTotal: num(job.totals?.misc),
DiscountTotal: num(job.totals?.discount),
TaxTotal: num(job.totals?.tax),
GrandTotal: num(job.totals?.grand)
}
: undefined,
// Payments
Payments: job.payments?.length
? {
Items: job.payments.map((p) => ({
PayerType: p.payer_type,
PayerName: p.payer_name,
Amount: num(p.amount),
Method: p.method,
Reference: p.reference,
ControlNumber: p.control_number
}))
}
: undefined,
// Insurance
Insurance: job.insurance
? {
CompanyName: job.insurance.company,
ClaimNumber: job.insurance.claim_number,
AdjusterName: job.insurance.adjuster_name,
AdjusterPhone: job.insurance.adjuster_phone
}
: undefined,
// Notes
Notes: job.notes?.length ? { Items: job.notes.map((n) => n.text || n) } : undefined
};
}
/**
* Map for repair order updates.
*/
function mapRepairOrderUpdate(job, bodyshopConfig) {
return {
...mapRepairOrderCreate(job, bodyshopConfig),
RequestId: `RO-UPDATE-${job.id}`
};
}
//
// ===================== LOOKUPS =====================
//
function mapAdvisorLookup(criteria, bodyshopConfig) {
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
SearchCriteria: {
Department: criteria.department || "Body Shop",
Status: criteria.status || "ACTIVE"
}
};
}
function mapPartsLookup(criteria, bodyshopConfig) {
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `LOOKUP-PART-${Date.now()}`,
SearchCriteria: {
PartNumber: criteria.part_number,
Description: criteria.description,
Make: criteria.make,
Model: criteria.model,
Year: num(criteria.year),
Category: criteria.category,
MaxResults: criteria.max_results || 25
}
};
}
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
// accept nested or flat input
const c = criteria || {};
const cust = c.customer || c.Customer || {};
const veh = c.vehicle || c.Vehicle || {};
const comp = c.company || c.Company || {};
// build optional blocks only if they have at least one value
const customerBlock = {
FirstName: cust.firstName || cust.FirstName || c.firstName,
LastName: cust.lastName || cust.LastName || c.lastName,
PhoneNumber: cust.phoneNumber || cust.PhoneNumber || c.phoneNumber || c.phone,
EmailAddress: cust.email || cust.EmailAddress || c.email,
CompanyName: cust.companyName || cust.CompanyName || c.companyName,
CustomerId: cust.customerId || cust.CustomerId || c.customerId
};
const vehicleBlock = {
VIN: veh.vin || veh.VIN || c.vin,
LicensePlate: veh.licensePlate || veh.LicensePlate || c.licensePlate,
Make: veh.make || veh.Make || c.make,
Model: veh.model || veh.Model || c.model,
Year: veh.year != null ? String(veh.year) : c.year != null ? String(c.year) : undefined,
VehicleId: veh.vehicleId || veh.VehicleId || c.vehicleId
};
const companyBlock = {
Name: comp.name || comp.Name || c.companyName,
Phone: comp.phone || comp.Phone || c.companyPhone
};
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerName: bodyshopConfig?.dealer_name,
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: c.requestId || `COMBINED-${Date.now()}`,
Environment: process.env.NODE_ENV,
// Only include these blocks when they have content; Mustache {{#Block}} respects undefined
Customer: hasAny(customerBlock) ? customerBlock : undefined,
Vehicle: hasAny(vehicleBlock) ? vehicleBlock : undefined, // template wraps as <rr:ServiceVehicle>…</rr:ServiceVehicle>
Company: hasAny(companyBlock) ? companyBlock : undefined,
// Search behavior flags
SearchMode: c.searchMode || c.SearchMode, // EXACT | PARTIAL
ExactMatch: toBoolStr(c.exactMatch ?? c.ExactMatch),
PartialMatch: toBoolStr(c.partialMatch ?? c.PartialMatch),
CaseInsensitive: toBoolStr(c.caseInsensitive ?? c.CaseInsensitive),
// Result shaping (default to true when unspecified)
ReturnCustomers: toBoolStr(c.returnCustomers ?? c.ReturnCustomers ?? true),
ReturnVehicles: toBoolStr(c.returnVehicles ?? c.ReturnVehicles ?? true),
ReturnCompanies: toBoolStr(c.returnCompanies ?? c.ReturnCompanies ?? true),
// Paging / sorting
MaxResults: c.maxResults ?? c.MaxResults,
PageNumber: c.pageNumber ?? c.PageNumber,
SortBy: c.sortBy ?? c.SortBy, // e.g., NAME, VIN, PARTNUMBER
SortDirection: c.sortDirection ?? c.SortDirection // ASC | DESC
};
}
module.exports = {
mapCustomerInsert,
mapCustomerUpdate,
mapServiceVehicle,
mapRepairOrderCreate,
mapRepairOrderUpdate,
mapAdvisorLookup,
mapPartsLookup,
mapCombinedSearch
};

View File

@@ -1,165 +1,17 @@
/**
* @file rr-repair-orders.js
* @description Rome (Reynolds & Reynolds) Repair Order Integration.
* Handles creation and updates of repair orders (BSMRepairOrderRq/Resp).
*/
const { withClient } = require("./withClient");
"use strict";
const { MakeRRCall } = require("./rr-helpers");
const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers");
const RRLogger = require("./rr-logger");
const { RrApiError } = require("./rr-error");
/**
* Very light sanity checks before we build XML.
* We keep these minimal because the mapper may derive some fields.
* Throws RrApiError when a required precondition is missing.
* @param {"CreateRepairOrder"|"UpdateRepairOrder"} action
* @param {Object} job
*/
function preflight(action, job) {
if (!job || !job.id) {
throw new RrApiError("Missing job payload or job.id", "RR_BAD_JOB_PAYLOAD");
}
// VIN is almost always required for BSM RO flows
const vin = job?.vehicle?.vin || job?.vehicle?.VIN || job?.VIN || job?.vin;
if (!vin) {
throw new RrApiError("Missing VIN on job.vehicle", "RR_MISSING_VIN");
}
if (action === "UpdateRepairOrder") {
// If your mapper expects a DMS RO number or an external RO number,
// you can tighten this guard based on your schema, e.g.:
// const hasKey = job?.dms_ro_no || job?.external_ro_number || job?.roNumber;
// if (!hasKey) throw new RrApiError("Missing RO key for update", "RR_MISSING_RO_KEY");
}
async function createRepairOrder({ bodyshopId, payload }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.createRepairOrder(payload, { routing });
return res;
});
}
/**
* Build an explicit ApplicationArea override so the envelope is always correct,
* even if the helper falls back to defaults. We include routing info
* and set Task/ReferenceId for BSM repair orders.
* @param {"CreateRepairOrder"|"UpdateRepairOrder"} action
* @param {Object} cfg
*/
function buildAppArea(action, cfg) {
const isCreate = action === "CreateRepairOrder";
return {
Sender: {
Component: "Rome",
Task: "BSMRO",
ReferenceId: isCreate ? "Insert" : "Update"
},
Destination: {
DealerNumber: cfg?.DealerNumber || cfg?.dealerNumber,
StoreNumber: cfg?.StoreNumber || cfg?.storeNumber,
AreaNumber: cfg?.AreaNumber || cfg?.areaNumber,
DestinationNameCode: "RR"
}
// CreationDateTime and BODId will be provided by rr-helpers if omitted.
};
async function updateRepairOrder({ bodyshopId, payload }) {
return withClient(bodyshopId, async (client, routing) => {
const res = await client.updateRepairOrder(payload, { routing });
return res;
});
}
/**
* Create a new repair order in Rome.
* @param {Socket} socket - active socket connection
* @param {Object} job - Hasura job object (including vehicle, customer, joblines)
* @param {Object} bodyshopConfig - DMS config for current bodyshop
* @returns {Promise<Object>} normalized result
*/
async function createRepairOrder(socket, job, bodyshopConfig) {
const action = "CreateRepairOrder";
const template = "CreateRepairOrder"; // maps to xml-templates/CreateRepairOrder.xml
try {
RRLogger(socket, "info", `Starting RR ${action} for job ${job?.id}`, {
jobid: job?.id,
dealer: bodyshopConfig?.DealerNumber || bodyshopConfig?.dealerNumber,
store: bodyshopConfig?.StoreNumber || bodyshopConfig?.storeNumber,
area: bodyshopConfig?.AreaNumber || bodyshopConfig?.areaNumber
});
preflight(action, job);
const data = mapRepairOrderCreate(job, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
appArea: buildAppArea(action, bodyshopConfig),
socket,
dealerConfig: bodyshopConfig,
jobid: job.id
});
RRLogger(socket, "debug", `${action} completed successfully`, { jobid: job.id });
return {
success: true,
dms: "Rome",
jobid: job.id,
action,
xml: resultXml
};
} catch (error) {
RRLogger(socket, "error", `Error in ${action} for job ${job?.id}`, {
message: error?.message,
stack: error?.stack
});
throw new RrApiError(`RR CreateRepairOrder failed: ${error.message}`, "CREATE_RO_ERROR");
}
}
/**
* Update an existing repair order in Rome.
* @param {Socket} socket
* @param {Object} job
* @param {Object} bodyshopConfig
* @returns {Promise<Object>}
*/
async function updateRepairOrder(socket, job, bodyshopConfig) {
const action = "UpdateRepairOrder";
const template = "UpdateRepairOrder";
try {
RRLogger(socket, "info", `Starting RR ${action} for job ${job?.id}`, {
jobid: job?.id,
dealer: bodyshopConfig?.DealerNumber || bodyshopConfig?.dealerNumber,
store: bodyshopConfig?.StoreNumber || bodyshopConfig?.storeNumber,
area: bodyshopConfig?.AreaNumber || bodyshopConfig?.areaNumber
});
preflight(action, job);
const data = mapRepairOrderUpdate(job, bodyshopConfig);
const resultXml = await MakeRRCall({
action,
body: { template, data },
appArea: buildAppArea(action, bodyshopConfig),
socket,
dealerConfig: bodyshopConfig,
jobid: job.id
});
RRLogger(socket, "debug", `${action} completed successfully`, { jobid: job.id });
return {
success: true,
dms: "Rome",
jobid: job.id,
action,
xml: resultXml
};
} catch (error) {
RRLogger(socket, "error", `Error in ${action} for job ${job?.id}`, {
message: error?.message,
stack: error?.stack
});
throw new RrApiError(`RR UpdateRepairOrder failed: ${error.message}`, "UPDATE_RO_ERROR");
}
}
module.exports = {
createRepairOrder,
updateRepairOrder
};
module.exports = { createRepairOrder, updateRepairOrder };

View File

@@ -1,484 +0,0 @@
#!/usr/bin/env node
/**
* rr-test.js — end-to-end exerciser for Reynolds "Rome"/STAR actions.
*
* Key improvements vs prior version:
* - Prints FULL XML responses (no truncation).
* - CombinedSearch now sends at least one search criterion by default.
* - InsertCustomer sets required IBFlag + LastName/FirstName.
* - Orchestrates dependent steps (CombinedSearch → InsertCustomer → InsertVehicle → Create/Update RO).
*
* Usage examples:
* node rr-test.js --ping
* node rr-test.js --all
* node rr-test.js --combined --last "SMITH" --phone 9375550001 --vin 1FTFW1E50JFA00000
*
* Common flags:
* --first, --last, --phone, --email, --vin, --plate, --advisor, --max N
*
* Env fallbacks:
* RR_TEST_LAST, RR_TEST_FIRST, RR_TEST_PHONE, RR_TEST_EMAIL, RR_TEST_VIN, RR_TEST_PLATE, RR_TEST_PARTDESC
*
* Dealer creds/env (used only in this test harness; runtime pulls from DB):
* RR_DEALER_NUMBER, RR_STORE_NUMBER, RR_AREA_NUMBER, RR_USERNAME, RR_PASSWORD, RR_ENDPOINT
*/
const path = require("path");
const fs = require("fs");
const fsp = require("fs/promises");
const minimist = require("minimist");
const dayjs = require("dayjs");
const { XMLParser } = require("fast-xml-parser");
// Load dev env if present
const envPath = path.resolve(__dirname, "../../.env.development");
if (fs.existsSync(envPath)) {
require("dotenv").config({ path: envPath });
console.log(envPath);
console.log(
`[dotenv@${require("dotenv/package.json").version}] injecting env (${Object.keys(process.env).length}) from ../../.env.development`
);
}
const { MakeRRCall, parseRRResponse } = require("./rr-helpers");
const RRLogger = require("./rr-logger");
// CLI flags
const argv = minimist(process.argv.slice(2));
const FLAG = (k, d) => (argv[k] !== undefined ? argv[k] : d);
// For templates that expect STAR NS in {{STAR_NS}}
const STAR_NS = "http://www.starstandards.org/STAR";
// Basic XML parser for optional extractions
const parser = new XMLParser({ ignoreAttributes: false, removeNSPrefix: true });
// Ensure templates exist
async function verifyTemplates() {
const required = [
"CombinedSearch",
"GetAdvisors",
"GetParts",
"InsertCustomer",
"UpdateCustomer",
"InsertServiceVehicle",
"CreateRepairOrder",
"UpdateRepairOrder"
];
for (const t of required) {
const p = path.join(__dirname, "xml-templates", `${t}.xml`);
await fsp.readFile(p, "utf8");
}
console.log("✅ Templates verified.");
}
// Format AppArea per action (Sender.Task/ReferenceId)
function appAreaFor(action, cfg) {
const pad2 = (x) => String(x ?? "").padStart(2, "0");
const base = {
CreationDateTime: new Date().toISOString(),
BODId: `BOD-${Date.now()}`,
Sender: { Component: "Rome", CreatorNameCode: "RCI", SenderNameCode: "RCI" },
Destination: {
DealerNumber: cfg.dealerNumber,
StoreNumber: pad2(cfg.storeNumber),
AreaNumber: pad2(cfg.branchNumber || "01")
}
};
switch (action) {
case "CreateRepairOrder":
return { ...base, Sender: { ...base.Sender, Task: "BSMRO", ReferenceId: "Insert" } };
case "UpdateRepairOrder":
return { ...base, Sender: { ...base.Sender, Task: "BSMRO", ReferenceId: "Update" } };
case "InsertCustomer":
return { ...base, Sender: { ...base.Sender, Task: "CU", ReferenceId: "Insert" } };
case "UpdateCustomer":
return { ...base, Sender: { ...base.Sender, Task: "CU", ReferenceId: "Update" } };
case "InsertServiceVehicle":
return { ...base, Sender: { ...base.Sender, Task: "SV", ReferenceId: "Insert" } };
case "CombinedSearch":
return { ...base, Sender: { ...base.Sender, Task: "SV", ReferenceId: "Query" } };
case "GetAdvisors":
return { ...base, Sender: { ...base.Sender, Task: "RCI", ReferenceId: "Lookup" } };
case "GetParts":
return { ...base, Sender: { ...base.Sender, Task: "RCI", ReferenceId: "Lookup" } };
default:
return base;
}
}
// Build cfg for test harness (env fallbacks allowed here)
function cfgFromEnv() {
return {
baseUrl:
process.env.RR_ENDPOINT || process.env.RR_BASE_URL || "https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx",
username: process.env.RR_USERNAME || "Rome",
password: process.env.RR_PASSWORD || "",
timeout: Number(process.env.RR_TIMEOUT || 30000),
dealerNumber: process.env.RR_DEALER_NUMBER || "PPERASV02000000",
storeNumber: process.env.RR_STORE_NUMBER || "05",
branchNumber: process.env.RR_AREA_NUMBER || "03"
};
}
// Utility: get a value from parsed JSON or raw XML (element or attribute)
function findFirstId(parsed, rawXml, keyRegex) {
// search parsed object
if (parsed && typeof parsed === "object") {
const stack = [parsed];
while (stack.length) {
const cur = stack.pop();
if (cur && typeof cur === "object") {
for (const [k, v] of Object.entries(cur)) {
if (keyRegex.test(k) && (typeof v === "string" || typeof v === "number")) {
const s = String(v).trim();
if (s) return s;
}
if (v && typeof v === "object") stack.push(v);
}
}
}
}
// search raw XML: <Key>value</Key> or Key="value"
if (rawXml) {
const re1 = new RegExp(`<([A-Za-z0-9:_-]*${keyRegex.source}[A-Za-z0-9:_-]*)>([^<]+)</\\1>`, "i");
const m1 = re1.exec(rawXml);
if (m1 && m1[2]) return m1[2].trim();
const re2 = new RegExp(`${keyRegex.source}="([^"]+)"`, "i");
const m2 = re2.exec(rawXml);
if (m2 && m2[1]) return m2[1].trim();
}
return undefined;
}
// Call wrapper: prints FULL XML
async function callRR(action, dataObj, cfg) {
const respXml = await MakeRRCall({
action,
dealerConfig: cfg,
body: { data: { STAR_NS, ...dataObj }, appArea: appAreaFor(action, cfg) }
});
const parsed = parseRRResponse(respXml);
return { respXml, parsed };
}
// --------- Individual Steps (full XML printed) ---------
async function stepGetAdvisors(ctx) {
const cfg = ctx.cfg;
const data = {
SearchCriteria: {
Department: FLAG("dept", "B"), // critical to avoid 201
Status: FLAG("status", "ACTIVE"),
MaxResults: Number(FLAG("max", 5)) || 5
}
};
const { respXml, parsed } = await callRR("GetAdvisors", data, cfg);
console.log("\n[GetAdvisors] RESPONSE (FULL):\n");
console.log(respXml);
const advId =
findFirstId(parsed.parsed, respXml, /(Advisor(ID|No|Number)?|AdvNo)/i) ||
findFirstId(parsed.parsed, respXml, /(Employee(ID|No|Number)?)/i);
if (advId) ctx.advisorId = advId;
return { ok: true, code: parsed.code, advisorId: advId || null };
}
async function stepCombinedSearch(ctx) {
const cfg = ctx.cfg;
// Provide at least one criterion by default to avoid 201
const defaultLast = FLAG("last", process.env.RR_TEST_LAST || "SMITH");
const defaultPhone = FLAG("phone", process.env.RR_TEST_PHONE || "");
const defaultVin = FLAG("vin", process.env.RR_TEST_VIN || "");
const data = {
MaxResults: Number(FLAG("max", 5)) || 5,
Customer: {},
Vehicle: {}
};
if (defaultLast) data.Customer.LastName = defaultLast;
if (defaultPhone) data.Customer.PhoneNumber = defaultPhone;
if (defaultVin) data.Vehicle.VIN = defaultVin;
const { respXml, parsed } = await callRR("CombinedSearch", data, cfg);
console.log("\n[CombinedSearch] RESPONSE (FULL):\n");
console.log(respXml);
const custId = findFirstId(parsed.parsed, respXml, /(Cust(omer)?(No|Id|ID|Number|Key)|NameRecId)/i);
const vehId = findFirstId(parsed.parsed, respXml, /(Veh(icle)?(No|Id|ID|Number|Key))/i);
const vin = findFirstId(parsed.parsed, respXml, /VIN/i);
if (custId) ctx.customerId = custId;
if (vehId) ctx.vehicleId = vehId;
if (vin) ctx.vin = vin;
return { ok: true, code: parsed.code, customerId: custId || null, vehicleId: vehId || null, vin: vin || null };
}
async function stepInsertCustomer(ctx) {
const cfg = ctx.cfg;
// RR requires IBFlag and LastName for individual customers
const ts = dayjs().format("YYYYMMDD-HHmmss");
const firstName = FLAG("first", process.env.RR_TEST_FIRST || "QA");
const lastName = FLAG("last", process.env.RR_TEST_LAST || "Test");
const email = FLAG("email", process.env.RR_TEST_EMAIL || `qa.${ts}@example.com`);
const phone = FLAG(
"phone",
process.env.RR_TEST_PHONE || `937555${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`
);
const data = {
IBFlag: "I", // <== REQUIRED (I=Individual, B=Business)
FirstName: firstName, // <== REQUIRED with IBFlag=I
LastName: lastName, // <== REQUIRED with IBFlag=I
Active: "Y",
Phones: [{ Type: "Mobile", CountryCode: "1", Number: phone, Preferred: "Y" }],
Emails: [{ Type: "Personal", Address: email, Preferred: "Y" }],
Addresses: [
{
Type: "Home",
Line1: "123 Test St",
City: "Dayton",
State: "OH",
PostalCode: "45402",
Country: "US",
IsPrimary: "Y"
}
]
};
const { respXml, parsed } = await callRR("InsertCustomer", data, cfg);
console.log("\n[InsertCustomer] RESPONSE (FULL):\n");
console.log(respXml);
// RR often returns the new customer key as DMSRecKey attribute in TransStatus
const custId = findFirstId(parsed.parsed, respXml, /(DMSRecKey|Cust(omer)?(No|Id|ID|Number|Key)|NameRecId)/i);
if (custId) ctx.customerId = custId;
return { ok: !!custId, code: parsed.code, customerId: custId || null };
}
async function stepUpdateCustomer(ctx) {
const cfg = ctx.cfg;
if (!ctx.customerId) return { ok: false, code: "NO_CONTEXT", message: "No customerId" };
const data = {
CustomerId: ctx.customerId,
Emails: [{ Type: "Personal", Address: `qa.${dayjs().format("HHmmss")}@example.com`, Preferred: "Y" }]
};
const { respXml, parsed } = await callRR("UpdateCustomer", data, cfg);
console.log("\n[UpdateCustomer] RESPONSE (FULL):\n");
console.log(respXml);
return { ok: parsed.success, code: parsed.code };
}
async function stepInsertServiceVehicle(ctx) {
const cfg = ctx.cfg;
if (!ctx.customerId) return { ok: false, code: "NO_CONTEXT", message: "No customerId" };
const vin =
FLAG("vin", process.env.RR_TEST_VIN) ||
`1FTFW1E50JFA${String(Math.floor(Math.random() * 1000000)).padStart(6, "0")}`;
const data = {
CustomerId: ctx.customerId,
VIN: vin,
Year: "2018",
Make: "FORD",
Model: "F-150",
Color: "WHITE",
LicensePlate:
FLAG("plate", process.env.RR_TEST_PLATE) || `QA${String(Math.floor(Math.random() * 100000)).padStart(5, "0")}`,
LicenseState: "OH",
Odometer: "123456",
OdometerUnits: "MI"
};
const { respXml, parsed } = await callRR("InsertServiceVehicle", data, cfg);
console.log("\n[InsertServiceVehicle] RESPONSE (FULL):\n");
console.log(respXml);
const vehId = findFirstId(parsed.parsed, respXml, /(Veh(icle)?(No|Id|ID|Number|Key))/i);
if (vehId) ctx.vehicleId = vehId;
ctx.vin = vin;
return { ok: parsed.success || !!vehId, code: parsed.code, vehicleId: vehId || null, vin };
}
async function stepCreateRepairOrder(ctx) {
const cfg = ctx.cfg;
if (!ctx.customerId) return { ok: false, code: "NO_CONTEXT", message: "No customerId" };
const roNumber = `BSM-${dayjs().format("MMDD-HHmmss")}-${String(Math.floor(Math.random() * 1000)).padStart(3, "0")}`;
const data = {
RepairOrderNumber: roNumber,
Department: "B",
ROType: "INS",
Status: "OPEN",
IsBodyShop: "Y",
ServiceAdvisorId: ctx.advisorId || FLAG("advisor", process.env.RR_TEST_ADVISOR) || undefined,
Customer: {
CustomerId: ctx.customerId,
FirstName: FLAG("first", process.env.RR_TEST_FIRST || "QA"),
LastName: FLAG("last", process.env.RR_TEST_LAST || "Test"),
PhoneNumber: FLAG("phone", process.env.RR_TEST_PHONE || "9375550000"),
EmailAddress: FLAG("email", process.env.RR_TEST_EMAIL || "qa@example.com")
},
ServiceVehicle: {
VIN: ctx.vin || FLAG("vin", process.env.RR_TEST_VIN) || undefined,
Odometer: "4321"
},
Totals: {
Currency: "USD",
LaborTotal: "0",
PartsTotal: "0",
MiscTotal: "0",
DiscountTotal: "0",
TaxTotal: "0",
GrandTotal: "0"
}
};
const respXml = await MakeRRCall({
action: "CreateRepairOrder",
dealerConfig: cfg,
body: { data: { STAR_NS, ...data }, appArea: appAreaFor("CreateRepairOrder", cfg) }
});
const parsed = parseRRResponse(respXml);
console.log("\n[CreateRepairOrder] RESPONSE (FULL):\n");
console.log(respXml);
const roNo = findFirstId(parsed.parsed, respXml, /(DMSRoNo|RO?No|OutsdRoNo|RepairOrder(Id|Number))/i) || roNumber;
ctx.roNumber = roNo;
return { ok: parsed.success, code: parsed.code, roNumber: roNo };
}
async function stepUpdateRepairOrder(ctx) {
const cfg = ctx.cfg;
if (!ctx.roNumber) return { ok: false, code: "NO_CONTEXT", message: "No roNumber" };
const data = {
RepairOrderNumber: ctx.roNumber,
Department: "B",
Status: "OPEN",
Totals: { LaborTotal: "0", PartsTotal: "0", MiscTotal: "0", TaxTotal: "0", GrandTotal: "0" }
};
const { respXml, parsed } = await callRR("UpdateRepairOrder", data, cfg);
console.log("\n[UpdateRepairOrder] RESPONSE (FULL):\n");
console.log(respXml);
return { ok: parsed.success, code: parsed.code };
}
async function stepGetParts(ctx) {
const cfg = ctx.cfg;
const data = {
MaxResults: Number(FLAG("max", 5)) || 5,
SearchMode: "Description",
Description: FLAG("partdesc", process.env.RR_TEST_PARTDESC || "clip")
};
const { respXml, parsed } = await callRR("GetParts", data, cfg);
console.log("\n[GetParts] RESPONSE (FULL):\n");
console.log(respXml);
return { ok: parsed.success, code: parsed.code };
}
// --------- Runner ---------
async function runAll() {
await verifyTemplates();
const cfg = cfgFromEnv();
const ctx = { cfg };
const results = [];
async function runStep(name, fn) {
const t0 = Date.now();
try {
const res = await fn(ctx);
results.push({ step: name, ok: !!res.ok, code: res.code || "OK", ms: Date.now() - t0 });
console.log(`\n${name} done in ${Date.now() - t0}ms`);
return res;
} catch (err) {
results.push({ step: name, ok: false, code: err.code || "ERR", ms: Date.now() - t0, error: err.message });
console.error(`\n${name} failed: ${err.message}`);
throw err;
}
}
// Sequence (dependencies respected)
await runStep("GetAdvisors", stepGetAdvisors);
await runStep("CombinedSearch", stepCombinedSearch);
if (!ctx.customerId) await runStep("InsertCustomer", stepInsertCustomer);
if (!ctx.vehicleId) await runStep("InsertServiceVehicle", stepInsertServiceVehicle);
await runStep("CreateRepairOrder", stepCreateRepairOrder);
await runStep("UpdateRepairOrder", stepUpdateRepairOrder);
await runStep("GetParts", stepGetParts);
// Summary
console.log("\n=== SUMMARY ===");
console.table(results.map((r) => ({ step: r.step, ok: r.ok ? "✅" : "❌", code: r.code, ms: r.ms })));
const failed = results.some((r) => !r.ok);
console.log(`Total: ${results.reduce((a, r) => a + r.ms, 0)}ms | Status: ${failed ? "FAIL" : "PASS"}`);
if (failed) process.exitCode = 1;
}
// Entrypoint
(async () => {
if (FLAG("ping", false)) {
await verifyTemplates();
const cfg = cfgFromEnv();
console.log("\n▶ Calling Rome action: GetAdvisors");
const r = await stepGetAdvisors({ cfg });
if (r.ok) console.log("\n✅ RR call completed.\n");
return;
}
if (FLAG("all", true)) {
await runAll();
return;
}
// Individual flags (optional)
await verifyTemplates();
const cfg = cfgFromEnv();
const ctx = { cfg };
const want = [];
if (FLAG("get-advisors", false)) want.push(stepGetAdvisors);
if (FLAG("combined", false)) want.push(stepCombinedSearch);
if (FLAG("insert-customer", false)) want.push(stepInsertCustomer);
if (FLAG("update-customer", false)) want.push(stepUpdateCustomer);
if (FLAG("insert-vehicle", false)) want.push(stepInsertServiceVehicle);
if (FLAG("create-ro", false)) want.push(stepCreateRepairOrder);
if (FLAG("update-ro", false)) want.push(stepUpdateRepairOrder);
if (FLAG("get-parts", false)) want.push(stepGetParts);
for (const fn of want) {
const res = await fn(ctx).catch((e) => ({ ok: false, code: "ERR", error: e.message }));
console.log(res);
}
})();

View File

@@ -1,126 +0,0 @@
/**
* @file rr-wsdl.js
* @description Lightweight service description + utilities for the Rome (R&R) SOAP actions.
* - Maps actions to SOAPAction headers (from rr-constants)
* - Maps actions to Mustache template filenames (xml-templates/*.xml)
* - Provides verification helpers to ensure templates exist
* - Provides normalized SOAP headers used by the transport
*/
const path = require("path");
const fs = require("fs/promises");
const { RR_ACTIONS, RR_SOAP_HEADERS } = require("./rr-constants");
const mustache = require("mustache");
// ---- Action <-> Template wiring ----
// Keep action names consistent with rr-helpers / rr-lookup / rr-repair-orders / rr-customer
const ACTION_TEMPLATES = Object.freeze({
InsertCustomer: "InsertCustomer",
UpdateCustomer: "UpdateCustomer",
InsertServiceVehicle: "InsertServiceVehicle",
CreateRepairOrder: "CreateRepairOrder",
UpdateRepairOrder: "UpdateRepairOrder",
GetAdvisors: "GetAdvisors",
GetParts: "GetParts",
CombinedSearch: "CombinedSearch"
});
/**
* Get the SOAPAction string for a known action.
* Throws if action is unknown.
*/
function getSoapAction(action) {
const entry = RR_ACTIONS[action];
if (!entry) {
const known = Object.keys(RR_ACTIONS).join(", ");
throw new Error(`Unknown RR action "${action}". Known: ${known}`);
}
return entry.soapAction;
}
/**
* Get the template filename (without extension) for a known action.
* e.g., "CreateRepairOrder" -> "CreateRepairOrder"
*/
function getTemplateForAction(action) {
const tpl = ACTION_TEMPLATES[action];
if (!tpl) {
const known = Object.keys(ACTION_TEMPLATES).join(", ");
throw new Error(`No template mapping for RR action "${action}". Known: ${known}`);
}
return tpl;
}
/**
* Build headers for a SOAP request, including SOAPAction.
* Consumers: rr-helpers (transport).
*/
function buildSoapHeadersForAction(action) {
return {
...RR_SOAP_HEADERS,
SOAPAction: getSoapAction(action)
};
}
/**
* List all known actions with their SOAPAction + template.
* Useful for diagnostics (e.g., /rr/actions route).
*/
function listActions() {
return Object.keys(ACTION_TEMPLATES).map((action) => ({
action,
soapAction: getSoapAction(action),
template: getTemplateForAction(action)
}));
}
/**
* Verify that every required template exists in xml-templates/.
* Returns an array of issues; empty array means all good.
*/
async function verifyTemplatesExist() {
const issues = [];
const baseDir = path.join(__dirname, "xml-templates");
for (const [action, tpl] of Object.entries(ACTION_TEMPLATES)) {
const filePath = path.join(baseDir, `${tpl}.xml`);
try {
const contents = await fs.readFile(filePath, "utf8"); // throws if missing
try {
// Parse-only to catch “Unclosed section …” and similar
mustache.parse(contents);
} catch (parseErr) {
issues.push({ action, template: tpl, error: `Mustache parse error: ${parseErr.message}`, filePath });
}
} catch {
issues.push({ action, template: tpl, error: `Missing file: ${filePath}` });
}
}
return issues;
}
/**
* Quick assert that throws if any template is missing.
* You can call this once during boot and log the result.
*/
async function assertTemplates() {
const issues = await verifyTemplatesExist();
if (issues.length) {
const msg =
"RR xml-templates verification failed:\n" +
issues.map((i) => ` - ${i.action} -> ${i.template}.xml :: ${i.error}`).join("\n");
throw new Error(msg);
}
}
module.exports = {
// Maps / helpers
ACTION_TEMPLATES,
listActions,
getSoapAction,
getTemplateForAction,
buildSoapHeadersForAction,
// Verification
verifyTemplatesExist,
assertTemplates
};

View File

@@ -1,18 +1,5 @@
/**
* @file rrRoutes.js
* @description Express routes for Reynolds & Reynolds (Rome) integration.
* Endpoints:
* - POST /rr/customer/insert
* - POST /rr/customer/update
* - POST /rr/repair-order/create
* - POST /rr/repair-order/update
* - POST /rr/lookup/advisors
* - POST /rr/lookup/parts
* - POST /rr/lookup/combined-search
* - POST /rr/export/job
* - GET /rr/actions
* - GET /rr/templates/verify
*/
// server/rr/rrRoutes.js
"use strict";
const express = require("express");
const router = express.Router();
@@ -20,19 +7,12 @@ const router = express.Router();
const RRLogger = require("./rr-logger");
const { RrApiError } = require("./rr-error");
// Domain modules
const customerApi = require("./rr-customer"); // insertCustomer, updateCustomer
const roApi = require("./rr-repair-orders"); // createRepairOrder, updateRepairOrder
const lookupApi = require("./rr-lookup"); // getAdvisors, getParts, combinedSearch
const { exportJobToRR } = require("./rr-job-export"); // orchestrator
const customerApi = require("./rr-customer");
const roApi = require("./rr-repair-orders");
const lookupApi = require("./rr-lookup");
const { exportJobToRR } = require("./rr-job-export");
// Diagnostics
const { listActions, verifyTemplatesExist } = require("./rr-wsdl");
// DB-driven RR config (no env fallback for dealer/store/branch here)
const { getRRConfigForBodyshop } = require("./rr-config");
// -------------------- Helpers --------------------
// --- helpers ---
function ok(res, payload = {}) {
return res.json({ success: true, ...payload });
@@ -44,174 +24,144 @@ function fail(res, error, status = 400) {
}
function socketOf(req) {
// If you stash a socket/logging context on the app, grab it; otherwise null
return (req.app && req.app.get && req.app.get("socket")) || null;
try {
return req.app?.get?.("socket") || null;
} catch {
return null;
}
}
/**
* Resolve the per-bodyshop RR config strictly from DB.
* Looks for bodyshopId in:
* - req.body.bodyshopId
* - req.body.job?.shopid
* - x-bodyshop-id header
* Throws if not found.
*/
async function resolveRRConfigHttp(req) {
const candidateHeader = req.get && req.get("x-bodyshop-id");
const body = req.body || {};
const bodyshopId = body.bodyshopId || (body.job && (body.job.shopid || body.job.bodyshopId)) || candidateHeader;
function requireBodyshopId(req) {
const body = req?.body || {};
const fromBody = body.bodyshopId;
const fromJob = body.job && (body.job.shopid || body.job.bodyshopId);
const fromHeader = typeof req.get === "function" ? req.get("x-bodyshop-id") : undefined;
const bodyshopId = fromBody || fromJob || fromHeader;
if (!bodyshopId) {
throw new RrApiError(
"Missing bodyshopId (expected in body.bodyshopId, body.job.shopid, or x-bodyshop-id header)",
"Missing bodyshopId (in body.bodyshopId, body.job.shopid/bodyshopId, or x-bodyshop-id header)",
"BAD_REQUEST"
);
}
return getRRConfigForBodyshop(bodyshopId);
return bodyshopId;
}
// -------------------- Customers --------------------
// --- customers ---
router.post("/rr/customer/insert", async (req, res) => {
const socket = socketOf(req);
const { customer } = req.body || {};
try {
const bodyshopId = requireBodyshopId(req);
const { customer } = req.body || {};
if (!customer) throw new RrApiError("Missing 'customer' in request body", "BAD_REQUEST");
const cfg = await resolveRRConfigHttp(req); // DB-driven, required
const result = await customerApi.insertCustomer(socket, customer, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /customer/insert failed", { err: err.message });
return fail(res, err);
const result = await customerApi.insertCustomer({ bodyshopId, payload: customer });
RRLogger(socket)("info", "RR customer insert", { bodyshopId });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/customer/insert failed", { error: e.message });
return fail(res, e);
}
});
router.post("/rr/customer/update", async (req, res) => {
const socket = socketOf(req);
const { customer } = req.body || {};
try {
const bodyshopId = requireBodyshopId(req);
const { customer } = req.body || {};
if (!customer) throw new RrApiError("Missing 'customer' in request body", "BAD_REQUEST");
const cfg = await resolveRRConfigHttp(req);
const result = await customerApi.updateCustomer(socket, customer, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /customer/update failed", { err: err.message });
return fail(res, err);
const result = await customerApi.updateCustomer({ bodyshopId, payload: customer });
RRLogger(socket)("info", "RR customer update", { bodyshopId });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/customer/update failed", { error: e.message });
return fail(res, e);
}
});
// -------------------- Repair Orders --------------------
// --- repair orders ---
router.post("/rr/repair-order/create", async (req, res) => {
const socket = socketOf(req);
const { job } = req.body || {};
try {
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
const cfg = await resolveRRConfigHttp(req);
const result = await roApi.createRepairOrder(socket, job, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /repair-order/create failed", { err: err.message });
return fail(res, err);
const bodyshopId = requireBodyshopId(req);
const { ro } = req.body || {};
if (!ro) throw new RrApiError("Missing 'ro' in request body", "BAD_REQUEST");
const result = await roApi.createRepairOrder({ bodyshopId, payload: ro });
RRLogger(socket)("info", "RR create RO", { bodyshopId });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/repair-order/create failed", { error: e.message });
return fail(res, e);
}
});
router.post("/rr/repair-order/update", async (req, res) => {
const socket = socketOf(req);
const { job } = req.body || {};
try {
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
const cfg = await resolveRRConfigHttp(req);
const result = await roApi.updateRepairOrder(socket, job, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /repair-order/update failed", { err: err.message });
return fail(res, err);
const bodyshopId = requireBodyshopId(req);
const { ro } = req.body || {};
if (!ro) throw new RrApiError("Missing 'ro' in request body", "BAD_REQUEST");
const result = await roApi.updateRepairOrder({ bodyshopId, payload: ro });
RRLogger(socket)("info", "RR update RO", { bodyshopId });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/repair-order/update failed", { error: e.message });
return fail(res, e);
}
});
// -------------------- Lookups --------------------
// --- lookups ---
router.post("/rr/lookup/advisors", async (req, res) => {
const socket = socketOf(req);
const { criteria = {} } = req.body || {};
try {
const cfg = await resolveRRConfigHttp(req);
const result = await lookupApi.getAdvisors(socket, criteria, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /lookup/advisors failed", { err: err.message });
return fail(res, err);
const bodyshopId = requireBodyshopId(req);
const result = await lookupApi.getAdvisors({ bodyshopId, ...req.body });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/lookup/advisors failed", { error: e.message });
return fail(res, e);
}
});
router.post("/rr/lookup/parts", async (req, res) => {
const socket = socketOf(req);
const { criteria = {} } = req.body || {};
try {
const cfg = await resolveRRConfigHttp(req);
const result = await lookupApi.getParts(socket, criteria, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /lookup/parts failed", { err: err.message });
return fail(res, err);
const bodyshopId = requireBodyshopId(req);
const result = await lookupApi.getParts({ bodyshopId, ...req.body });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/lookup/parts failed", { error: e.message });
return fail(res, e);
}
});
router.post("/rr/lookup/combined-search", async (req, res) => {
const socket = socketOf(req);
const { criteria = {} } = req.body || {};
try {
const cfg = await resolveRRConfigHttp(req);
const result = await lookupApi.combinedSearch(socket, criteria, cfg);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /lookup/combined-search failed", { err: err.message });
return fail(res, err);
const bodyshopId = requireBodyshopId(req);
const result = await lookupApi.combinedSearch({ bodyshopId, ...req.body });
return ok(res, { data: result.data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/lookup/combined-search failed", { error: e.message });
return fail(res, e);
}
});
// -------------------- Orchestrated export --------------------
// --- export orchestrator ---
router.post("/rr/export/job", async (req, res) => {
const socket = socketOf(req);
const logger = (level, message, ctx) => RRLogger(socket)(level, message, ctx);
try {
const bodyshopId = requireBodyshopId(req);
const { job, options = {} } = req.body || {};
try {
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
const cfg = await resolveRRConfigHttp(req);
const result = await exportJobToRR(socket, job, cfg, options);
return ok(res, result);
} catch (err) {
RRLogger(socket, "error", "RR /export/job failed", { err: err.message });
return fail(res, err);
}
});
// -------------------- Diagnostics --------------------
router.get("/rr/actions", (_req, res) => {
try {
return ok(res, { actions: listActions() });
} catch (err) {
return fail(res, err);
}
});
router.get("/rr/templates/verify", async (_req, res) => {
try {
const issues = await verifyTemplatesExist();
return ok(res, { ok: issues.length === 0, issues });
} catch (err) {
return fail(res, err);
const data = await exportJobToRR({ bodyshopId, job, logger, ...options });
return ok(res, { data });
} catch (e) {
RRLogger(socket)("error", "RR /rr/export/job failed", { error: e.message });
return fail(res, e);
}
});

11
server/rr/withClient.js Normal file
View File

@@ -0,0 +1,11 @@
const { getRRConfigForBodyshop } = require("./rr-config");
const { makeRRClient } = require("./rr-client");
const logger = require("../utils/logger");
async function withClient(bodyshopId, fn) {
const routing = await getRRConfigForBodyshop(bodyshopId);
const client = makeRRClient({ logger });
return fn(client, routing);
}
module.exports = { withClient };

View File

@@ -1,15 +0,0 @@
<rey_RomeCustServVehCombReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
<!-- NOTE: ApplicationArea is injected by buildStarEnvelope(); do not include it here. -->
<CustServVehCombReq>
<QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}>
{{#Customer.PhoneNumber}}<Phone Num="{{Customer.PhoneNumber}}"/>{{/Customer.PhoneNumber}}
{{#Customer.FirstName}}<FirstName>{{Customer.FirstName}}</FirstName>{{/Customer.FirstName}}
{{#Customer.LastName}}<LastName>{{Customer.LastName}}</LastName>{{/Customer.LastName}}
{{#Customer.EmailAddress}}<EMail>{{Customer.EmailAddress}}</EMail>{{/Customer.EmailAddress}}
{{#Vehicle.VIN}}<VIN>{{Vehicle.VIN}}</VIN>{{/Vehicle.VIN}}
{{#Vehicle.LicensePlate}}<LicensePlate>{{Vehicle.LicensePlate}}</LicensePlate>{{/Vehicle.LicensePlate}}
</QueryData>
</CustServVehCombReq>
</rey_RomeCustServVehCombReq>

View File

@@ -1,123 +0,0 @@
<rey_RomeCreateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
<BSMRepairOrderReq>
<RepairOrder>
<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>
{{#DmsRepairOrderId}}<DmsRepairOrderId>{{DmsRepairOrderId}}</DmsRepairOrderId>{{/DmsRepairOrderId}}
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
{{#ProfitCenter}}<ProfitCenter>{{ProfitCenter}}</ProfitCenter>{{/ProfitCenter}}
{{#ROType}}<ROType>{{ROType}}</ROType>{{/ROType}}
{{#Status}}<Status>{{Status}}</Status>{{/Status}}
{{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
{{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
<Customer>
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
{{#Address}}
<Address>
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#City}}<City>{{City}}</City>{{/City}}
{{#State}}<State>{{State}}</State>{{/State}}
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
</Address>
{{/Address}}
</Customer>
<ServiceVehicle>
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
</ServiceVehicle>
{{#JobLines}}
<JobLine>
<Sequence>{{Sequence}}</Sequence>
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
{{#LineType}}<LineType>{{LineType}}</LineType>{{/LineType}}
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
{{#Taxes}}
<Taxes>
{{#Items}}
<Tax>
<Code>{{Code}}</Code>
<Amount>{{Amount}}</Amount>
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
</Tax>
{{/Items}}
</Taxes>
{{/Taxes}}
</JobLine>
{{/JobLines}}
{{#Totals}}
<Totals>
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
{{#DiscountTotal}}<DiscountTotal>{{DiscountTotal}}</DiscountTotal>{{/DiscountTotal}}
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
<GrandTotal>{{GrandTotal}}</GrandTotal>
</Totals>
{{/Totals}}
{{#Payments}}
<Payments>
{{#Items}}
<Payment>
<PayerType>{{PayerType}}</PayerType>
{{#PayerName}}<PayerName>{{PayerName}}</PayerName>{{/PayerName}}
<Amount>{{Amount}}</Amount>
{{#Method}}<Method>{{Method}}</Method>{{/Method}}
{{#Reference}}<Reference>{{Reference}}</Reference>{{/Reference}}
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
</Payment>
{{/Items}}
</Payments>
{{/Payments}}
{{#Insurance}}
<Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
{{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
</Insurance>
{{/Insurance}}
{{#Notes}}
<Notes>
{{#Items}}<Note>{{.}}</Note>{{/Items}}
</Notes>
{{/Notes}}
</RepairOrder>
</BSMRepairOrderReq>
</rey_RomeCreateBSMRepairOrderReq>

View File

@@ -1,15 +0,0 @@
<rey_RomeGetAdvisorsReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
<GetAdvisorsReq>
<QueryData{{#SearchCriteria.MaxResults}} MaxRecs="{{SearchCriteria.MaxResults}}"{{/SearchCriteria.MaxResults}}>
{{#SearchCriteria.AdvisorId}}<AdvisorID>{{SearchCriteria.AdvisorId}}</AdvisorID>{{/SearchCriteria.AdvisorId}}
{{#SearchCriteria.FirstName}}<FirstName>{{SearchCriteria.FirstName}}</FirstName>{{/SearchCriteria.FirstName}}
{{#SearchCriteria.LastName}}<LastName>{{SearchCriteria.LastName}}</LastName>{{/SearchCriteria.LastName}}
{{#SearchCriteria.Department}}<Department>{{SearchCriteria.Department}}</Department>{{/SearchCriteria.Department}}
{{#SearchCriteria.Status}}<Status>{{SearchCriteria.Status}}</Status>{{/SearchCriteria.Status}}
{{#SearchCriteria.IncludeInactive}}<IncludeInactive>{{SearchCriteria.IncludeInactive}}</IncludeInactive>{{/SearchCriteria.IncludeInactive}}
{{#SearchCriteria.PageNumber}}<PageNumber>{{SearchCriteria.PageNumber}}</PageNumber>{{/SearchCriteria.PageNumber}}
{{#SearchCriteria.SortBy}}<SortBy>{{SearchCriteria.SortBy}}</SortBy>{{/SearchCriteria.SortBy}}
{{#SearchCriteria.SortDirection}}<SortDirection>{{SearchCriteria.SortDirection}}</SortDirection>{{/SearchCriteria.SortDirection}}
</QueryData>
</GetAdvisorsReq>
</rey_RomeGetAdvisorsReq>

View File

@@ -1,25 +0,0 @@
<rey_RomeGetPartsReq xmlns="{{STAR_NS}}" revision="1.0">
<GetPartReq>
<QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}{{#PageNumber}} Page="{{PageNumber}}"{{/PageNumber}}>
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#Vendor}}<Vendor>{{Vendor}}</Vendor>{{/Vendor}}
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
{{#Brand}}<Brand>{{Brand}}</Brand>{{/Brand}}
{{#IsOEM}}<IsOEM>{{IsOEM}}</IsOEM>{{/IsOEM}}
{{#IsAftermarket}}<IsAftermarket>{{IsAftermarket}}</IsAftermarket>{{/IsAftermarket}}
{{#InStock}}<InStock>{{InStock}}</InStock>{{/InStock}}
{{#Warehouse}}<Warehouse>{{Warehouse}}</Warehouse>{{/Warehouse}}
{{#Location}}<Location>{{Location}}</Location>{{/Location}}
{{#MinPrice}}<MinPrice>{{MinPrice}}</MinPrice>{{/MinPrice}}
{{#MaxPrice}}<MaxPrice>{{MaxPrice}}</MaxPrice>{{/MaxPrice}}
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
{{#SearchMode}}<SearchMode>{{SearchMode}}</SearchMode>{{/SearchMode}}
{{#SortBy}}<SortBy>{{SortBy}}</SortBy>{{/SortBy}}
{{#SortDirection}}<SortDirection>{{SortDirection}}</SortDirection>{{/SortDirection}}
</QueryData>
</GetPartReq>
</rey_RomeGetPartsReq>

View File

@@ -1,63 +0,0 @@
<rey_RomeCustomerInsertReq xmlns="{{STAR_NS}}" revision="1.0">
<CustomerInsertReq>
<Customer>
{{#CustomerNumber}}<CustomerNumber>{{CustomerNumber}}</CustomerNumber>{{/CustomerNumber}}
{{#CustomerType}}<CustomerType>
{{CustomerType}}</CustomerType>{{/CustomerType}}
<CustomerName>{{CustomerName}}</CustomerName>
{{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
{{#Language}}<Language>{{Language}}</Language>{{/Language}}
{{#GroupName}}<GroupName>{{GroupName}}</GroupName>{{/GroupName}}
{{#TaxExempt}}<TaxExempt>{{TaxExempt}}</TaxExempt>{{/TaxExempt}}
{{#DiscountLevel}}<DiscountLevel>{{DiscountLevel}}</DiscountLevel>{{/DiscountLevel}}
{{#Active}}<Active>{{Active}}</Active>{{/Active}}
{{#Addresses}}
<Address>
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#City}}<City>{{City}}</City>{{/City}}
{{#State}}<State>{{State}}</State>{{/State}}
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
</Address>
{{/Addresses}}
{{#Phones}}
<Phone>
<Type>{{Type}}</Type>
<Number>{{Number}}</Number>
{{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
</Phone>
{{/Phones}}
{{#Emails}}
<Email>
<Type>{{Type}}</Type>
<Address>{{Address}}</Address>
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
</Email>
{{/Emails}}
{{#Insurance}}
<Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
</Insurance>
{{/Insurance}}
{{#LinkedAccounts}}
<LinkedAccount>
<Type>{{Type}}</Type>
<AccountNumber>{{AccountNumber}}</AccountNumber>
{{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
</LinkedAccount>
{{/LinkedAccounts}}
{{#Notes}}
<Notes>
{{#Items}}<Note>{{.}}</Note>{{/Items}}
</Notes>
{{/Notes}}
</Customer>
</CustomerInsertReq>
</rey_RomeCustomerInsertReq>

View File

@@ -1,57 +0,0 @@
<rey_RomeServVehicleInsertReq xmlns="{{STAR_NS}}" revision="1.0">
<ServVehicleInsertReq>
<ServiceVehicle>
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
{{#UnitNumber}}<UnitNumber>{{UnitNumber}}</UnitNumber>{{/UnitNumber}}
{{#StockNumber}}<StockNumber>{{StockNumber}}</StockNumber>{{/StockNumber}}
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#Trim}}<Trim>{{Trim}}</Trim>{{/Trim}}
{{#BodyStyle}}<BodyStyle>{{BodyStyle}}</BodyStyle>{{/BodyStyle}}
{{#Transmission}}<Transmission>{{Transmission}}</Transmission>{{/Transmission}}
{{#Engine}}<Engine>{{Engine}}</Engine>{{/Engine}}
{{#FuelType}}<FuelType>{{FuelType}}</FuelType>{{/FuelType}}
{{#DriveType}}<DriveType>{{DriveType}}</DriveType>{{/DriveType}}
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
{{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
{{#RegistrationExpiry}}<RegistrationExpiry>{{RegistrationExpiry}}</RegistrationExpiry>{{/RegistrationExpiry}}
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
{{#OdometerUnits}}<OdometerUnits>
{{OdometerUnits}}</OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
{{#InServiceDate}}<InServiceDate>{{InServiceDate}}</InServiceDate>{{/InServiceDate}}
{{#Ownership}}
<Ownership>
{{#OwnerId}}<OwnerId>{{OwnerId}}</OwnerId>{{/OwnerId}}
{{#OwnerName}}<OwnerName>{{OwnerName}}</OwnerName>{{/OwnerName}}
{{#OwnershipType}}<OwnershipType>
{{OwnershipType}}</OwnershipType>{{/OwnershipType}}
</Ownership>
{{/Ownership}}
{{#Insurance}}
<Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
</Insurance>
{{/Insurance}}
{{#Warranty}}
<Warranty>
{{#WarrantyCompany}}<WarrantyCompany>{{WarrantyCompany}}</WarrantyCompany>{{/WarrantyCompany}}
{{#WarrantyNumber}}<WarrantyNumber>{{WarrantyNumber}}</WarrantyNumber>{{/WarrantyNumber}}
{{#WarrantyType}}<WarrantyType>{{WarrantyType}}</WarrantyType>{{/WarrantyType}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
</Warranty>
{{/Warranty}}
{{#VehicleNotes}}
<Notes>
{{#Items}}<Note>{{.}}</Note>{{/Items}}
</Notes>
{{/VehicleNotes}}
</ServiceVehicle>
</ServVehicleInsertReq>
</rey_RomeServVehicleInsertReq>

View File

@@ -1,83 +0,0 @@
<rey_RomeCustomerUpdateReq xmlns="{{STAR_NS}}" revision="1.0">
<CustomerUpdateReq>
<Customer>
<CustomerId>{{CustomerId}}</CustomerId>
{{#CustomerType}}<CustomerType>
{{CustomerType}}</CustomerType>{{/CustomerType}}
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
{{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
{{#PreferredName}}<PreferredName>{{PreferredName}}</PreferredName>{{/PreferredName}}
{{#Language}}<Language>{{Language}}</Language>{{/Language}}
{{#GroupName}}<GroupName>{{GroupName}}</GroupName>{{/GroupName}}
{{#TaxExempt}}<TaxExempt>{{TaxExempt}}</TaxExempt>{{/TaxExempt}}
{{#DiscountLevel}}<DiscountLevel>{{DiscountLevel}}</DiscountLevel>{{/DiscountLevel}}
{{#Active}}<Active>{{Active}}</Active>{{/Active}}
{{#Addresses}}
<Address>
{{#AddressId}}<AddressId>{{AddressId}}</AddressId>{{/AddressId}}
{{#Type}}<Type>
{{Type}}</Type>{{/Type}}
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#City}}<City>{{City}}</City>{{/City}}
{{#State}}<State>{{State}}</State>{{/State}}
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
{{#IsPrimary}}<IsPrimary>{{IsPrimary}}</IsPrimary>{{/IsPrimary}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</Address>
{{/Addresses}}
{{#Phones}}
<Phone>
{{#PhoneId}}<PhoneId>{{PhoneId}}</PhoneId>{{/PhoneId}}
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
{{#Number}}<Number>{{Number}}</Number>{{/Number}}
{{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</Phone>
{{/Phones}}
{{#Emails}}
<Email>
{{#EmailId}}<EmailId>{{EmailId}}</EmailId>{{/EmailId}}
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
{{#Address}}<Address>{{Address}}</Address>{{/Address}}
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</Email>
{{/Emails}}
{{#DriverLicense}}
<DriverLicense>
{{#LicenseNumber}}<LicenseNumber>{{LicenseNumber}}</LicenseNumber>{{/LicenseNumber}}
{{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
</DriverLicense>
{{/DriverLicense}}
{{#Insurance}}
<Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
</Insurance>
{{/Insurance}}
{{#LinkedAccounts}}
<LinkedAccount>
{{#AccountId}}<AccountId>{{AccountId}}</AccountId>{{/AccountId}}
<Type>{{Type}}
</Type>
{{#AccountNumber}}<AccountNumber>{{AccountNumber}}</AccountNumber>{{/AccountNumber}}
{{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</LinkedAccount>
{{/LinkedAccounts}}
{{#Notes}}
<Notes>
{{#Items}}<Note>{{.}}</Note>{{/Items}}
</Notes>
{{/Notes}}
</Customer>
</CustomerUpdateReq>
</rey_RomeCustomerUpdateReq>

View File

@@ -1,123 +0,0 @@
<rey_RomeUpdateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
<BSMRepairOrderReq>
<RepairOrder>
<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>
{{#DmsRepairOrderId}}<DmsRepairOrderId>{{DmsRepairOrderId}}</DmsRepairOrderId>{{/DmsRepairOrderId}}
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
{{#ProfitCenter}}<ProfitCenter>{{ProfitCenter}}</ProfitCenter>{{/ProfitCenter}}
{{#ROType}}<ROType>{{ROType}}</ROType>{{/ROType}}
{{#Status}}<Status>{{Status}}</Status>{{/Status}}
{{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
{{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
<Customer>
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
{{#Address}}
<Address>
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#City}}<City>{{City}}</City>{{/City}}
{{#State}}<State>{{State}}</State>{{/State}}
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
</Address>
{{/Address}}
</Customer>
<ServiceVehicle>
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
</ServiceVehicle>
{{#JobLines}}
<JobLine>
<Sequence>{{Sequence}}</Sequence>
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
{{#LineType}}<LineType>{{LineType}}</LineType>{{/LineType}}
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
{{#Taxes}}
<Taxes>
{{#Items}}
<Tax>
<Code>{{Code}}</Code>
<Amount>{{Amount}}</Amount>
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
</Tax>
{{/Items}}
</Taxes>
{{/Taxes}}
</JobLine>
{{/JobLines}}
{{#Totals}}
<Totals>
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
{{#DiscountTotal}}<DiscountTotal>{{DiscountTotal}}</DiscountTotal>{{/DiscountTotal}}
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
<GrandTotal>{{GrandTotal}}</GrandTotal>
</Totals>
{{/Totals}}
{{#Payments}}
<Payments>
{{#Items}}
<Payment>
<PayerType>{{PayerType}}</PayerType>
{{#PayerName}}<PayerName>{{PayerName}}</PayerName>{{/PayerName}}
<Amount>{{Amount}}</Amount>
{{#Method}}<Method>{{Method}}</Method>{{/Method}}
{{#Reference}}<Reference>{{Reference}}</Reference>{{/Reference}}
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
</Payment>
{{/Items}}
</Payments>
{{/Payments}}
{{#Insurance}}
<Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
{{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
</Insurance>
{{/Insurance}}
{{#Notes}}
<Notes>
{{#Items}}<Note>{{.}}</Note>{{/Items}}
</Notes>
{{/Notes}}
</RepairOrder>
</BSMRepairOrderReq>
</rey_RomeUpdateBSMRepairOrderReq>

View File

@@ -5,7 +5,6 @@ const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const { exportJobToRR } = require("../rr/rr-job-export");
const lookupApi = require("../rr/rr-lookup");
const { getRRConfigForBodyshop } = require("../rr/rr-config");
const redisSocketEvents = ({
io,
@@ -341,94 +340,122 @@ const redisSocketEvents = ({
});
};
// Reynolds & Reynolds socket events (uses new client-backed ops)
const registerRREvents = (socket) => {
const log = (level, message, ctx) => RRLogger(socket)(level, message, ctx);
const resolveBodyshopId = (payload, job) =>
payload?.bodyshopId || socket.bodyshopId || job?.shopid || job?.bodyshopId;
const resolveJobId = (explicitJobId, payload, job) =>
explicitJobId ||
payload?.jobid ||
payload?.txEnvelope?.jobid ||
job?.id ||
payload?.txEnvelope?.job?.id ||
null;
// Orchestrated Export (Customer → Vehicle → Repair Order)
socket.on("rr-export-job", async (payload = {}) => {
try {
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, options }
// Prefer direct job, otherwise try txEnvelope.job
const job = payload.job || payload.txEnvelope?.job;
const options = payload.options || payload.txEnvelope?.options || {};
// Resolve per-bodyshop RR config strictly from DB:
const bodyshopId = payload.bodyshopId || socket.bodyshopId || job?.shopid;
const cfg = await getRRConfigForBodyshop(bodyshopId);
const bodyshopId = resolveBodyshopId(payload, job);
const jobid = resolveJobId(payload.jobid, payload, job);
if (!job) {
RRLogger(socket, "error", "RR export missing job payload");
log("error", "RR export missing job payload", { jobid });
return;
}
if (!bodyshopId) {
log("error", "RR export missing bodyshopId", { jobid });
return;
}
const result = await exportJobToRR(socket, job, cfg, options);
// Broadcast to bodyshop room for UI to pick up
const room = getBodyshopRoom(socket.bodyshopId);
io.to(room).emit("rr-export-job:result", { jobid: job.id, result });
const result = await exportJobToRR({ bodyshopId, job, logger: log, ...options });
// Broadcast keyed by bodyshop + include jobid
const room = getBodyshopRoom(bodyshopId);
io.to(room).emit("rr-export-job:result", { jobid, bodyshopId, result });
} catch (error) {
RRLogger(socket, "error", `Error during RR export: ${error.message}`);
logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack });
const jobid = resolveJobId(payload?.jobid, payload, payload?.job || payload?.txEnvelope?.job);
log("error", `Error during RR export: ${error.message}`, { jobid, stack: error.stack });
logger.log("rr-job-export-error", "error", null, null, { jobid, message: error.message, stack: error.stack });
}
});
// Combined search
// Combined search (customer/vehicle)
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
try {
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
cb?.(data);
const bodyshopId = resolveBodyshopId({ bodyshopId: params?.bodyshopId }, null);
const resolvedJobId = resolveJobId(jobid, { jobid }, null);
if (!bodyshopId) throw new Error("Missing bodyshopId");
const res = await lookupApi.combinedSearch({ bodyshopId, ...(params || {}) });
cb?.({ jobid: resolvedJobId, data: res?.data ?? res });
} catch (e) {
RRLogger(socket, "error", `RR combined lookup error: ${e.message}`);
cb?.(null);
log("error", `RR combined lookup error: ${e.message}`, { jobid });
cb?.({ jobid, error: e.message });
}
});
// Get Advisors
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
try {
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
cb?.(data);
const bodyshopId = resolveBodyshopId({ bodyshopId: params?.bodyshopId }, null);
const resolvedJobId = resolveJobId(jobid, { jobid }, null);
if (!bodyshopId) throw new Error("Missing bodyshopId");
const res = await lookupApi.getAdvisors({ bodyshopId, ...(params || {}) });
cb?.({ jobid: resolvedJobId, data: res?.data ?? res });
} catch (e) {
RRLogger(socket, "error", `RR get advisors error: ${e.message}`);
cb?.(null);
log("error", `RR get advisors error: ${e.message}`, { jobid });
cb?.({ jobid, error: e.message });
}
});
// Get Parts
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
try {
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
const data = await lookupApi.getParts(socket, params || {}, cfg);
cb?.(data);
const bodyshopId = resolveBodyshopId({ bodyshopId: params?.bodyshopId }, null);
const resolvedJobId = resolveJobId(jobid, { jobid }, null);
if (!bodyshopId) throw new Error("Missing bodyshopId");
const res = await lookupApi.getParts({ bodyshopId, ...(params || {}) });
cb?.({ jobid: resolvedJobId, data: res?.data ?? res });
} catch (e) {
RRLogger(socket, "error", `RR get parts error: ${e.message}`);
cb?.(null);
log("error", `RR get parts error: ${e.message}`, { jobid });
cb?.({ jobid, error: e.message });
}
});
// (Optional) Selected customer — only keep this if you actually implement it for RR
// Optional: Selected customer — currently a no-op for RR
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId } = {}) => {
try {
RRLogger(socket, "info", "rr-selected-customer not implemented for RR (no-op)", {
jobid,
const resolvedJobId = resolveJobId(jobid, { jobid }, null);
log("info", "rr-selected-customer not implemented for RR (no-op)", {
jobid: resolvedJobId,
selectedCustomerId
});
// If later you add support, call your implementation here.
} catch (error) {
RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`);
logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack });
}
});
// Calculate allocations (unchanged — CDK utility)
// Calculate allocations (CDK utility unchanged)
socket.on("rr-calculate-allocations", async (jobid, callback) => {
try {
const allocations = await CdkCalculateAllocations(socket, jobid);
callback(allocations);
const resolvedJobId = resolveJobId(jobid, { jobid }, null);
const allocations = await CdkCalculateAllocations(socket, resolvedJobId);
callback({ jobid: resolvedJobId, allocations });
} catch (error) {
RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`);
logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack });
log("error", `Error during RR calculate allocations: ${error.message}`, { jobid, stack: error.stack });
logger.log("rr-calc-allocations-error", "error", null, null, {
jobid,
message: error.message,
stack: error.stack
});
callback?.({ jobid, error: error.message });
}
});
};
// Call Handlers
registerRoomAndBroadcastEvents(socket);
registerUpdateEvents(socket);