diff --git a/server/integrations/partsManagement/partsManagement.queries.js b/server/integrations/partsManagement/partsManagement.queries.js new file mode 100644 index 000000000..45a88ad58 --- /dev/null +++ b/server/integrations/partsManagement/partsManagement.queries.js @@ -0,0 +1,40 @@ +// GraphQL Queries and Mutations +const GET_BODYSHOP_STATUS = ` + query GetBodyshopStatus($id: uuid!) { + bodyshops_by_pk(id: $id) { + md_order_statuses + } + } +`; + +const GET_VEHICLE_BY_SHOP_VIN = ` + query GetVehicleByShopVin($shopid: uuid!, $v_vin: String!) { + vehicles(where: { shopid: { _eq: $shopid }, v_vin: { _eq: $v_vin } }, limit: 1) { + id + } + } +`; + +const INSERT_OWNER = ` + mutation InsertOwner($owner: owners_insert_input!) { + insert_owners_one(object: $owner) { + id + } + } +`; + +const INSERT_JOB_WITH_LINES = ` + mutation InsertJob($job: jobs_insert_input!) { + insert_jobs_one(object: $job) { + id + joblines { id unq_seq } + } + } +`; + +module.exports = { + GET_BODYSHOP_STATUS, + GET_VEHICLE_BY_SHOP_VIN, + INSERT_OWNER, + INSERT_JOB_WITH_LINES +}; diff --git a/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js index 31ca61031..c163a51da 100644 --- a/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js @@ -4,357 +4,370 @@ 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"; // Default status if not found in bodyshop +const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; -// 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. +// 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" +]; /** - * Handles the VehicleDamageEstimateAddRq XML request from parts management. - * @param req - * @param res - * @returns {Promise<*>} + * 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 partsManagementVehicleDamageEstimateAddRq = async (req, res) => { - const { logger } = req; - const xml = req.body; - - // ── PARSE XML ──────────────────────────────────────────────────────────────── - let payload; +const parseXml = async (xml, logger) => { try { - payload = await xml2js.parseStringPromise(xml, { + return 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"); + 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 + }; } - const rq = payload.VehicleDamageEstimateAddRq; - if (!rq) { - logger.log("parts-missing-root", "error"); - return res.status(400).send("Missing "); - } + 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 { - // ── 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 }); + 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; } - // ── 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; + } catch (err) { + logger.log("parts-vehicle-fetch-failed", "warn", null, null, { error: err }); + } + return 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" - ]; +/** + * 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; + } +}; - const profile = rq.ProfileInfo || {}; - const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; +/** + * 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; - 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 - }; + 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 "); } - // ── 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 + // Extract job data + const { + shopId, + refClaimNum, 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 + classType, comment, date_exported, asgn_no, @@ -362,45 +375,73 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { asgn_date, scheduled_in, scheduled_completion, + clm_no, + status, + policy_no, + ded_amt + } = extractJobData(rq); - // 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, + if (!shopId) { + throw { status: 400, message: "Missing in XML" }; + } - // estimator - est_co_nm, - est_ct_fn, - est_ct_ln, - est_ea, + // Get default status + const defaultStatus = await getDefaultOrderStatus(shopId, logger); - // adjuster - agt_ct_fn, - agt_ct_ln, - agt_ct_ph, - agt_ea, + // 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); - // repair facility - servicing_dealer, - servicing_dealer_contact, + // Find or create relationships + const ownerid = await insertOwner(ownerData, logger); + const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger); - // nested relationships - vehicle: { data: vehicleData }, + // 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 });