// 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"); // GraphQL Queries and Mutations const { GET_BODYSHOP_STATUS, GET_VEHICLE_BY_SHOP_VIN, INSERT_OWNER, INSERT_JOB_WITH_LINES } = require("../partsManagement.queries"); // Defaults const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; // Config: include labor lines and labor in totals (default true) const INCLUDE_LABOR = true; /** * Fetches the default order status for a bodyshop. * @param {string} shopId - The bodyshop UUID. * @param {object} logger - The logger instance. * @returns {Promise} The default status or fallback. */ const getDefaultOrderStatus = async (shopId, logger) => { try { const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId }); return bodyshop_by_pk?.md_order_statuses?.default_open || FALLBACK_DEFAULT_ORDER_STATUS; } catch (err) { logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err }); return FALLBACK_DEFAULT_ORDER_STATUS; } }; /** * Finds an existing vehicle by shopId and VIN. * @param {string} shopId - The bodyshop UUID. * @param {string} v_vin - The vehicle VIN. * @param {object} logger - The logger instance. * @returns {Promise} The vehicle ID or null if not found. */ const findExistingVehicle = async (shopId, v_vin, logger) => { if (!v_vin) return null; try { const { vehicles } = await client.request(GET_VEHICLE_BY_SHOP_VIN, { shopid: shopId, v_vin }); if (vehicles?.length > 0) { logger.log("parts-vehicle-found", "info", vehicles[0].id, null, { shopid: shopId, v_vin }); return vehicles[0].id; } } catch (err) { logger.log("parts-vehicle-fetch-failed", "warn", null, null, { error: err }); } return null; }; /** * Extracts job-related data from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @returns {object} Extracted job data. */ const extractJobData = (rq) => { const doc = rq.DocumentInfo || {}; const ev = rq.EventInfo || {}; const asgn = ev.AssignmentEvent || {}; const ci = rq.ClaimInfo || {}; return { shopId: rq.ShopID || rq.shopId, refClaimNum: rq.RefClaimNum, ciecaid: rq.RqUID || null, // Pull Cieca_ttl from ClaimInfo per schema/sample cieca_ttl: parseFloat(ci.Cieca_ttl || 0), cat_no: doc.VendorCode || null, category: doc.DocumentType || null, classType: doc.DocumentStatus || null, comment: doc.Comment || null, // TODO: This causes the job to be read only in the UI // date_exported: doc.TransmitDateTime || null, asgn_no: asgn.AssignmentNumber || null, asgn_type: asgn.AssignmentType || null, asgn_date: asgn.AssignmentDate || null, // asgn_created: asgn.CreateDateTime || null, scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null, scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null, clm_no: ci.ClaimNum || null, status: ci.ClaimStatus || null, policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null, ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0) }; }; /** * Extracts owner data from the XML request. * Falls back to Claimant if Owner is missing. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @param {string} shopId - The bodyshop UUID. * @returns {object} Owner data for insertion and inline use. */ const extractOwnerData = (rq, shopId) => { const ownerOrClaimant = rq.AdminInfo?.Owner?.Party || rq.AdminInfo?.Claimant?.Party || {}; const personInfo = ownerOrClaimant.PersonInfo || {}; const personName = personInfo.PersonName || {}; const address = personInfo.Communications?.Address || {}; let ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph; const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications) ? ownerOrClaimant.ContactInfo.Communications : [ownerOrClaimant.ContactInfo?.Communications || {}]; for (const c of comms) { if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone; if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone; if (c.CommQualifier === "EM") ownr_ea = c.CommEmail; if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone; } return { shopid: shopId, ownr_fn: personName.FirstName || null, ownr_ln: personName.LastName || null, ownr_co_nm: ownerOrClaimant.OrgInfo?.CompanyName || null, ownr_addr1: address.Address1 || null, ownr_addr2: address.Address2 || null, ownr_city: address.City || null, ownr_st: address.StateProvince || null, ownr_zip: address.PostalCode || null, ownr_ctry: address.Country || null, ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph // ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New // ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New // ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New }; }; /** * Extracts estimator data from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @returns {object} Estimator data. */ const extractEstimatorData = (rq) => { const estParty = rq.AdminInfo?.Estimator?.Party || {}; const estComms = Array.isArray(estParty.ContactInfo?.Communications) ? estParty.ContactInfo.Communications : [estParty.ContactInfo?.Communications || {}]; return { est_co_nm: rq.AdminInfo?.Estimator?.Affiliation || null, est_ct_fn: estParty.PersonInfo?.PersonName?.FirstName || null, est_ct_ln: estParty.PersonInfo?.PersonName?.LastName || null, est_ea: estComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null }; }; /** * Extracts adjuster data from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @returns {object} Adjuster data. */ const extractAdjusterData = (rq) => { const adjParty = rq.AdminInfo?.Adjuster?.Party || {}; const adjComms = Array.isArray(adjParty.ContactInfo?.Communications) ? adjParty.ContactInfo.Communications : [adjParty.ContactInfo?.Communications || {}]; return { 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, agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null }; }; /** * Extracts repair facility data from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @returns {object} Repair facility data. */ const extractRepairFacilityData = (rq) => { const rfParty = rq.AdminInfo?.RepairFacility?.Party || {}; const rfComms = Array.isArray(rfParty.ContactInfo?.Communications) ? rfParty.ContactInfo.Communications : [rfParty.ContactInfo?.Communications || {}]; return { servicing_dealer: rfParty.OrgInfo?.CompanyName || null, servicing_dealer_contact: rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null }; }; /** * Extracts loss information from the XML request. * @param rq * @returns {{loss_dt: (*|null), reported_dt: (*|null), loss_type_code: (*|null), loss_type_desc: (*|null)}} */ const extractLossInfo = (rq) => { const loss = rq.ClaimInfo?.LossInfo?.Facts || {}; const custom = rq.ClaimInfo?.CustomElement || {}; return { loss_date: loss.LossDateTime || null, loss_type: custom.LossTypeCode || null, loss_desc: custom.LossTypeDesc || null // primary_poi: loss.PrimaryPOI?.POICode || null, // secondary_poi: loss.SecondaryPOI?.POICode || null, // damage_memo: loss.DamageMemo || null, //(maybe ins_memo) // total_loss_ind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null // New }; }; /** * Extracts insurance data from the XML request. * @param rq * @returns {{insd_ln: (*|null), insd_fn: (string|null), insd_title: (*|null), insd_co_nm: (*|string|null), insd_addr1: * (*|null), insd_addr2: (*|null), insd_city: (*|null), insd_st: (*|null), insd_zip: (*|null), insd_ctry: (*|null), * insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea}} */ const extractInsuranceData = (rq) => { const insuredParty = rq.AdminInfo?.Insured?.Party || {}; const insuredPerson = insuredParty.PersonInfo || {}; const insuredComms = Array.isArray(insuredParty.ContactInfo?.Communications) ? insuredParty.ContactInfo.Communications : [insuredParty.ContactInfo?.Communications || {}]; const insuredAddress = insuredPerson.Communications?.Address || {}; const insurerParty = rq.AdminInfo?.InsuranceCompany?.Party || {}; let insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea; for (const c of insuredComms) { if (c.CommQualifier === "CP") { insd_ph1 = c.CommPhone; insd_ph1x = c.CommPhoneExt; } if (c.CommQualifier === "WP") { insd_ph2 = c.CommPhone; insd_ph2x = c.CommPhoneExt; } if (c.CommQualifier === "FX") { insd_fax = c.CommPhone; insd_faxx = c.CommPhoneExt; } if (c.CommQualifier === "EM") insd_ea = c.CommEmail; } return { insd_ln: insuredPerson.PersonName?.LastName || null, insd_fn: insuredPerson.PersonName?.FirstName || null, insd_title: insuredPerson.PersonName?.Title || null, insd_co_nm: insurerParty.OrgInfo?.CompanyName || insuredParty.OrgInfo?.CompanyName || null, insd_addr1: insuredAddress.Address1 || null, insd_addr2: insuredAddress.Address2 || null, insd_city: insuredAddress.City || null, insd_st: insuredAddress.StateProvince || null, insd_zip: insuredAddress.PostalCode || null, insd_ctry: insuredAddress.Country || null, insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea }; }; /** * Extracts vehicle data from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @param {string} shopId - The bodyshop UUID. * @returns {object} Vehicle data for insertion and inline use. */ const extractVehicleData = (rq, shopId) => { const desc = rq.VehicleInfo?.VehicleDesc || {}; const exterior = rq.VehicleInfo?.Paint?.Exterior || {}; const interior = rq.VehicleInfo?.Paint?.Interior || {}; return { shopid: shopId, // VIN may be either VINInfo.VINNum or VINInfo.VIN.VINNum depending on producer v_vin: rq.VehicleInfo?.VINInfo?.VINNum || rq.VehicleInfo?.VINInfo?.VIN?.VINNum || null, plate_no: rq.VehicleInfo?.License?.LicensePlateNum || null, plate_st: rq.VehicleInfo?.License?.LicensePlateStateProvince || null, v_model_yr: desc.ModelYear || null, v_make_desc: desc.MakeDesc || null, v_model_desc: desc.ModelName || null, v_color: exterior.Color?.ColorName || null, v_bstyle: desc.BodyStyle || null, v_engine: desc.EngineDesc || null, v_options: desc.SubModelDesc || null, v_type: desc.FuelType || null, v_cond: rq.VehicleInfo?.Condition?.DrivableInd, v_trimcode: desc.TrimCode || null, v_tone: exterior.Tone || null, v_stage: exterior.RefinishStage || rq.VehicleInfo?.Paint?.RefinishStage || null, v_prod_dt: desc.ProductionDate || null, v_paint_codes: Array.isArray(exterior.PaintCodeInfo) ? exterior.PaintCodeInfo.map((p) => p.PaintCode).join(",") : exterior.PaintCode || null, v_mldgcode: desc.MldgCode || null, v_makecode: desc.MakeCode || null, trim_color: interior.ColorName || desc.TrimColor || null, db_v_code: desc.DatabaseCode || null }; }; /** * Extracts job lines from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @returns {object[]} Array of job line objects. */ const extractJobLines = (rq) => { // Normalize to array without lodash toArray (which flattens object values incorrectly) const dl = rq.DamageLineInfo; const damageLines = Array.isArray(dl) ? dl : dl ? [dl] : []; if (damageLines.length === 0) { return []; } const out = []; for (const line of damageLines) { const partInfo = line.PartInfo || {}; const laborInfo = line.LaborInfo || {}; const refinishInfo = line.RefinishLaborInfo || {}; const subletInfo = line.SubletInfo || {}; let jobLineType = "PART"; if (Object.keys(subletInfo).length > 0) jobLineType = "SUBLET"; else if (Object.keys(laborInfo).length > 0 && Object.keys(partInfo).length === 0) jobLineType = "LABOR"; const base = { line_no: parseInt(line.LineNum || 0, 10), unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), status: line.LineStatusCode || null, line_desc: line.LineDesc || null, notes: line.LineMemo || null }; if (jobLineType === "PART") { const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); // Push the part line with ONLY part pricing/fields out.push({ ...base, part_type: partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null, part_qty: parseFloat(partInfo.Quantity || 0) || 1, oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null, db_price: price, act_price: price, // Tax flag from PartInfo.TaxableInd when provided ...(partInfo.TaxableInd !== undefined && (typeof partInfo.TaxableInd === "string" || typeof partInfo.TaxableInd === "number" || typeof partInfo.TaxableInd === "boolean") ? { tax_part: partInfo.TaxableInd === true || partInfo.TaxableInd === 1 || partInfo.TaxableInd === "1" || (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y") } : {}), // Manual line flag coercion ...(line.ManualLineInd !== undefined ? { manual_line: line.ManualLineInd === true || line.ManualLineInd === 1 || line.ManualLineInd === "1" || (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") } : { manual_line: null }) }); // If labor is present on the same damage line, split it to a separate LABOR jobline // TODO: Verify with patrick this is desired. if (INCLUDE_LABOR) { const hrs = parseFloat(laborInfo.LaborHours || 0); const amt = parseFloat(laborInfo.LaborAmt || 0); const hasLabor = (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || (!isNaN(hrs) && hrs !== 0) || (!isNaN(amt) && amt !== 0); if (hasLabor) { out.push({ ...base, // tweak unq_seq to avoid collisions in later upserts unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000, mod_lbr_ty: laborInfo.LaborType || null, mod_lb_hrs: isNaN(hrs) ? 0 : hrs, lbr_op: laborInfo.LaborOperation || null, lbr_amt: isNaN(amt) ? 0 : amt, ...(line.ManualLineInd !== undefined ? { manual_line: line.ManualLineInd === true || line.ManualLineInd === 1 || line.ManualLineInd === "1" || (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") } : { manual_line: null }) }); } } } else if (jobLineType === "SUBLET") { out.push({ ...base, part_type: "PAS", part_qty: 1, act_price: parseFloat(subletInfo.SubletAmount || 0), // Manual line flag ...(line.ManualLineInd !== undefined ? { manual_line: line.ManualLineInd === true || line.ManualLineInd === 1 || line.ManualLineInd === "1" || (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") } : { manual_line: null }) }); } else if (INCLUDE_LABOR) { // Labor-only line (only when enabled) out.push({ ...base, mod_lbr_ty: laborInfo.LaborType || null, mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0), lbr_op: laborInfo.LaborOperation || null, lbr_amt: parseFloat(laborInfo.LaborAmt || 0), ...(line.ManualLineInd !== undefined ? { manual_line: line.ManualLineInd === true || line.ManualLineInd === 1 || line.ManualLineInd === "1" || (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") } : { manual_line: null }) }); } // Add a separate refinish labor line if present and enabled if (INCLUDE_LABOR && Object.keys(refinishInfo).length > 0) { const hrs = parseFloat(refinishInfo.LaborHours || 0); const amt = parseFloat(refinishInfo.LaborAmt || 0); if (!isNaN(hrs) || !isNaN(amt)) { out.push({ ...base, // tweak unq_seq to avoid collisions in later upserts unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, line_desc: base.line_desc || "Refinish", mod_lbr_ty: "LAR", mod_lb_hrs: isNaN(hrs) ? 0 : hrs, lbr_op: refinishInfo.LaborOperation || null, lbr_amt: isNaN(amt) ? 0 : amt, ...(line.ManualLineInd !== undefined ? { manual_line: line.ManualLineInd === true || line.ManualLineInd === 1 || line.ManualLineInd === "1" || (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") } : { manual_line: null }) }); } } } return out; }; // Helper to extract a GRAND TOTAL amount from RepairTotalsInfo const extractGrandTotal = (rq) => { const rti = rq.RepairTotalsInfo; const groups = Array.isArray(rti) ? rti : rti ? [rti] : []; for (const grp of groups) { const sums = Array.isArray(grp.SummaryTotalsInfo) ? grp.SummaryTotalsInfo : grp.SummaryTotalsInfo ? [grp.SummaryTotalsInfo] : []; for (const s of sums) { const type = (s.TotalType || "").toString().toUpperCase(); const desc = (s.TotalTypeDesc || "").toString().toUpperCase(); if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) { const amt = parseFloat(s.TotalAmt ?? "NaN"); if (!isNaN(amt)) return amt; } } } return null; }; /** * Inserts an owner and returns the owner ID. * @param {object} ownerInput - The owner data to insert. * @param {object} logger - The logger instance. * @returns {Promise} The owner ID or null if insertion fails. */ const insertOwner = async (ownerInput, logger) => { try { const { insert_owners_one } = await client.request(INSERT_OWNER, { owner: ownerInput }); return insert_owners_one?.id; } catch (err) { logger.log("parts-owner-insert-failed", "warn", null, null, { error: err }); return null; } }; // Fallback: compute a naive total from joblines (parts + sublet + labor amounts) const computeLinesTotal = (joblines = []) => { let parts = 0; let labor = 0; for (const jl of joblines) { if (jl && jl.part_type) { const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1; const price = Number.isFinite(jl.act_price) ? jl.act_price : 0; parts += price * (qty || 1); } else if (!jl.part_type && Number.isFinite(jl.act_price)) { parts += jl.act_price; } if (INCLUDE_LABOR && Number.isFinite(jl.lbr_amt)) { labor += jl.lbr_amt; } } const total = parts + labor; return Number.isFinite(total) && total > 0 ? total : 0; }; /** * Handles the VehicleDamageEstimateAddRq XML request from parts management. * @param {object} req - The HTTP request object. * @param {object} res - The HTTP response object. * @returns {Promise} */ const vehicleDamageEstimateAddRq = async (req, res) => { const { logger } = req; try { // Parse XML const payload = await parseXml(req.body, logger); const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq); if (!rq) { logger.log("parts-missing-root", "error"); return res.status(400).send("Missing "); } // Extract job data const { shopId, refClaimNum, ciecaid, cieca_ttl, cat_no, category, classType, comment, date_exported, asgn_no, asgn_type, asgn_date, scheduled_in, scheduled_completion, clm_no, status, policy_no, ded_amt } = extractJobData(rq); if (!shopId) { throw { status: 400, message: "Missing in XML" }; } // Get default status const defaultStatus = await getDefaultOrderStatus(shopId, logger); // Extract additional data const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo); const ownerData = extractOwnerData(rq, shopId); const estimatorData = extractEstimatorData(rq); const adjusterData = extractAdjusterData(rq); const repairFacilityData = extractRepairFacilityData(rq); const vehicleData = extractVehicleData(rq, shopId); const lossInfo = extractLossInfo(rq); const joblinesData = extractJobLines(rq); const insuranceData = extractInsuranceData(rq); // Derive clm_total: prefer RepairTotalsInfo SummaryTotals GRAND TOTAL; else sum from lines const grandTotal = extractGrandTotal(rq); const computedTotal = grandTotal ?? computeLinesTotal(joblinesData); // Find or create relationships const ownerid = await insertOwner(ownerData, logger); const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger); // Build job input const jobInput = { shopid: shopId, converted: true, ownerid, ro_number: refClaimNum, ciecaid, cieca_ttl, cat_no, category, class: classType, parts_tax_rates, clm_no, status: status || defaultStatus, clm_total: computedTotal || null, policy_no, ded_amt, comment, date_exported, asgn_no, asgn_type, asgn_date, scheduled_in, scheduled_completion, // Inline insurance/loss/contacts ...insuranceData, ...lossInfo, ...ownerData, ...estimatorData, ...adjusterData, ...repairFacilityData, // Inline vehicle data v_vin: vehicleData.v_vin, v_model_yr: vehicleData.v_model_yr, v_model_desc: vehicleData.v_model_desc, v_make_desc: vehicleData.v_make_desc, v_color: vehicleData.v_color, plate_no: vehicleData.plate_no, plate_st: vehicleData.plate_st, ...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }), joblines: { data: joblinesData } }; // Insert job const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput }); return res.status(200).json({ success: true, jobId: newJob.id }); } catch (err) { logger.log("parts-route-error", "error", null, null, { error: err }); return res.status(err.status || 500).json({ error: err.message || "Internal error" }); } }; module.exports = vehicleDamageEstimateAddRq;