From 13cb68b0af7afee97fb1b50b2611f5f1ea576681 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 5 Sep 2025 10:18:03 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - Checkpoint --- .../endpoints/vehicleDamageEstimateAddRq.js | 88 +++++++++++-------- .../endpoints/vehicleDamageEstimateChgRq.js | 28 ++++-- 2 files changed, 71 insertions(+), 45 deletions(-) diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index cc88cf78f..e53724b12 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -1,9 +1,7 @@ -// no-dd-sa:javascript-code-style/assignment-name -// CamelCase is used for GraphQL and database fields. - const client = require("../../../graphql-client/graphql-client").client; const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); +const opCodes = require("./lib/opCodes.json"); // GraphQL Queries and Mutations const { @@ -65,6 +63,7 @@ const extractJobData = (rq) => { const ci = rq.ClaimInfo || {}; return { + driveable: !!rq.VehicleInfo?.Condition?.DrivableInd, shopId: rq.ShopID || rq.shopId, // status: ci.ClaimStatus || null, Proper, setting it default for now refClaimNum: rq.RefClaimNum, @@ -107,8 +106,7 @@ const extractOwnerData = (rq, shopId) => { : [ownerOrClaimant.ContactInfo?.Communications || {}]; for (const c of comms) { - // TODO: Should document this logic. 1 and 2 don't - // typically indicate type in EMS. This makes sense, but good to document. + // -- Document if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone; if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone; if (c.CommQualifier === "EM") ownr_ea = c.CommEmail; @@ -167,7 +165,7 @@ const extractEstimatorData = (rq) => { // : [adjParty.ContactInfo?.Communications || {}]; // // return { -// //TODO: I dont think we display agt_ct_* fields in app. Have they typically been sending data here? +// //TODO (FUTURE): I dont think we display agt_ct_* fields in app. Have they typically been sending data here? // agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null, // agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null, // agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null, @@ -188,8 +186,8 @@ const extractEstimatorData = (rq) => { // // return { // servicing_dealer: rfParty.OrgInfo?.CompanyName || null, -// // TODO: The servicing dealer fields are a relic from synergy for a few folks -// // TODO: I suspect RF data could be ignored since they are the RF. +// // TODO (Future): The servicing dealer fields are a relic from synergy for a few folks +// // TODO (Future): I suspect RF data could be ignored since they are the RF. // servicing_dealer_contact: // rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null // }; @@ -294,10 +292,9 @@ const extractVehicleData = (rq, shopId) => { v_color: exterior.Color?.ColorName || null, v_bstyle: desc.BodyStyle || null, v_engine: desc.EngineDesc || null, - // TODO Need to confirm with exact data, but this is typically a list of options. Not used AFAIK. - v_options: desc.SubModelDesc || null, + // TODO (for future) Need to confirm with exact data, but this is typically a list of options. Not used AFAIK. + // v_options: desc.SubModelDesc || null, v_type: desc.FuelType || null, - // TODO there is a separate driveable flag on the job. v_cond: rq.VehicleInfo?.Condition?.DrivableInd, v_trimcode: desc.TrimCode || null, v_tone: exterior.Tone || null, @@ -345,30 +342,34 @@ const extractJobLines = (rq) => { const lineOut = { ...base }; // Manual line flag coercion - if (line.ManualLineInd !== undefined) { - lineOut.manual_line = - line.ManualLineInd === true || - line.ManualLineInd === 1 || - line.ManualLineInd === "1" || - // TODO: manual line tracks manual in IO or not, this woudl presumably always be false - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y"); - } else { - lineOut.manual_line = null; - } + // if (line.ManualLineInd !== undefined) { + // lineOut.manual_line = + // line.ManualLineInd === true || + // line.ManualLineInd === 1 || + // line.ManualLineInd === "1" || + // // TODO (FUTURE): manual line tracks manual in IO or not, this woudl presumably always be false + // (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y"); + // } else { + // lineOut.manual_line = null; + // } + + // Is set to false because anything coming from the DMS is considered not a manual line, it becomes + // a manual line once it is edited in OUR system. + lineOut.manual_line = false; // Parts (preferred) or Sublet (fallback when no PartInfo) const hasPart = Object.keys(partInfo).length > 0; const hasSublet = Object.keys(subletInfo).length > 0; if (hasPart) { - const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null; lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1; - //TODO: if aftermarket part, we have alt_part_no to capture. - lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null; - //TODO: the Db and act price often are different. These should map back to their EMS equivalents. - lineOut.db_price = isNaN(price) ? 0 : price; - lineOut.act_price = isNaN(price) ? 0 : price; + lineOut.oem_partno = partInfo.OEMPartNum; + lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum; + + // THIS NEEDS TO BE CHANGED IN CHANGE REQUEST + lineOut.act_price = parseFloat(partInfo?.PartPrice || 0); + lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0); // Tax flag from PartInfo.TaxableInd when provided if ( @@ -384,8 +385,10 @@ const extractJobLines = (rq) => { (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y"); } } - //TODO: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically - // could.May require additional discussion. + + //TODO (FUTURE): Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically + // could. May require additional discussion. + // EMS - > Misc Amount, calibration for example, painting, etc else if (hasSublet) { const amt = parseFloat(subletInfo.SubletAmount || 0); lineOut.part_type = "PAS"; // Sublet as parts-as-service @@ -400,18 +403,22 @@ const extractJobLines = (rq) => { (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || (!isNaN(hrs) && hrs !== 0) || (!isNaN(amt) && amt !== 0); + if (hasLabor) { lineOut.mod_lbr_ty = laborInfo.LaborType || null; lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs; - //TODO: can add lbr_op_desc according to mapping available in new partner. - lineOut.lbr_op = laborInfo.LaborOperation || null; + const opCodeKey = + typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null; + lineOut.op_code_desc = opCodes?.[opCodeKey]?.desc || null; + lineOut.lbr_amt = isNaN(amt) ? 0 : amt; } - //TODO: what's the BMS logic for this? Body and refinish operations can often happen to the same part, + //TODO (FUTURE): what's the BMS logic for this? Body and refinish operations can often happen to the same part, // but most systems output a second line for the refinish labor. - //TODO: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC. + //TODO (FUTURE): 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC. // Refinish labor (if present) recorded on the same line using secondary labor fields + const rHrs = parseFloat(refinishInfo.LaborHours || 0); const rAmt = parseFloat(refinishInfo.LaborAmt || 0); const hasRefinish = @@ -421,9 +428,9 @@ const extractJobLines = (rq) => { !isNaN(rAmt) || !!refinishInfo.LaborOperation); if (hasRefinish) { - lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //TODO: _j fields indicate judgement, and are bool type. - lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; //TODO: _j fields indicate judgement, and are bool type. - lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //TODO: _j fields indicate judgement, and are bool type. + lineOut.lbr_typ_j = !!refinishInfo?.LaborAmtJudgmentInd; + lineOut.lbr_hrs_j = !!refinishInfo?.LaborHoursJudgmentInd; + lineOut.lbr_op_j = !!refinishInfo.LaborOperationJudgmentInd; // Aggregate refinish labor amount into the total labor amount for the line if (!isNaN(rAmt)) { lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt; @@ -494,9 +501,10 @@ const insertOwner = async (ownerInput, logger) => { // } // const total = parts + labor; // -// //TODO: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system. +// //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system. // return Number.isFinite(total) && total > 0 ? total : 0; -// }; +// //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible, +// // betterment and taxes. Typically provided by the source system. /** * Handles the VehicleDamageEstimateAddRq XML request from parts management. @@ -534,7 +542,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => { scheduled_completion, clm_no, policy_no, - ded_amt + ded_amt, + driveable // status, } = extractJobData(rq); @@ -567,6 +576,7 @@ const vehicleDamageEstimateAddRq = async (req, res) => { // Build job input const jobInput = { shopid: shopId, + driveable, converted: true, ownerid, ro_number: refClaimNum, diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js index b69b2466c..c79340387 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js @@ -4,6 +4,7 @@ const client = require("../../../graphql-client/graphql-client").client; const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); +const opCodes = require("./lib/opCodes.json"); const { GET_JOB_BY_ID, @@ -55,6 +56,11 @@ const extractUpdatedJobData = (rq) => { out.parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo); } + // Mirror AddRq: update driveable if present + if (rq.VehicleInfo?.Condition?.DrivableInd !== undefined) { + out.driveable = !!rq.VehicleInfo.Condition.DrivableInd; + } + return out; }; @@ -118,12 +124,14 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) const hasSublet = Object.keys(subletInfo).length > 0; if (hasPart) { - const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); - lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null; + // Mirror AddRq behavior: use OEMPartNum fields and parse prices directly lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1; - lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null; - lineOut.db_price = isNaN(price) ? 0 : price; - lineOut.act_price = isNaN(price) ? 0 : price; + lineOut.oem_partno = partInfo.OEMPartNum; + lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum; + + // Pricing: act_price from PartPrice, db_price from OEMPartPrice + lineOut.act_price = parseFloat(partInfo?.PartPrice || 0); + lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0); // Optional: taxability flag for parts if ( @@ -155,7 +163,12 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) if (hasLabor) { lineOut.mod_lbr_ty = laborInfo.LaborType || null; lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs; - lineOut.lbr_op = laborInfo.LaborOperation || null; + + // Map operation code description from opCodes.json (case-insensitive) + const opCodeKey = + typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null; + lineOut.op_code_desc = opCodeKey && opCodes?.[opCodeKey]?.desc ? opCodes[opCodeKey].desc : null; + lineOut.lbr_amt = isNaN(amt) ? 0 : amt; } @@ -250,6 +263,9 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => { //TODO: for changed lines, are they deleted and then reinserted? //TODO: Updated lines should get an upsert to update things like desc, price, etc. + // Updated Seqs should not be soft deleted + // logic in available jobs container + // Encapsulate so it is not multiple queries if (deletedLineIds?.length || updatedSeqs?.length) { const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])])); if (allToDelete.length) {