feature/IO-3255-simplified-parts-management - Bump deps, add Change Request

This commit is contained in:
Dave Richer
2025-07-07 12:14:23 -04:00
parent 0891c7d4b3
commit 8bc6bea4b2
7 changed files with 242 additions and 113 deletions

View File

@@ -0,0 +1,54 @@
const xml2js = require("xml2js");
/**
* Parses XML string into a JavaScript object.
* @param {string} xml - The XML string to parse.
* @param {object} logger - The logger instance.
* @returns {Promise<object>} The parsed XML object.
* @throws {Error} If XML parsing fails.
*/
const parseXml = async (xml, logger) => {
try {
return await xml2js.parseStringPromise(xml, {
explicitArray: false,
tagNameProcessors: [xml2js.processors.stripPrefix],
attrNameProcessors: [xml2js.processors.stripPrefix]
});
} catch (err) {
logger.log("parts-xml-parse-error", "error", null, null, { error: err });
throw new Error("Invalid XML");
}
};
/**
* Recursively strip `xml2js`-style { _: 'value', $: { ... } } nodes into plain strings.
* @param {*} obj - Parsed XML object
* @returns {*} Normalized object
*/
const normalizeXmlObject = (obj) => {
if (Array.isArray(obj)) {
return obj.map(normalizeXmlObject);
}
if (typeof obj === "object" && obj !== null) {
if (Object.keys(obj).length === 2 && "_" in obj && "$" in obj) {
return normalizeXmlObject(obj._); // unwrap {_:"value",$:{...}} to just "value"
}
if (Object.keys(obj).length === 1 && "_" in obj) {
return normalizeXmlObject(obj._); // unwrap {_:"value"}
}
const normalized = {};
for (const key in obj) {
normalized[key] = normalizeXmlObject(obj[key]);
}
return normalized;
}
return obj;
};
module.exports = {
parseXml,
normalizeXmlObject
};

View File

@@ -1,8 +1,8 @@
// no-dd-sa:javascript-code-style/assignment-name
// CamelCase is used for GraphQL and database fields.
const xml2js = require("xml2js");
const client = require("../../graphql-client/graphql-client").client;
const { parseXml, normalizeXmlObject } = require("./partsManagementUtils");
// GraphQL Queries and Mutations
const {
@@ -34,55 +34,6 @@ const KNOWN_PART_RATE_TYPES = [
"CCM",
"CCDR"
];
/**
* Parses XML string into a JavaScript object.
* @param {string} xml - The XML string to parse.
* @param {object} logger - The logger instance.
* @returns {Promise<object>} The parsed XML object.
* @throws {Error} If XML parsing fails.
*/
const parseXml = async (xml, logger) => {
try {
return await xml2js.parseStringPromise(xml, {
explicitArray: false,
tagNameProcessors: [xml2js.processors.stripPrefix],
attrNameProcessors: [xml2js.processors.stripPrefix]
});
} catch (err) {
logger.log("parts-xml-parse-error", "error", null, null, { error: err });
throw new Error("Invalid XML");
}
};
/**
* Recursively strip `xml2js`-style { _: 'value', $: { ... } } nodes into plain strings.
* @param {*} obj - Parsed XML object
* @returns {*} Normalized object
*/
const normalizeXmlObject = (obj) => {
if (Array.isArray(obj)) {
return obj.map(normalizeXmlObject);
}
if (typeof obj === "object" && obj !== null) {
if (Object.keys(obj).length === 2 && "_" in obj && "$" in obj) {
return normalizeXmlObject(obj._); // unwrap {_:"value",$:{...}} to just "value"
}
if (Object.keys(obj).length === 1 && "_" in obj) {
return normalizeXmlObject(obj._); // unwrap {_:"value"}
}
const normalized = {};
for (const key in obj) {
normalized[key] = normalizeXmlObject(obj[key]);
}
return normalized;
}
return obj;
};
/**
* Fetches the default order status for a bodyshop.
* @param {string} shopId - The bodyshop UUID.
@@ -508,7 +459,7 @@ const insertOwner = async (ownerInput, logger) => {
* @param {object} res - The HTTP response object.
* @returns {Promise<void>}
*/
const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
const vehicleDamageEstimateAddRq = async (req, res) => {
const { logger } = req;
try {
@@ -617,4 +568,4 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
}
};
module.exports = partsManagementVehicleDamageEstimateAddRq;
module.exports = vehicleDamageEstimateAddRq;

View File

@@ -0,0 +1,106 @@
// no-dd-sa:javascript-code-style/assignment-name
// Handler for VehicleDamageEstimateChgRq
const client = require("../../graphql-client/graphql-client").client;
const { parseXml, normalizeXmlObject } = require("./partsManagementUtils");
const {
GET_JOB_BY_CLAIM_OR_DOCID,
UPDATE_JOB_BY_PK,
UPSERT_JOBLINES,
DELETE_JOBLINES_BY_IDS
} = require("./partsManagement.queries");
const findJob = async (claimNum, documentId, logger) => {
try {
const { jobs } = await client.request(GET_JOB_BY_CLAIM_OR_DOCID, { claimNum, documentId });
return jobs?.[0] || null;
} catch (err) {
logger.log("parts-job-lookup-failed", "error", null, null, { error: err });
return null;
}
};
const extractUpdatedJobData = (rq) => {
const doc = rq.DocumentInfo || {};
const claim = rq.ClaimInfo || {};
return {
comment: doc.Comment || null,
clm_no: claim.ClaimNum || null,
status: claim.ClaimStatus || null,
policy_no: claim.PolicyInfo?.PolicyNum || null
};
};
const extractUpdatedJobLines = (addsChgs = {}) => {
const lines = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || []];
return lines.map((line) => ({
line_no: parseInt(line.LineNum, 10),
unq_seq: parseInt(line.UniqueSequenceNum, 10),
status: line.LineStatusCode || null,
line_desc: line.LineDesc || null,
part_type: line.PartInfo?.PartType || null,
part_qty: parseFloat(line.PartInfo?.Quantity || 0),
oem_partno: line.PartInfo?.OEMPartNum || null,
db_price: parseFloat(line.PartInfo?.PartPrice || 0),
act_price: parseFloat(line.PartInfo?.PartPrice || 0),
mod_lbr_ty: line.LaborInfo?.LaborType || null,
mod_lb_hrs: parseFloat(line.LaborInfo?.LaborHours || 0),
lbr_op: line.LaborInfo?.LaborOperation || null,
lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0),
notes: line.LineMemo || null,
manual_line: line.ManualLineInd || null
}));
};
const extractDeletions = (deletions = {}) => {
const lines = Array.isArray(deletions.DamageLineInfo) ? deletions.DamageLineInfo : [deletions.DamageLineInfo || []];
return lines.map((line) => parseInt(line.UniqueSequenceNum, 10)).filter((id) => !isNaN(id));
};
const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
const { logger } = req;
try {
const payload = await parseXml(req.body, logger);
const rq = normalizeXmlObject(payload.VehicleDamageEstimateChgRq);
if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>");
const shopId = rq.ShopID;
const claimNum = rq.ClaimInfo?.ClaimNum;
const documentId = rq.DocumentInfo?.DocumentID;
if (!shopId || !claimNum) return res.status(400).send("Missing ShopID or ClaimNum");
const job = await findJob(claimNum, documentId, logger);
if (!job) return res.status(404).send("Job not found");
const updatedJobData = extractUpdatedJobData(rq);
const updatedLines = extractUpdatedJobLines(rq.AddsChgs);
const deletedLineIds = extractDeletions(rq.Deletions);
await client.request(UPDATE_JOB_BY_PK, { id: job.id, changes: updatedJobData });
if (updatedLines.length > 0) {
await client.request(UPSERT_JOBLINES, {
jobid: job.id,
joblines: updatedLines
});
}
if (deletedLineIds.length > 0) {
await client.request(DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: deletedLineIds });
}
logger.log("parts-job-changed", "info", job.id, null);
return res.status(200).json({ success: true, jobId: job.id });
} catch (err) {
logger.log("parts-chgrq-error", "error", null, null, { error: err });
return res.status(err.status || 500).json({ error: err.message || "Internal error" });
}
};
module.exports = partsManagementVehicleDamageEstimateChgRq;

View File

@@ -1,6 +1,6 @@
const express = require("express");
const router = express.Router();
const bodyParser = require("body-parser"); // Add body-parser dependency
const bodyParser = require("body-parser");
// Pull secrets from env
const { VSSTA_INTEGRATION_SECRET, PARTS_MANAGEMENT_INTEGRATION_SECRET } = process.env;
@@ -17,18 +17,36 @@ if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.len
// Only load Parts Management routes if that secret is set
if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_INTEGRATION_SECRET.length > 0) {
const XML_BODY_LIMIT = "10mb"; // Set a limit for XML body size
const partsManagementProvisioning = require("../integrations/partsManagement/partsManagementProvisioning");
const partsManagementIntegrationMiddleware = require("../middleware/partsManagementIntegrationMiddleware");
const partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq");
const partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/vehicleDamageEstimateAddRq");
const partsManagementVehicleDamageEstimateChqRq = require("../integrations/partsManagement/vehicleDamageEstimateChgRq");
// Add XML parsing middleware for the VehicleDamageEstimateAddRq route
/**
* Route to handle Vehicle Damage Estimate Add Request
*/
router.post(
"/parts-management/VehicleDamageEstimateAddRq",
bodyParser.raw({ type: "application/xml", limit: "10mb" }), // Parse XML body
bodyParser.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body
partsManagementIntegrationMiddleware,
partsManagementVehicleDamageEstimateAddRq
);
/**
* Route to handle Vehicle Damage Estimate Change Request
*/
router.post(
"/parts-management/VehicleDamageEstimateChgRq",
bodyParser.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body
partsManagementIntegrationMiddleware,
partsManagementVehicleDamageEstimateChqRq
);
/**
* Route to handle Parts Management Provisioning
*/
router.post("/parts-management/provision", partsManagementIntegrationMiddleware, partsManagementProvisioning);
} else {
console.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");