feature/IO-3255-simplified-parts-management - Checkpoint

This commit is contained in:
Dave Richer
2025-06-23 14:00:58 -04:00
parent b0283f827e
commit 4b83330db9
5 changed files with 193 additions and 71 deletions

View File

@@ -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"
},

View File

@@ -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",

16
package-lock.json generated
View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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 <ShopID> 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 didnt 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 });