From f071a5cc9e9eae98c116356a8c1abb28d7bfda80 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 28 Aug 2025 14:24:35 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - Checkpoint --- .../endpoints/lib/extractPartsTaxRates.js | 10 +- .../partsManagementDeprovisioning.js | 30 +-- .../endpoints/partsManagementProvisioning.js | 50 ++-- .../endpoints/vehicleDamageEstimateAddRq.js | 219 ++++++++++-------- .../endpoints/vehicleDamageEstimateChgRq.js | 14 +- .../partsManagement.queries.js | 2 +- 6 files changed, 173 insertions(+), 152 deletions(-) diff --git a/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js b/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js index cefe09c58..7c70d82a4 100644 --- a/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js +++ b/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js @@ -23,17 +23,21 @@ const KNOWN_PART_RATE_TYPES = [ * @returns {object} The parts tax rates object. */ -//PF: Major validation would be required on this - EMS files are inconsistent with things like 5% being passed as 5.0 or .05. -//PF: Is this data being sent by them now? +//TODO: Major validation would be required on this - EMS files are inconsistent with things like 5% being passed as 5.0 or .05. const extractPartsTaxRates = (profile = {}) => { const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; const partsTaxRates = {}; + /** + * In this context, r.RateType._ accesses the property named _ on the RateType object. + * This pattern is common when handling data parsed from XML, where element values are stored under the _ key. So, + * _ aligns to the actual value/content of the RateType field when RateType is an object (not a string). + */ for (const r of rateInfos) { const rateTypeRaw = typeof r?.RateType === "string" ? r.RateType - : typeof r?.RateType === "object" && r?.RateType._ //PF: what does _ align to? + : typeof r?.RateType === "object" && r?.RateType._ ? r.RateType._ : ""; const rateType = (rateTypeRaw || "").toUpperCase(); diff --git a/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js b/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js index 69a6c9379..6261b3a27 100644 --- a/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js +++ b/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js @@ -83,46 +83,46 @@ const deleteJobsByIds = async (jobIds) => { */ const partsManagementDeprovisioning = async (req, res) => { const { logger } = req; - const p = req.body; //Same as other file, can p be renamed to be more descriptive? + const body = req.body; if (process.env.NODE_ENV === "production") { return res.status(403).json({ error: "Deprovisioning not allowed in production environment." }); } try { - if (!p.shopId) { + if (!body.shopId) { throw { status: 400, message: "shopId is required." }; } // Fetch bodyshop and check external_shop_id - const shopResp = await client.request(GET_BODYSHOP, { id: p.shopId }); + const shopResp = await client.request(GET_BODYSHOP, { id: body.shopId }); const shop = shopResp.bodyshops_by_pk; if (!shop) { - throw { status: 404, message: `Bodyshop with id ${p.shopId} not found.` }; + throw { status: 404, message: `Bodyshop with id ${body.shopId} not found.` }; } if (!shop.external_shop_id) { throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." }; } logger.log("admin-delete-shop", "debug", null, null, { - shopId: p.shopId, + shopId: body.shopId, shopname: shop.shopname, ioadmin: true }); // Get vendors - const vendorsResp = await client.request(GET_VENDORS, { shopId: p.shopId }); + const vendorsResp = await client.request(GET_VENDORS, { shopId: body.shopId }); const deletedVendors = vendorsResp.vendors.map((v) => v.name); // Get associated users - const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: p.shopId }); + const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: body.shopId }); const associatedUsers = assocResp.associations.map((assoc) => ({ authId: assoc.user.authid, email: assoc.user.email })); // Delete associations for the shop - const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: p.shopId }); + const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: body.shopId }); const associationsDeleted = assocDeleteResp.delete_associations.affected_rows; // For each user, check if they have remaining associations; if not, delete user and Firebase account @@ -138,23 +138,23 @@ const partsManagementDeprovisioning = async (req, res) => { } // Get all job ids for this shop, then delete joblines and jobs (joblines first) - const jobIds = await getJobIdsForShop(p.shopId); + const jobIds = await getJobIdsForShop(body.shopId); const joblinesDeleted = await deleteJoblinesForJobs(jobIds); const jobsDeleted = await deleteJobsByIds(jobIds); // Delete any audit trail entries tied to this bodyshop to avoid FK violations - const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: p.shopId }); + const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: body.shopId }); const auditDeleted = auditResp.delete_audit_trail.affected_rows; // Delete vendors - await deleteVendorsByShop(p.shopId); + await deleteVendorsByShop(body.shopId); // Delete shop - await deleteBodyshop(p.shopId); + await deleteBodyshop(body.shopId); // Summary log logger.log("admin-delete-shop-summary", "info", null, null, { - shopId: p.shopId, + shopId: body.shopId, shopname: shop.shopname, associationsDeleted, deletedUsers, @@ -165,8 +165,8 @@ const partsManagementDeprovisioning = async (req, res) => { }); return res.status(200).json({ - message: `Bodyshop ${p.shopId} and associated resources deleted successfully.`, - deletedShop: { id: p.shopId, name: shop.shopname }, + message: `Bodyshop ${body.shopId} and associated resources deleted successfully.`, + deletedShop: { id: body.shopId, name: shop.shopname }, deletedAssociationsCount: associationsDeleted, deletedUsers: deletedUsers, deletedVendors: deletedVendors, diff --git a/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js b/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js index 92c265dda..857b2d6c1 100644 --- a/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js +++ b/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js @@ -139,12 +139,11 @@ const insertUserAssociation = async (uid, email, shopId) => { */ const partsManagementProvisioning = async (req, res) => { const { logger } = req; - const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; - // Can p be renamed to be more descriptive? + const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; try { - await ensureEmailNotRegistered(p.userEmail); - requireFields(p, [ + await ensureEmailNotRegistered(body.userEmail); + requireFields(body, [ "external_shop_id", "shopname", "address1", @@ -156,27 +155,27 @@ const partsManagementProvisioning = async (req, res) => { "phone", "userEmail" ]); - await ensureExternalIdUnique(p.external_shop_id); + await ensureExternalIdUnique(body.external_shop_id); - logger.log("admin-create-shop-user", "debug", p.userEmail, null, { + logger.log("admin-create-shop-user", "debug", body.userEmail, null, { request: req.body, ioadmin: true }); const shopInput = { - shopname: p.shopname, - address1: p.address1, - address2: p.address2 || null, - city: p.city, - state: p.state, - zip_post: p.zip_post, - country: p.country, - email: p.email, - external_shop_id: p.external_shop_id, - timezone: p.timezone || DefaultNewShop.timezone, - phone: p.phone, + shopname: body.shopname, + address1: body.address1, + address2: body.address2 || null, + city: body.city, + state: body.state, + zip_post: body.zip_post, + country: body.country, + email: body.email, + external_shop_id: body.external_shop_id, + timezone: body.timezone || DefaultNewShop.timezone, + phone: body.phone, logo_img_path: { - src: p.logoUrl, + src: body.logoUrl, width: "", height: "", headerMargin: DefaultNewShop.logo_img_path.headerMargin @@ -201,7 +200,7 @@ const partsManagementProvisioning = async (req, res) => { appt_alt_transport: DefaultNewShop.appt_alt_transport, md_jobline_presets: DefaultNewShop.md_jobline_presets, vendors: { - data: p.vendors.map((v) => ({ //Many of these will be empty, but good to call out explicitly to self document. + data: body.vendors.map((v) => ({ name: v.name, street1: v.street1 || null, street2: v.street2 || null, @@ -222,23 +221,22 @@ const partsManagementProvisioning = async (req, res) => { }; const newShopId = await insertBodyshop(shopInput); - const userRecord = await createFirebaseUser(p.userEmail, p.userPassword); + const userRecord = await createFirebaseUser(body.userEmail, body.userPassword); let resetLink = null; - if (!p.userPassword) resetLink = await generateResetLink(p.userEmail); + if (!body.userPassword) resetLink = await generateResetLink(body.userEmail); - const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId); - //Association can be included in shop creation call, but this is also prescriptive and fine. + const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId); return res.status(200).json({ - shop: { id: newShopId, shopname: p.shopname }, + shop: { id: newShopId, shopname: body.shopname }, user: { id: createdUser.id, email: createdUser.email, - resetLink: resetLink || undefined //Does WE know to expect this? + resetLink: resetLink || undefined } }); } catch (err) { - logger.log("admin-create-shop-user-error", "error", p.userEmail, null, { + logger.log("admin-create-shop-user-error", "error", body.userEmail, null, { message: err.message, detail: err.detail || err }); diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index ed7084953..cc88cf78f 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -14,7 +14,7 @@ const { } = require("../partsManagement.queries"); // Defaults -const FALLBACK_DEFAULT_ORDER_STATUS = "Open"; +const FALLBACK_DEFAULT_JOB_STATUS = "Open"; /** * Fetches the default order status for a bodyshop. @@ -22,13 +22,13 @@ const FALLBACK_DEFAULT_ORDER_STATUS = "Open"; * @param {object} logger - The logger instance. * @returns {Promise} The default status or fallback. */ -const getDefaultOrderStatus = async (shopId, logger) => { +const getDefaultJobStatus = 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; //I think this is intended to be called job status, not order status. + return bodyshop_by_pk?.md_ro_statuses?.default_imported || FALLBACK_DEFAULT_JOB_STATUS; } catch (err) { logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err }); - return FALLBACK_DEFAULT_ORDER_STATUS; + return FALLBACK_DEFAULT_JOB_STATUS; } }; /** @@ -66,6 +66,7 @@ const extractJobData = (rq) => { return { shopId: rq.ShopID || rq.shopId, + // status: ci.ClaimStatus || null, Proper, setting it default for now refClaimNum: rq.RefClaimNum, ciecaid: rq.RqUID || null, // Pull Cieca_ttl from ClaimInfo per schema/sample @@ -81,8 +82,6 @@ const extractJobData = (rq) => { scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null, scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null, clm_no: ci.ClaimNum || null, - // status: ci.ClaimStatus || null, Proper, setting it default for now - status: FALLBACK_DEFAULT_ORDER_STATUS, policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null, ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0) }; @@ -101,17 +100,19 @@ const extractOwnerData = (rq, shopId) => { const personName = personInfo.PersonName || {}; const address = personInfo.Communications?.Address || {}; - let ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph; + let ownr_ph1, ownr_ph2, ownr_ea; 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; //PF: Should document this logic. 1 and 2 don't typically indicate type in EMS. This makes sense, but good to document. + // TODO: Should document this logic. 1 and 2 don't + // typically indicate type in EMS. This makes sense, but good to document. + 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; + // if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone; } return { @@ -127,8 +128,8 @@ const extractOwnerData = (rq, shopId) => { ownr_ctry: address.Country || null, ownr_ph1, ownr_ph2, - ownr_ea, - ownr_alt_ph //PF: This is not in the DB, if this object is inserted in place, this will fail. + 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 @@ -159,38 +160,40 @@ const extractEstimatorData = (rq) => { * @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, //PF: I dont think we display agt_ct_* fields in app. Have they typically been sending data here? - 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 - }; -}; +// const extractAdjusterData = (rq) => { +// const adjParty = rq.AdminInfo?.Adjuster?.Party || {}; +// const adjComms = Array.isArray(adjParty.ContactInfo?.Communications) +// ? adjParty.ContactInfo.Communications +// : [adjParty.ContactInfo?.Communications || {}]; +// +// return { +// //TODO: I dont think we display agt_ct_* fields in app. Have they typically been sending data here? +// 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, //PF: The servicing dealer fields are a relic from synergy for a few folks - //PF: I suspect RF data could be ignored since they are the RF. - servicing_dealer_contact: - rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null - }; -}; +// 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, +// // TODO: The servicing dealer fields are a relic from synergy for a few folks +// // TODO: I suspect RF data could be ignored since they are the RF. +// servicing_dealer_contact: +// rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null +// }; +// }; /** * Extracts loss information from the XML request. @@ -204,10 +207,12 @@ const extractLossInfo = (rq) => { loss_date: loss.LossDateTime || null, loss_type: custom.LossTypeCode || null, loss_desc: custom.LossTypeDesc || null - // primary_poi: loss.PrimaryPOI?.POICode || null, //PF: These map back to area_of_impact.impact_# - // secondary_poi: loss.SecondaryPOI?.POICode || null, + // area_of_impact: { + // impact_1: loss.PrimaryPOI?.POICode || null, + // imact_2 :loss.SecondaryPOI?.POICode || null, + // }, + // tlosind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null, // damage_memo: loss.DamageMemo || null, //(maybe ins_memo) - // total_loss_ind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null // New //PF: tlosind i believe is our field. }; }; @@ -289,9 +294,11 @@ const extractVehicleData = (rq, shopId) => { v_color: exterior.Color?.ColorName || null, v_bstyle: desc.BodyStyle || null, v_engine: desc.EngineDesc || null, - v_options: desc.SubModelDesc || null, //PF: Need to confirm with exact data, but this is typically a list of options. Not used AFAIK. + // TODO Need to confirm with exact data, but this is typically a list of options. Not used AFAIK. + v_options: desc.SubModelDesc || null, v_type: desc.FuelType || null, - v_cond: rq.VehicleInfo?.Condition?.DrivableInd, //PF: there is a separate driveable flag on the job. + // TODO there is a separate driveable flag on the job. + 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, @@ -343,7 +350,8 @@ const extractJobLines = (rq) => { line.ManualLineInd === true || line.ManualLineInd === 1 || line.ManualLineInd === "1" || - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y"); //PF: manual line tracks manual in IO or not, this woudl presumably always be false + // TODO: manual line tracks manual in IO or not, this woudl presumably always be false + (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y"); } else { lineOut.manual_line = null; } @@ -356,8 +364,10 @@ const extractJobLines = (rq) => { const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null; lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1; - lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null; //PF: if aftermarket part, we have alt_part_no to capture. - lineOut.db_price = isNaN(price) ? 0 : price; //PF: the Db and act price often are different. These should map back to their EMS equivalents. + //TODO: if aftermarket part, we have alt_part_no to capture. + lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null; + //TODO: the Db and act price often are different. These should map back to their EMS equivalents. + lineOut.db_price = isNaN(price) ? 0 : price; lineOut.act_price = isNaN(price) ? 0 : price; // Tax flag from PartInfo.TaxableInd when provided @@ -373,8 +383,11 @@ const extractJobLines = (rq) => { partInfo.TaxableInd === "1" || (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y"); } - } else if (hasSublet) { - const amt = parseFloat(subletInfo.SubletAmount || 0); //PF: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically could. May require additional discussion. + } + //TODO: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically + // could.May require additional discussion. + else if (hasSublet) { + const amt = parseFloat(subletInfo.SubletAmount || 0); lineOut.part_type = "PAS"; // Sublet as parts-as-service lineOut.part_qty = 1; lineOut.act_price = isNaN(amt) ? 0 : amt; @@ -390,12 +403,14 @@ const extractJobLines = (rq) => { if (hasLabor) { lineOut.mod_lbr_ty = laborInfo.LaborType || null; lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs; - lineOut.lbr_op = laborInfo.LaborOperation || null; //PF: can add lbr_op_desc according to mapping available in new partner. + //TODO: can add lbr_op_desc according to mapping available in new partner. + lineOut.lbr_op = laborInfo.LaborOperation || null; lineOut.lbr_amt = isNaN(amt) ? 0 : amt; } - //PF: what's the BMS logic for this? Body and refinish operations can often happen to the same part, but most systems output a second line for the refinish labor. - //PF: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC. + //TODO: what's the BMS logic for this? Body and refinish operations can often happen to the same part, + // but most systems output a second line for the refinish labor. + //TODO: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC. // Refinish labor (if present) recorded on the same line using secondary labor fields const rHrs = parseFloat(refinishInfo.LaborHours || 0); const rAmt = parseFloat(refinishInfo.LaborAmt || 0); @@ -406,9 +421,9 @@ const extractJobLines = (rq) => { !isNaN(rAmt) || !!refinishInfo.LaborOperation); if (hasRefinish) { - lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //PF: _j fields indicate judgement, and are bool type. - lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;//PF: _j fields indicate judgement, and are bool type. - lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //PF: _j fields indicate judgement, and are bool type. + lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //TODO: _j fields indicate judgement, and are bool type. + lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; //TODO: _j fields indicate judgement, and are bool type. + lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //TODO: _j fields indicate judgement, and are bool type. // Aggregate refinish labor amount into the total labor amount for the line if (!isNaN(rAmt)) { lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt; @@ -424,26 +439,26 @@ const extractJobLines = (rq) => { }; // 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; -}; +// 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. @@ -462,24 +477,26 @@ const insertOwner = async (ownerInput, logger) => { }; // 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?.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 (Number.isFinite(jl.lbr_amt)) { - labor += jl.lbr_amt; - } - } - const total = parts + labor; //PF: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system. - return Number.isFinite(total) && total > 0 ? total : 0; -}; +// const computeLinesTotal = (joblines = []) => { +// let parts = 0; +// let labor = 0; +// for (const jl of joblines) { +// if (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 (Number.isFinite(jl.lbr_amt)) { +// labor += jl.lbr_amt; +// } +// } +// const total = parts + labor; +// +// //TODO: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system. +// return Number.isFinite(total) && total > 0 ? total : 0; +// }; /** * Handles the VehicleDamageEstimateAddRq XML request from parts management. @@ -516,9 +533,9 @@ const vehicleDamageEstimateAddRq = async (req, res) => { scheduled_in, scheduled_completion, clm_no, - status, policy_no, ded_amt + // status, } = extractJobData(rq); if (!shopId) { @@ -526,22 +543,22 @@ const vehicleDamageEstimateAddRq = async (req, res) => { } // Get default status - const defaultStatus = await getDefaultOrderStatus(shopId, logger); //This likely should be get default job status, not order. + const defaultStatus = await getDefaultJobStatus(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 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); + // const grandTotal = extractGrandTotal(rq); + // const computedTotal = grandTotal ?? computeLinesTotal(joblinesData); // Find or create relationships const ownerid = await insertOwner(ownerData, logger); @@ -560,8 +577,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => { class: classType, parts_tax_rates, clm_no, - status: status || defaultStatus, - clm_total: computedTotal || null, + status: defaultStatus, + clm_total: 0, // computedTotal || null, policy_no, ded_amt, comment, @@ -576,8 +593,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => { ...lossInfo, ...ownerData, ...estimatorData, - ...adjusterData, - ...repairFacilityData, + // ...adjusterData, + // ...repairFacilityData, // Inline vehicle data v_vin: vehicleData.v_vin, v_model_yr: vehicleData.v_model_yr, diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js index 62acac4dd..b69b2466c 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js @@ -38,13 +38,15 @@ const findJob = async (shopId, jobId, logger) => { const extractUpdatedJobData = (rq) => { const doc = rq.DocumentInfo || {}; const claim = rq.ClaimInfo || {}; - //PF: In the full BMS world, much more can change. + //TODO: In the full BMS world, much more can change, this will need to be expanded + // before it can be considered an generic BMS importer, currently it is bespoke to webest const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null; const out = { comment: doc.Comment || null, clm_no: claim.ClaimNum || null, - status: claim.ClaimStatus || null, + // TODO: Commented out so they do not blow over with 'Auth Cust' + // status: claim.ClaimStatus || null, policy_no: policyNo }; @@ -239,7 +241,6 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => { } } // --- End fetch current notes --- - //PF: These are several different calls that can be pulled together to make the operation faster. const updatedJobData = extractUpdatedJobData(rq); const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes); @@ -247,12 +248,13 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => { await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData }); - //PF: for changed lines, are they deleted and then reinserted? - //PF: Updated lines should get an upsert to update things like desc, price, etc. + //TODO: for changed lines, are they deleted and then reinserted? + //TODO: Updated lines should get an upsert to update things like desc, price, etc. if (deletedLineIds?.length || updatedSeqs?.length) { const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])])); if (allToDelete.length) { - await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete }); //PF: appears to soft delete updated lines as well. + await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete }); + //TODO: appears to soft delete updated lines as well. } } diff --git a/server/integrations/partsManagement/partsManagement.queries.js b/server/integrations/partsManagement/partsManagement.queries.js index 683c34516..afcc9a43c 100644 --- a/server/integrations/partsManagement/partsManagement.queries.js +++ b/server/integrations/partsManagement/partsManagement.queries.js @@ -2,7 +2,7 @@ const GET_BODYSHOP_STATUS = ` query GetBodyshopStatus($id: uuid!) { bodyshops_by_pk(id: $id) { - md_order_statuses + md_ro_statuses } } `;