// 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; // 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"; // Known part rate types for tax rates const KNOWN_PART_RATE_TYPES = [ "PAA", "PAC", "PAG", "PAL", "PAM", "PAN", "PAO", "PAP", "PAR", "PAS", "PASL", "CCC", "CCD", "CCF", "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} 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"); } }; /** * 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; } }; /** * Extracts and processes parts tax rates from profile info. * @param {object} profile - The ProfileInfo object from XML. * @returns {object} The parts tax rates object. */ const extractPartsTaxRates = (profile = {}) => { const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; const partsTaxRates = {}; for (const code of KNOWN_PART_RATE_TYPES) { const rateInfo = rateInfos.find((r) => (r?.RateType || "").toUpperCase() === code); if (!rateInfo) { partsTaxRates[code] = {}; continue; } const taxInfo = rateInfo.TaxInfo; const taxTier = taxInfo?.TaxTierInfo; let percentage = parseFloat(taxTier?.Percentage ?? "NaN"); if (isNaN(percentage)) { const tierRate = Array.isArray(rateInfo.RateTierInfo) ? rateInfo.RateTierInfo[0]?.Rate : rateInfo.RateTierInfo?.Rate; percentage = parseFloat(tierRate ?? "NaN"); } partsTaxRates[code] = isNaN(percentage) ? {} : { prt_discp: 0, prt_mktyp: false, prt_mkupp: 0, prt_tax_in: true, prt_tax_rt: percentage / 100 }; } return partsTaxRates; }; /** * 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, cieca_ttl: parseFloat(rq.Cieca_ttl || 0), cat_no: doc.VendorCode || null, category: doc.DocumentType || null, classType: doc.DocumentStatus || null, comment: doc.Comment || null, date_exported: doc.TransmitDateTime || null, asgn_no: asgn.AssignmentNumber || null, asgn_type: asgn.AssignmentType || null, asgn_date: asgn.AssignmentDate || 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?.PolicyNum || null, ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0) }; }; /** * Extracts owner data from the XML request. * @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 ownerParty = rq.AdminInfo?.Owner?.Party || {}; const adr = ownerParty.PersonInfo?.Communications?.Address || {}; let ownr_ph1, ownr_ph2, ownr_ea; (Array.isArray(ownerParty.ContactInfo?.Communications) ? ownerParty.ContactInfo.Communications : [ownerParty.ContactInfo?.Communications || {}] ).forEach((c) => { if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone; if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone; if (c.CommQualifier === "EM") ownr_ea = c.CommEmail; }); return { shopid: shopId, ownr_fn: ownerParty.PersonInfo?.PersonName?.FirstName || null, ownr_ln: ownerParty.PersonInfo?.PersonName?.LastName || null, ownr_co_nm: ownerParty.OrgInfo?.CompanyName || null, ownr_addr1: adr.Address1 || null, ownr_addr2: adr.Address2 || null, ownr_city: adr.City || null, ownr_st: adr.StateProvince || null, ownr_zip: adr.PostalCode || null, ownr_ctry: adr.Country || null, ownr_ph1, ownr_ph2, ownr_ea }; }; /** * 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 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 || {}; return { shopid: shopId, v_vin: 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: rq.VehicleInfo?.Paint?.Exterior?.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 }; }; /** * Extracts job lines from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. * @returns {object[]} Array of job line objects. */ const extractJobLines = (rq) => { const damageLines = Array.isArray(rq.DamageLineInfo) ? rq.DamageLineInfo : [rq.DamageLineInfo]; return damageLines.map((line) => { const jobLine = { 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 }; // TODO: Commented out as not clear if needed for version 1, this only applies to Imex and not rome on the front // end // if ((jobLine.part_type === "PASL" || jobLine.part_type === "PAS") && jobLine.lbr_op !== "OP11") { // jobLine.tax_part = true; // } // if (line.db_ref === "900510") { // jobLine.tax_part = true; // } return jobLine; }); }; /** * 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; }; /** * 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; } }; /** * 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 partsManagementVehicleDamageEstimateAddRq = async (req, res) => { const { logger } = req; try { // Parse XML const payload = await parseXml(req.body, logger); const rq = 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 joblinesData = extractJobLines(rq); // 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, ownerid, ro_number: refClaimNum, ciecaid, cieca_ttl, cat_no, category, class: classType, parts_tax_rates, clm_no, status: status || defaultStatus, clm_total: cieca_ttl, policy_no, ded_amt, comment, date_exported, asgn_no, asgn_type, asgn_date, scheduled_in, scheduled_completion, ...ownerData, // Inline owner data ...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 }); logger.log("parts-job-created", "info", newJob.id, null); 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 = partsManagementVehicleDamageEstimateAddRq;