diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index 441f6ed56..8970ef5af 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -16,8 +16,6 @@ const { // Defaults const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; -// Config: include labor lines and labor in totals (default true) -const INCLUDE_LABOR = true; /** * Fetches the default order status for a bodyshop. * @param {string} shopId - The bodyshop UUID. @@ -327,10 +325,6 @@ const extractJobLines = (rq) => { const refinishInfo = line.RefinishLaborInfo || {}; const subletInfo = line.SubletInfo || {}; - let jobLineType = "PART"; - if (Object.keys(subletInfo).length > 0) jobLineType = "SUBLET"; - else if (Object.keys(laborInfo).length > 0 && Object.keys(partInfo).length === 0) jobLineType = "LABOR"; - const base = { line_no: parseInt(line.LineNum || 0, 10), unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), @@ -339,134 +333,87 @@ const extractJobLines = (rq) => { notes: line.LineMemo || null }; - if (jobLineType === "PART") { + const lineOut = { ...base }; + + // Manual line flag coercion + if (line.ManualLineInd !== undefined) { + lineOut.manual_line = + line.ManualLineInd === true || + line.ManualLineInd === 1 || + line.ManualLineInd === "1" || + (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y"); + } else { + lineOut.manual_line = null; + } + + // Parts (preferred) or Sublet (fallback when no PartInfo) + const hasPart = Object.keys(partInfo).length > 0; + const hasSublet = Object.keys(subletInfo).length > 0; + + if (hasPart) { const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); - // Push the part line with ONLY part pricing/fields - out.push({ - ...base, - part_type: partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null, - part_qty: parseFloat(partInfo.Quantity || 0) || 1, - oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null, - db_price: price, - act_price: price, - // Tax flag from PartInfo.TaxableInd when provided - ...(partInfo.TaxableInd !== undefined && + 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; + lineOut.db_price = isNaN(price) ? 0 : price; + lineOut.act_price = isNaN(price) ? 0 : price; + + // Tax flag from PartInfo.TaxableInd when provided + if ( + partInfo.TaxableInd !== undefined && (typeof partInfo.TaxableInd === "string" || typeof partInfo.TaxableInd === "number" || typeof partInfo.TaxableInd === "boolean") - ? { - tax_part: - partInfo.TaxableInd === true || - partInfo.TaxableInd === 1 || - partInfo.TaxableInd === "1" || - (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y") - } - : {}), - // Manual line flag coercion - ...(line.ManualLineInd !== undefined - ? { - manual_line: - line.ManualLineInd === true || - line.ManualLineInd === 1 || - line.ManualLineInd === "1" || - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") - } - : { manual_line: null }) - }); - - // If labor is present on the same damage line, split it to a separate LABOR jobline - // TODO: Verify with patrick this is desired. - if (INCLUDE_LABOR) { - const hrs = parseFloat(laborInfo.LaborHours || 0); - const amt = parseFloat(laborInfo.LaborAmt || 0); - const hasLabor = - (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || - (!isNaN(hrs) && hrs !== 0) || - (!isNaN(amt) && amt !== 0); - if (hasLabor) { - out.push({ - ...base, - // tweak unq_seq to avoid collisions in later upserts - unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000, - mod_lbr_ty: laborInfo.LaborType || null, - mod_lb_hrs: isNaN(hrs) ? 0 : hrs, - lbr_op: laborInfo.LaborOperation || null, - lbr_amt: isNaN(amt) ? 0 : amt, - ...(line.ManualLineInd !== undefined - ? { - manual_line: - line.ManualLineInd === true || - line.ManualLineInd === 1 || - line.ManualLineInd === "1" || - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") - } - : { manual_line: null }) - }); - } + ) { + lineOut.tax_part = + partInfo.TaxableInd === true || + partInfo.TaxableInd === 1 || + partInfo.TaxableInd === "1" || + (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y"); } - } else if (jobLineType === "SUBLET") { - out.push({ - ...base, - part_type: "PAS", - part_qty: 1, - act_price: parseFloat(subletInfo.SubletAmount || 0), - // Manual line flag - ...(line.ManualLineInd !== undefined - ? { - manual_line: - line.ManualLineInd === true || - line.ManualLineInd === 1 || - line.ManualLineInd === "1" || - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") - } - : { manual_line: null }) - }); - } else if (INCLUDE_LABOR) { - // Labor-only line (only when enabled) - out.push({ - ...base, - mod_lbr_ty: laborInfo.LaborType || null, - mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0), - lbr_op: laborInfo.LaborOperation || null, - lbr_amt: parseFloat(laborInfo.LaborAmt || 0), - ...(line.ManualLineInd !== undefined - ? { - manual_line: - line.ManualLineInd === true || - line.ManualLineInd === 1 || - line.ManualLineInd === "1" || - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") - } - : { manual_line: null }) - }); + } 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; } - // Add a separate refinish labor line if present and enabled - if (INCLUDE_LABOR && Object.keys(refinishInfo).length > 0) { - const hrs = parseFloat(refinishInfo.LaborHours || 0); - const amt = parseFloat(refinishInfo.LaborAmt || 0); - if (!isNaN(hrs) || !isNaN(amt)) { - out.push({ - ...base, - // tweak unq_seq to avoid collisions in later upserts - unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, - line_desc: base.line_desc || "Refinish", - mod_lbr_ty: "LAR", - mod_lb_hrs: isNaN(hrs) ? 0 : hrs, - lbr_op: refinishInfo.LaborOperation || null, - lbr_amt: isNaN(amt) ? 0 : amt, - ...(line.ManualLineInd !== undefined - ? { - manual_line: - line.ManualLineInd === true || - line.ManualLineInd === 1 || - line.ManualLineInd === "1" || - (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") - } - : { manual_line: null }) - }); - } + // Primary labor (if present) recorded on the same line + const hrs = parseFloat(laborInfo.LaborHours || 0); + const amt = parseFloat(laborInfo.LaborAmt || 0); + const hasLabor = + (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || + (!isNaN(hrs) && hrs !== 0) || + (!isNaN(amt) && amt !== 0); + if (hasLabor) { + lineOut.mod_lbr_ty = laborInfo.LaborType || null; + lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs; + lineOut.lbr_op = laborInfo.LaborOperation || null; + lineOut.lbr_amt = isNaN(amt) ? 0 : amt; } + + // 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); + const hasRefinish = + Object.keys(refinishInfo).length > 0 && + ((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) || + !isNaN(rHrs) || + !isNaN(rAmt) || + !!refinishInfo.LaborOperation); + if (hasRefinish) { + lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; + lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; + lineOut.lbr_op_j = refinishInfo.LaborOperation || null; + // 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; + } + if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum; + if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum; + } + + out.push(lineOut); } return out; @@ -522,7 +469,7 @@ const computeLinesTotal = (joblines = []) => { } else if (!jl.part_type && Number.isFinite(jl.act_price)) { parts += jl.act_price; } - if (INCLUDE_LABOR && Number.isFinite(jl.lbr_amt)) { + if (Number.isFinite(jl.lbr_amt)) { labor += jl.lbr_amt; } } diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js index 254224385..f2aafbd8d 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js @@ -56,11 +56,10 @@ const extractUpdatedJobData = (rq) => { }; /** - * Extracts updated job lines from the request payload, mirroring the AddRq splitting rules: - * - PART lines carry only part pricing (act_price) and related fields - * - If LaborInfo exists on a part line, add a separate LABOR line at unq_seq + 400000 - * - If RefinishLaborInfo exists, add a separate LABOR line at unq_seq + 500000 with mod_lbr_ty=LAR - * - SUBLET lines become PAS part_type with act_price=SubletAmount + * Extracts updated job lines from the request payload without splitting parts and labor: + * - Keep part and labor on the same jobline + * - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt + * - SUBLET-only lines become PAS part_type with act_price = SubletAmount */ const extractUpdatedJobLines = (addsChgs = {}, jobId) => { const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}]; @@ -88,74 +87,74 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => { manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null }; + const lineOut = { ...base }; + const hasPart = Object.keys(partInfo).length > 0; - const hasLaborOnly = Object.keys(laborInfo).length > 0 && !hasPart && Object.keys(subletInfo).length === 0; const hasSublet = Object.keys(subletInfo).length > 0; if (hasPart) { const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); - out.push({ - ...base, - part_type: partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null, - part_qty: parseFloat(partInfo.Quantity || 0) || 1, - oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null, - db_price: isNaN(price) ? 0 : price, - act_price: isNaN(price) ? 0 : price - }); + lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null; + lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1; + lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null; + lineOut.db_price = isNaN(price) ? 0 : price; + lineOut.act_price = isNaN(price) ? 0 : price; - // Split any attached labor on the part line into a derived labor jobline - const hrs = parseFloat(laborInfo.LaborHours || 0); - const amt = parseFloat(laborInfo.LaborAmt || 0); - const hasLabor = - (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || - (!isNaN(hrs) && hrs !== 0) || - (!isNaN(amt) && amt !== 0); - if (hasLabor) { - out.push({ - ...base, - unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000, - mod_lbr_ty: laborInfo.LaborType || null, - mod_lb_hrs: isNaN(hrs) ? 0 : hrs, - lbr_op: laborInfo.LaborOperation || null, - lbr_amt: isNaN(amt) ? 0 : amt - }); + // Optional: taxability flag for parts + if ( + partInfo.TaxableInd !== undefined && + (typeof partInfo.TaxableInd === "string" || + typeof partInfo.TaxableInd === "number" || + typeof partInfo.TaxableInd === "boolean") + ) { + lineOut.tax_part = + partInfo.TaxableInd === true || + partInfo.TaxableInd === 1 || + partInfo.TaxableInd === "1" || + (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y"); } } else if (hasSublet) { - out.push({ - ...base, - part_type: "PAS", - part_qty: 1, - act_price: parseFloat(subletInfo.SubletAmount || 0) || 0 - }); + const amt = parseFloat(subletInfo.SubletAmount || 0); + lineOut.part_type = "PAS"; + lineOut.part_qty = 1; + lineOut.act_price = isNaN(amt) ? 0 : amt; } - // Labor-only line (no PartInfo): still upsert as a labor entry - if (hasLaborOnly) { - out.push({ - ...base, - mod_lbr_ty: laborInfo.LaborType || null, - mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0) || 0, - lbr_op: laborInfo.LaborOperation || null, - lbr_amt: parseFloat(laborInfo.LaborAmt || 0) || 0 - }); + // Primary labor on same line + const hrs = parseFloat(laborInfo.LaborHours || 0); + const amt = parseFloat(laborInfo.LaborAmt || 0); + const hasLabor = + (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || + (!isNaN(hrs) && hrs !== 0) || + (!isNaN(amt) && amt !== 0); + if (hasLabor) { + lineOut.mod_lbr_ty = laborInfo.LaborType || null; + lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs; + lineOut.lbr_op = laborInfo.LaborOperation || null; + lineOut.lbr_amt = isNaN(amt) ? 0 : amt; } - // Separate refinish labor line - if (Object.keys(refinishInfo).length > 0) { - const rHrs = parseFloat(refinishInfo.LaborHours || 0); - const rAmt = parseFloat(refinishInfo.LaborAmt || 0); - if (!isNaN(rHrs) || !isNaN(rAmt)) { - out.push({ - ...base, - unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, - line_desc: base.line_desc || "Refinish", - mod_lbr_ty: "LAR", - mod_lb_hrs: isNaN(rHrs) ? 0 : rHrs, - lbr_op: refinishInfo.LaborOperation || null, - lbr_amt: isNaN(rAmt) ? 0 : rAmt - }); + // Refinish labor on same line using secondary fields; aggregate amount into lbr_amt + const rHrs = parseFloat(refinishInfo.LaborHours || 0); + const rAmt = parseFloat(refinishInfo.LaborAmt || 0); + const hasRefinish = + Object.keys(refinishInfo).length > 0 && + ((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) || + !isNaN(rHrs) || + !isNaN(rAmt) || + !!refinishInfo.LaborOperation); + if (hasRefinish) { + lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; + lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; + lineOut.lbr_op_j = refinishInfo.LaborOperation || null; + if (!isNaN(rAmt)) { + lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt; } + if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum; + if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum; } + + out.push(lineOut); } return out;