const xml2js = require("xml2js"); const client = require("../../graphql-client/graphql-client").client; // GraphQL statements const INSERT_JOB_WITH_LINES = ` mutation InsertJob($job: jobs_insert_input!) { insert_jobs_one(object: $job) { id joblines { id unq_seq } } } `; const INSERT_PARTS_ORDERS = ` mutation InsertPartsOrders($po: [parts_orders_insert_input!]!) { insert_parts_orders(objects: $po) { returning { id order_number } } } `; /** * Handles incoming VehicleDamageEstimateAddRq XML, * parses every known field, inserts a Job + nested JobLines, * then any PartsOrders (grouped per SupplierRefNum). */ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { const { logger } = req; const xml = req.body; let payload; try { payload = await xml2js.parseStringPromise(xml, { explicitArray: false, tagNameProcessors: [xml2js.processors.stripPrefix] }); logger.log("parts-xml-parse", "debug", null, null, { success: true }); } 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 ID ─────────────────────────────────────────────────────────────────── // // pulled directly from in your XML // const shopId = rq.ShopID || rq.shopId; if (!shopId) { throw { status: 400, message: "Missing in XML" }; } // // ── DOCUMENT INFO ───────────────────────────────────────────────────────────── // const { RqUID, RefClaimNum } = rq; const doc = rq.DocumentInfo || {}; const comment = doc.Comment || null; const transmitDate = doc.TransmitDateTime || null; // capture all entries const docVers = doc.DocumentVer ? (Array.isArray(doc.DocumentVer) ? doc.DocumentVer : [doc.DocumentVer]) : []; const documentVersions = docVers.map((dv) => ({ code: dv.DocumentVerCode, num: dv.DocumentVerNum })); // pull out any OtherReferenceInfo (RO Number + Job UUID) const otherRefs = doc.ReferenceInfo?.OtherReferenceInfo ? Array.isArray(doc.ReferenceInfo.OtherReferenceInfo) ? doc.ReferenceInfo.OtherReferenceInfo : [doc.ReferenceInfo.OtherReferenceInfo] : []; const originalRoNumber = otherRefs.find((r) => r.OtherReferenceName === "RO Number")?.OtherRefNum; const originalJobUuid = otherRefs.find((r) => r.OtherReferenceName === "Job UUID")?.OtherRefNum; // // ── EVENT INFO ──────────────────────────────────────────────────────────────── // const ev = rq.EventInfo || {}; const assignEv = ev.AssignmentEvent || {}; const assignmentEvent = { number: assignEv.AssignmentNumber, type: assignEv.AssignmentType, date: assignEv.AssignmentDate, createdAt: assignEv.CreateDateTime }; const repairEv = ev.RepairEvent || {}; const scheduled_completion = repairEv.TargetCompletionDateTime || null; const scheduled_in = repairEv.RequestedPickUpDateTime || null; // // ── CLAIM INFO ──────────────────────────────────────────────────────────────── // const ci = rq.ClaimInfo || {}; const clm_no = ci.ClaimNum; const ClaimStatus = ci.ClaimStatus || null; const policy_no = ci.PolicyInfo?.PolicyNum || null; const ded_amt = parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0); // if your XML ever has a `` you'd parse it here const clm_total = parseFloat(ci.Cieca_ttl || 0); // // ── OWNER ───────────────────────────────────────────────────────────────────── // const ownerParty = rq.AdminInfo?.Owner?.Party || {}; const ownerName = ownerParty.PersonInfo?.PersonName || {}; const ownerOrg = ownerParty.OrgInfo || {}; const ownerAddr = ownerParty.PersonInfo?.Communications?.Address || {}; const ownerComms = ownerParty.ContactInfo?.Communications ? Array.isArray(ownerParty.ContactInfo.Communications) ? ownerParty.ContactInfo.Communications : [ownerParty.ContactInfo.Communications] : []; let ownerPhone = null, ownerEmail = null; ownerComms.forEach((c) => { if (c.CommQualifier === "CP") ownerPhone = c.CommPhone; if (c.CommQualifier === "EM") ownerEmail = c.CommEmail; }); const ownerPrefContact = ownerParty.PreferredContactMethod || null; // // ── VEHICLE INFO ────────────────────────────────────────────────────────────── // 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 production_date = desc.ProductionDate || null; const sub_model_desc = desc.SubModelDesc || null; const fuel_type = desc.FuelType || null; const v_color = rq.VehicleInfo?.Paint?.Exterior?.ColorName || null; const drivable = rq.VehicleInfo?.Condition?.DrivableInd === "Y"; // // ── PROFILE & RATES ─────────────────────────────────────────────────────────── // const rateInfos = rq.ProfileInfo?.RateInfo ? Array.isArray(rq.ProfileInfo.RateInfo) ? rq.ProfileInfo.RateInfo : [rq.ProfileInfo.RateInfo] : []; const rates = {}; // main { rate_lab, rate_laf, … } const rateTier = {}; // e.g. { MA2S: [ {tier, pct}, … ] } const materialCalc = {}; // e.g. { LAR: { CalcMethodCode, CalcMaxHours }, … } rateInfos.forEach((r) => { if (!r || !r.RateType) return; const t = r.RateType; // main per‐unit rate if (r.Rate) rates[`rate_${t.toLowerCase()}`] = parseFloat(r.Rate) || 0; // any tier settings if (r.RateTierInfo) { const tiers = Array.isArray(r.RateTierInfo) ? r.RateTierInfo : [r.RateTierInfo]; rateTier[t] = tiers.map((ti) => ({ tier: ti.TierNum, pct: parseFloat(ti.Percentage) || 0 })); } // any material‐calc limits if (r.MaterialCalcSettings) { materialCalc[t] = r.MaterialCalcSettings; } }); // // ── DAMAGE LINES → joblinesData ───────────────────────────────────────────── // const damageLines = Array.isArray(rq.DamageLineInfo) ? rq.DamageLineInfo : [rq.DamageLineInfo]; const joblinesData = damageLines.map((line) => ({ line_no: parseInt(line.LineNum, 10), unq_seq: parseInt(line.UniqueSequenceNum, 10), manual_line: line.ManualLineInd === "1", automated_entry: line.AutomatedEntry === "1", desc_judgment_ind: line.DescJudgmentInd === "1", status: line.LineStatusCode || null, line_desc: line.LineDesc || null, // parts part_type: line.PartInfo.PartType || null, part_qty: parseInt(line.PartInfo.Quantity || 0, 10), db_price: parseFloat(line.PartInfo.PartPrice || 0), act_price: parseFloat(line.PartInfo.PartPrice || 0), oem_partno: line.PartInfo.OEMPartNum || null, // non-OEM block non_oem_part_num: line.PartInfo?.NonOEM?.NonOEMPartNum || null, non_oem_part_price: parseFloat(line.PartInfo?.NonOEM?.NonOEMPartPrice || 0), supplier_ref_num: line.PartInfo?.NonOEM?.SupplierRefNum || null, part_selected_ind: line.PartInfo?.NonOEM?.PartSelectedInd === "1", after_market_usage: line.PartInfo.AfterMarketUsage || null, certification_type: line.PartInfo.CertificationType || null, tax_part: line.PartInfo.TaxableInd === "1", glass_flag: line.PartInfo.GlassPartInd === "1", price_j: line.PriceJudgmentInd === "1", price_inc: line.PriceInclInd === "1", order_by_application_ind: String(line.PartInfo.OrderByApplicationInd).toLowerCase() === "true", // 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), // linkage & memo parent_line_no: line.ParentLineNum ? parseInt(line.ParentLineNum, 10) : null, notes: line.LineMemo || null })); // // ── BUILD & INSERT THE JOB ─────────────────────────────────────────────────── // const jobInput = { shopid: shopId, // identifiers ro_number: RefClaimNum, original_ro_number: originalRoNumber, original_job_uuid: originalJobUuid, // claim & policy clm_no, status: ClaimStatus, clm_total, policy_no, ded_amt, // timestamps & comments comment, date_exported: transmitDate, // owner ownr_fn: ownerName.FirstName || null, ownr_ln: ownerName.LastName || null, ownr_co_nm: ownerOrg.CompanyName || null, ownr_addr1: ownerAddr.Address1 || null, ownr_city: ownerAddr.City || null, ownr_st: ownerAddr.StateProvince || null, ownr_zip: ownerAddr.PostalCode || null, ownr_country: ownerAddr.Country || null, ownr_ph1: ownerPhone, ownr_ea: ownerEmail, ownr_pref_contact: ownerPrefContact, // vehicle v_vin: vin, plate_no, plate_st, v_model_yr, v_make_desc, v_model_desc, v_color, body_style, engine_desc, production_date, sub_model_desc, fuel_type, drivable, // labor & material rates ...rates, // everything extra in one JSON column production_vars: { rqUid: RqUID, documentVersions, assignmentEvent, scheduled_completion, scheduled_in, rateTier, materialCalc }, // nested joblines joblines: { data: joblinesData } }; logger.log("parts-insert-job", "debug", null, null, { jobInput }); const jobResp = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput }); const newJob = jobResp.insert_jobs_one; const jobId = newJob.id; logger.log("parts-job-created", "info", jobId, null); // // ── BUILD & INSERT PARTS ORDERS ──────────────────────────────────────────── // // group lines by their SupplierRefNum const insertedMap = newJob.joblines.reduce((m, ln) => { m[ln.unq_seq] = ln.id; return m; }, {}); const poGroups = {}; damageLines.forEach((line) => { const pt = line.PartInfo.PartType; const qty = parseInt(line.PartInfo.Quantity || 0, 10); if (["PAN", "PAC", "PAM", "PAA"].includes(pt) && qty > 0) { const vendorid = line.PartInfo?.NonOEM?.SupplierRefNum || process.env.DEFAULT_VENDOR_ID; if (!poGroups[vendorid]) poGroups[vendorid] = []; poGroups[vendorid].push({ line, unq_seq: parseInt(line.UniqueSequenceNum, 10) }); } }); const partsOrders = Object.entries(poGroups).map(([vendorid, entries]) => ({ jobid: jobId, vendorid, order_number: `${clm_no}-${entries[0].line.LineNum}`, parts_order_lines: { data: entries.map(({ line, unq_seq }) => ({ job_line_id: insertedMap[unq_seq], part_type: line.PartInfo.PartType, quantity: parseInt(line.PartInfo.Quantity || 0, 10), act_price: parseFloat(line.PartInfo.PartPrice || 0), db_price: parseFloat(line.PartInfo.PartPrice || 0), line_desc: line.LineDesc, non_oem_part_num: line.PartInfo?.NonOEM?.NonOEMPartNum || null, non_oem_part_price: parseFloat(line.PartInfo?.NonOEM?.NonOEMPartPrice || 0), part_selected_ind: line.PartInfo?.NonOEM?.PartSelectedInd === "1" })) } })); if (partsOrders.length) { logger.log("parts-insert-orders", "debug", null, null, { partsOrders }); await client.request(INSERT_PARTS_ORDERS, { po: partsOrders }); logger.log("parts-orders-created", "info", jobId, null); } return res.status(200).json({ success: true, jobId }); } 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;