// 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; // 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!) { insert_jobs_one(object: $job) { id joblines { id unq_seq } } } `; 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; // ── PARSE XML ──────────────────────────────────────────────────────────────── let payload; try { payload = await xml2js.parseStringPromise(xml, { explicitArray: false, tagNameProcessors: [xml2js.processors.stripPrefix], attrNameProcessors: [xml2js.processors.stripPrefix] // ignoreAttrs: false, // xmlns: false }); } catch (err) { logger.log("parts-xml-parse-error", "error", null, null, { error: err }); return res.status(400).send("Invalid XML"); } const rq = payload.VehicleDamageEstimateAddRq; if (!rq) { logger.log("parts-missing-root", "error"); return res.status(400).send("Missing "); } try { // ── SHOP & CLAIM IDs ──────────────────────────────────────────────────────── const shopId = rq.ShopID || rq.shopId; if (!shopId) throw { status: 400, message: "Missing 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; const date_exported = doc.TransmitDateTime || null; // capture CIECA ID & totals const ciecaid = rq.RqUID || null; const cieca_ttl = parseFloat(rq.Cieca_ttl || 0); // map DocumentInfo fields to our category/class fields const cat_no = doc.VendorCode || null; 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 // If this has become an issue, default it to an empty object for version 1 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 || {}; const asgn_no = asgn.AssignmentNumber || null; const asgn_type = asgn.AssignmentType || null; const asgn_date = asgn.AssignmentDate || null; const scheduled_in = ev.RepairEvent?.RequestedPickUpDateTime || null; const scheduled_completion = ev.RepairEvent?.TargetCompletionDateTime || null; // ── CLAIM & POLICY ────────────────────────────────────────────────────────── const ci = rq.ClaimInfo || {}; const clm_no = ci.ClaimNum || null; const status = ci.ClaimStatus || null; const policy_no = ci.PolicyInfo?.PolicyNum || null; const ded_amt = parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0); // ── OWNER ──────────────────────────────────────────────────────────────────── const ownerParty = rq.AdminInfo?.Owner?.Party || {}; const ownr_fn = ownerParty.PersonInfo?.PersonName?.FirstName || null; const ownr_ln = ownerParty.PersonInfo?.PersonName?.LastName || null; const ownr_co_nm = ownerParty.OrgInfo?.CompanyName || null; const adr = ownerParty.PersonInfo?.Communications?.Address || {}; const ownr_addr1 = adr.Address1 || null; const ownr_addr2 = adr.Address2 || null; const ownr_city = adr.City || null; const ownr_st = adr.StateProvince || null; const ownr_zip = adr.PostalCode || null; const ownr_ctry = adr.Country || null; let ownr_ph1; let ownr_ph2; let ownr_fax; let 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 === "FX") ownr_fax = c.CommPhone; if (c.CommQualifier === "EM") ownr_ea = c.CommEmail; }); // Estimator → map to est_… fields const estParty = rq.AdminInfo?.Estimator?.Party || {}; // grab raw first/last const est_fn = estParty.PersonInfo?.PersonName?.FirstName || null; const est_ln = estParty.PersonInfo?.PersonName?.LastName || null; // now alias into the GraphQL names const est_ct_fn = est_fn; const est_ct_ln = est_ln; const est_co_nm = rq.AdminInfo?.Estimator?.Affiliation || null; const estComms = Array.isArray(estParty.ContactInfo?.Communications) ? estParty.ContactInfo.Communications : [estParty.ContactInfo?.Communications || {}]; const est_ea = estComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null; // ── ADJUSTER ──────────────────────────────────────────────────────────────── const adjParty = rq.AdminInfo?.Adjuster?.Party || {}; const agt_ct_fn = adjParty.PersonInfo?.PersonName?.FirstName || null; const agt_ct_ln = adjParty.PersonInfo?.PersonName?.LastName || null; const agt_ct_ph = (Array.isArray(adjParty.ContactInfo?.Communications) ? adjParty.ContactInfo.Communications : [adjParty.ContactInfo?.Communications || {}] ).find((c) => c.CommQualifier === "CP")?.CommPhone || null; const agt_ea = (Array.isArray(adjParty.ContactInfo?.Communications) ? adjParty.ContactInfo.Communications : [adjParty.ContactInfo?.Communications || {}] ).find((c) => c.CommQualifier === "EM")?.CommEmail || null; // ── REPAIR FACILITY ───────────────────────────────────────────────────────── const rfParty = rq.AdminInfo?.RepairFacility?.Party || {}; const servicing_dealer = rfParty.OrgInfo?.CompanyName || null; const servicing_dealer_contact = (Array.isArray(rfParty.ContactInfo?.Communications) ? rfParty.ContactInfo.Communications : [rfParty.ContactInfo?.Communications || {}] ).find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null; // ── VEHICLE (one-to-one) ───────────────────────────────────────────────────── const vin = rq.VehicleInfo?.VINInfo?.VINNum || null; const plate_no = rq.VehicleInfo?.License?.LicensePlateNum || null; const plate_st = rq.VehicleInfo?.License?.LicensePlateStateProvince || null; const desc = rq.VehicleInfo?.VehicleDesc || {}; const v_model_yr = desc.ModelYear || null; const v_make_desc = desc.MakeDesc || null; const v_model_desc = desc.ModelName || null; const body_style = desc.BodyStyle || null; const engine_desc = desc.EngineDesc || null; const v_options = desc.SubModelDesc || null; const v_type = desc.FuelType || null; const v_cond = rq.VehicleInfo?.Condition?.DrivableInd; const vehicleData = { shopid: shopId, v_vin: vin, plate_no, plate_st, v_model_yr, v_make_desc, v_model_desc, v_color: rq.VehicleInfo?.Paint?.Exterior?.ColorName || null, v_bstyle: body_style, v_engine: engine_desc, // prod_dt: production_date, v_options, v_type, v_cond }; // ── DAMAGE LINES → joblinesData ──────────────────────────────────────────── const damageLines = Array.isArray(rq.DamageLineInfo) ? rq.DamageLineInfo : [rq.DamageLineInfo]; const joblinesData = 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, // parts 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), // labor 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; }); 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 ciecaid, cieca_ttl, cat_no, category, class: classType, // tax parts_tax_rates, // claim & policy clm_no, status: status || defaultStatus, clm_total: cieca_ttl, policy_no, ded_amt, // document & events comment, date_exported, asgn_no, asgn_type, asgn_date, scheduled_in, scheduled_completion, // owner ownr_fn, ownr_ln, ownr_co_nm, ownr_addr1, ownr_addr2, ownr_city, ownr_st, ownr_zip, ownr_ctry, ownr_ph1, ownr_ph2, ownr_fax, ownr_ea, // estimator est_co_nm, est_ct_fn, est_ct_ln, est_ea, // adjuster agt_ct_fn, agt_ct_ln, agt_ct_ph, agt_ea, // repair facility servicing_dealer, servicing_dealer_contact, // nested relationships vehicle: { data: vehicleData }, joblines: { data: joblinesData } }; 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;