From 4b83330db98df376b7f25a54dbec73bb66e6f45e Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 23 Jun 2025 14:00:58 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - Checkpoint --- client/package-lock.json | 94 ++++++------ client/package.json | 6 +- package-lock.json | 16 +- package.json | 4 +- ...rtsManagementVehicleDamageEstimateAddRq.js | 144 ++++++++++++++++-- 5 files changed, 193 insertions(+), 71 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 6d710e174..b357aab33 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -21,7 +21,7 @@ "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.46.0", - "@sentry/react": "^9.30.0", + "@sentry/react": "^9.31.0", "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.3.1", "@tanem/react-nprogress": "^5.0.53", @@ -91,7 +91,7 @@ "@ant-design/icons": "^6.0.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.27.1", - "@dotenvx/dotenvx": "^1.44.2", + "@dotenvx/dotenvx": "^1.45.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.29.0", @@ -100,7 +100,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^4.6.0", "browserslist": "^4.25.0", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.4.1", @@ -2585,9 +2585,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.44.2", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.2.tgz", - "integrity": "sha512-2C44+G2dch4cB6zw7+oGQ9VcFQuuVhc5xOzfVvY7iUEj2PRhiVMIB6SpNMK1V5TvpdqrAqCYFjclK18Mh9vwNQ==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.45.1.tgz", + "integrity": "sha512-wKHPD+/NMMJVBPg3i98uD9jsURDy+Ck6RQRiWf39TlOAzC+Ge1FkmDk3sgeljYZxA3qF6E7SJmvRqC70XQuuVA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3884,9 +3884,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", "dev": true, "license": "MIT" }, @@ -4466,50 +4466,50 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.30.0.tgz", - "integrity": "sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.31.0.tgz", + "integrity": "sha512-rviu/jUmeQbY4rSO8l4pubOtRIhFtH5Gu/ryRNMTlpJRdomp4uxddqthHUDH5g6xCXZsMTyJEIdx0aTqbgr/GQ==", "license": "MIT", "dependencies": { - "@sentry/core": "9.30.0" + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.30.0.tgz", - "integrity": "sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.31.0.tgz", + "integrity": "sha512-Ygi/8UZ7p2B4DhXQjZDtOc45vNUHkfk2XETBTBGkByEQkE8vygzSiKhgRcnVpzwq+8xKFMRy+PxvpcCo+PNQew==", "license": "MIT", "dependencies": { - "@sentry/core": "9.30.0" + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.30.0.tgz", - "integrity": "sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.31.0.tgz", + "integrity": "sha512-V5rvcO/xSj8JMw4ZnZT2cBYC+UOuIiZ2Flj4EoIurxMrTgowE1uMXUBA32EBfuB5/vQSJXB6W5uAudhk7LjBPQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.30.0", - "@sentry/core": "9.30.0" + "@sentry-internal/browser-utils": "9.31.0", + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.30.0.tgz", - "integrity": "sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.31.0.tgz", + "integrity": "sha512-VGqfvQCIuXQZeecrBf8bd4sj8lYGzUA/2CffTAkad1nB1Onyz0Kzo54qLWemivCxA3ufHf6DCpNA3Loa/0ywFQ==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.30.0", - "@sentry/core": "9.30.0" + "@sentry-internal/replay": "9.31.0", + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" @@ -4525,16 +4525,16 @@ } }, "node_modules/@sentry/browser": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.30.0.tgz", - "integrity": "sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.31.0.tgz", + "integrity": "sha512-DzG72JJTqHzE0Qo2fHeHm3xgFs97InaSQStmTMxOA59yPqvAXbweNPcsgCNu1q76+jZyaJcoy1qOwahnLuEVDg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.30.0", - "@sentry-internal/feedback": "9.30.0", - "@sentry-internal/replay": "9.30.0", - "@sentry-internal/replay-canvas": "9.30.0", - "@sentry/core": "9.30.0" + "@sentry-internal/browser-utils": "9.31.0", + "@sentry-internal/feedback": "9.31.0", + "@sentry-internal/replay": "9.31.0", + "@sentry-internal/replay-canvas": "9.31.0", + "@sentry/core": "9.31.0" }, "engines": { "node": ">=18" @@ -4911,22 +4911,22 @@ } }, "node_modules/@sentry/core": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.30.0.tgz", - "integrity": "sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.31.0.tgz", + "integrity": "sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.30.0.tgz", - "integrity": "sha512-asA49AkZ/g9CCeW0eA0Ent0DF60S4k2IHxbu+Q1mqgbRRmbn859oL2Bgsu/EvzWf5edeQtuUml8LIo4YoFwfMA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.31.0.tgz", + "integrity": "sha512-cZT/AwRiawRED7pB4Ug6ZRbcWd92HQxOPc12KKe5ZUQFEc9jUqH6HqwzQUSMzkg86NrE9Hc6XXga+JZ3Q1Lzow==", "license": "MIT", "dependencies": { - "@sentry/browser": "9.30.0", - "@sentry/core": "9.30.0", + "@sentry/browser": "9.31.0", + "@sentry/core": "9.31.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -5819,16 +5819,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz", - "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.11", + "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, diff --git a/client/package.json b/client/package.json index 955a343b2..67de920bf 100644 --- a/client/package.json +++ b/client/package.json @@ -20,7 +20,7 @@ "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.46.0", - "@sentry/react": "^9.30.0", + "@sentry/react": "^9.31.0", "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.3.1", "@tanem/react-nprogress": "^5.0.53", @@ -131,7 +131,7 @@ "@ant-design/icons": "^6.0.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.27.1", - "@dotenvx/dotenvx": "^1.44.2", + "@dotenvx/dotenvx": "^1.45.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.29.0", @@ -140,7 +140,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^4.6.0", "browserslist": "^4.25.0", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.4.1", diff --git a/package-lock.json b/package-lock.json index e598c0600..58ac9324e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "aws4": "^1.13.2", "axios": "^1.10.0", "better-queue": "^3.8.12", - "bullmq": "^5.54.3", + "bullmq": "^5.56.0", "chart.js": "^4.5.0", "cloudinary": "^2.7.0", "compression": "^1.8.0", @@ -72,7 +72,7 @@ "globals": "^15.15.0", "mock-require": "^3.0.3", "p-limit": "^3.1.0", - "prettier": "^3.5.3", + "prettier": "^3.6.0", "supertest": "^7.1.1", "vitest": "^3.2.4" }, @@ -4674,9 +4674,9 @@ } }, "node_modules/bullmq": { - "version": "5.54.3", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.54.3.tgz", - "integrity": "sha512-MVK2pOkB3hvrIcubwI8dS4qWHJLNKakKPpgRBTw91sIpPZArmvZ4t2hvryyEaJXJbAS/JHd6pKYOUd+RGRkWQQ==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.56.0.tgz", + "integrity": "sha512-j5ct2tdc9M8PKcjhJw+euO24BsO1wXBAkNGXYI1R1qvh7FvRldZ5wtLixLWqQ4/crafj0Vrwi+y1kXFXMwBJFA==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -9825,9 +9825,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz", + "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 656a0c36c..61eaf07d8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "aws4": "^1.13.2", "axios": "^1.10.0", "better-queue": "^3.8.12", - "bullmq": "^5.54.3", + "bullmq": "^5.56.0", "chart.js": "^4.5.0", "cloudinary": "^2.7.0", "compression": "^1.8.0", @@ -79,7 +79,7 @@ "globals": "^15.15.0", "mock-require": "^3.0.3", "p-limit": "^3.1.0", - "prettier": "^3.5.3", + "prettier": "^3.6.0", "supertest": "^7.1.1", "vitest": "^3.2.4" } diff --git a/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js index b6bff71b4..fde4b2082 100644 --- a/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js @@ -4,6 +4,9 @@ const xml2js = require("xml2js"); const client = require("../../graphql-client/graphql-client").client; +// Defaults +const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; // Default status if not found in bodyshop + // GraphQL statements const INSERT_JOB_WITH_LINES = ` mutation InsertJob($job: jobs_insert_input!) { @@ -14,6 +17,30 @@ const INSERT_JOB_WITH_LINES = ` } `; +const GET_BODYSHOP_STATUS = ` + query GetBodyshopStatus($id: uuid!) { + bodyshops_by_pk(id: $id) { + md_order_statuses + } + } +`; + +const INSERT_OWNER = ` + mutation InsertOwner($owner: owners_insert_input!) { + insert_owners_one(object: $owner) { + id + } + } +`; + +// Do they call the add call first, future ones will be updates, we need to upcycle. Or we need to send a new add request, we treat it as an upsert. + +/** + * Handles the VehicleDamageEstimateAddRq XML request from parts management. + * @param req + * @param res + * @returns {Promise<*>} + */ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { const { logger } = req; const xml = req.body; @@ -23,9 +50,11 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { try { payload = await xml2js.parseStringPromise(xml, { explicitArray: false, - tagNameProcessors: [xml2js.processors.stripPrefix] + tagNameProcessors: [xml2js.processors.stripPrefix], + attrNameProcessors: [xml2js.processors.stripPrefix] + // ignoreAttrs: false, + // xmlns: false }); - logger.log("parts-xml-parse", "debug", null, null, { success: true }); } catch (err) { logger.log("parts-xml-parse-error", "error", null, null, { error: err }); return res.status(400).send("Invalid XML"); @@ -43,6 +72,14 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { if (!shopId) throw { status: 400, message: "Missing in XML" }; const { RefClaimNum } = rq; + let defaultStatus = FALLBACK_DEFAULT_ORDER_STATUS; + + try { + const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId }); + defaultStatus = bodyshop_by_pk?.md_order_statuses?.default_open || defaultStatus; + } catch (err) { + logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err }); + } // ── DOCUMENT INFO ────────────────────────────────────────────────────────── const doc = rq.DocumentInfo || {}; const comment = doc.Comment || null; @@ -55,6 +92,68 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { const category = doc.DocumentType || null; const classType = doc.DocumentStatus || null; + // ── PARTS TAX RATES STRUCTURE ─────────────────────────────────────────────── + // Known rate types that map to your parts_tax_rates keys + const knownPartRateTypes = [ + "PAA", + "PAC", + "PAG", + "PAL", + "PAM", + "PAN", + "PAO", + "PAP", + "PAR", + "PAS", + "PASL", + "CCC", + "CCD", + "CCF", + "CCM", + "CCDR" + ]; + + const profile = rq.ProfileInfo || {}; + const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; + + const parts_tax_rates = {}; + + for (const code of knownPartRateTypes) { + const rateInfo = rateInfos.find((r) => (r?.RateType || "").toUpperCase() === code); + if (!rateInfo) { + parts_tax_rates[code] = {}; + continue; + } + + const taxInfo = rateInfo.TaxInfo; + const taxTier = taxInfo?.TaxTierInfo; + + // Try to find Percentage first + let percentage = parseFloat(taxTier?.Percentage ?? "NaN"); + if (isNaN(percentage)) { + // fallback to RateTierInfo.Rate if that's where it might be + const tierRate = Array.isArray(rateInfo.RateTierInfo) + ? rateInfo.RateTierInfo[0]?.Rate + : rateInfo.RateTierInfo?.Rate; + + percentage = parseFloat(tierRate ?? "NaN"); + } + + // Still no tax rate? fallback to null object + if (isNaN(percentage)) { + parts_tax_rates[code] = {}; + continue; + } + + parts_tax_rates[code] = { + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: percentage / 100 + }; + } + // ── EVENT INFO ────────────────────────────────────────────────────────────── const ev = rq.EventInfo || {}; const asgn = ev.AssignmentEvent || {}; @@ -106,8 +205,7 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { const est_ct_fn = est_fn; const est_ct_ln = est_ln; - // TODO: SHould be the estimator insurance company name est_co_name - const est_aff = rq.AdminInfo?.Estimator?.Affiliation || null; + const est_co_nm = rq.AdminInfo?.Estimator?.Affiliation || null; const estComms = Array.isArray(estParty.ContactInfo?.Communications) ? estParty.ContactInfo.Communications @@ -193,9 +291,34 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { notes: line.LineMemo || null })); + const ownerInput = { + shopid: shopId, + ownr_fn, + ownr_ln, + ownr_co_nm, + ownr_addr1, + ownr_addr2, + ownr_city, + ownr_st, + ownr_zip, + ownr_ctry, + ownr_ph1, + ownr_ph2, + ownr_ea + }; + + let ownerid = null; + try { + const { insert_owners_one } = await client.request(INSERT_OWNER, { owner: ownerInput }); + ownerid = insert_owners_one?.id; + } catch (err) { + logger.log("parts-owner-insert-failed", "warn", null, null, { error: err }); + } + // ── BUILD & INSERT THE JOB ────────────────────────────────────────────────── const jobInput = { shopid: shopId, + ownerid, ro_number: RefClaimNum, // IDs & CIECA metadata @@ -205,10 +328,12 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { category, class: classType, + // tax + parts_tax_rates, + // claim & policy clm_no, - // default job: bodyshop.md_status.default_open - status: status || "OPEN", + status: status || defaultStatus, clm_total: cieca_ttl, policy_no, ded_amt, @@ -238,7 +363,7 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { ownr_ea, // estimator - // est_co_id: est_aff, + est_co_nm, est_ct_fn, est_ct_ln, est_ea, @@ -253,16 +378,13 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { servicing_dealer, servicing_dealer_contact, - // stash any extra CIECA stuff we didn’t map above - production_vars: {}, - // nested relationships vehicle: { data: vehicleData }, joblines: { data: joblinesData } }; - logger.log("parts-insert-job", "debug", null, null, { jobInput }); const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput }); + logger.log("parts-job-created", "info", newJob.id, null); return res.status(200).json({ success: true, jobId: newJob.id });