From b2184a2d1166fa0e75dca12900e5ab4253b4bfba Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 20 Nov 2025 21:57:49 -0500 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration Checkpoint --- server/rr/rr-calculate-allocations.js | 789 +++++++++++++++++++++++++ server/rr/rr-job-export.js | 2 +- server/rr/rr-register-socket-events.js | 2 +- server/rr/rr-service-vehicles.js | 25 +- 4 files changed, 794 insertions(+), 24 deletions(-) create mode 100644 server/rr/rr-calculate-allocations.js diff --git a/server/rr/rr-calculate-allocations.js b/server/rr/rr-calculate-allocations.js new file mode 100644 index 000000000..1625e4137 --- /dev/null +++ b/server/rr/rr-calculate-allocations.js @@ -0,0 +1,789 @@ +const { GraphQLClient } = require("graphql-request"); +const Dinero = require("dinero.js"); +const _ = require("lodash"); + +const queries = require("../graphql-client/queries"); +const CreateRRLogEvent = require("./rr-logger-event"); +const InstanceManager = require("../utils/instanceMgr").default; + +const { DiscountNotAlreadyCounted } = InstanceManager({ + imex: require("../job/job-totals"), + rome: require("../job/job-totals-USA") +}); + +/** + * Dinero helpers for safe, compact logging. + */ +const summarizeMoney = (dinero) => { + if (!dinero || typeof dinero.getAmount !== "function") return { cents: null }; + return { cents: dinero.getAmount() }; +}; + +const summarizeHash = (hash) => + Object.entries(hash || {}).map(([center, dinero]) => ({ + center, + ...summarizeMoney(dinero) + })); + +const summarizeTaxAllocations = (tax) => + Object.entries(tax || {}).map(([key, entry]) => ({ + key, + sale: summarizeMoney(entry?.sale), + cost: summarizeMoney(entry?.cost) + })); + +const summarizeAllocationsArray = (arr) => + (arr || []).map((a) => ({ + center: a.center || a.tax || null, + tax: a.tax || null, + sale: summarizeMoney(a.sale), + cost: summarizeMoney(a.cost) + })); + +/** + * Thin logger wrapper: always uses CreateRRLogEvent, + * with structured data passed via meta arg. + */ +function createDebugLogger(connectionData) { + return (msg, meta, level = "DEBUG") => { + const baseMsg = `[CdkCalculateAllocations] ${msg}`; + + CreateRRLogEvent(connectionData, level, baseMsg, meta !== undefined ? meta : undefined); + }; +} + +/** + * Query job data for allocations. + */ +async function QueryJobData(connectionData, token, jobid) { + CreateRRLogEvent(connectionData, "DEBUG", "Querying job data for allocations", { jobid }); + + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); + const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid }); + + return result.jobs_by_pk; +} + +/** + * Build tax allocation object depending on environment (imex vs rome). + */ +function buildTaxAllocations(bodyshop, job) { + return InstanceManager({ + executeFunction: true, + deubg: true, + args: [], + imex: () => ({ + state: { + center: bodyshop.md_responsibility_centers.taxes.state.name, + sale: Dinero(job.job_totals.totals.state_tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.state, + costCenter: bodyshop.md_responsibility_centers.taxes.state + }, + federal: { + center: bodyshop.md_responsibility_centers.taxes.federal.name, + sale: Dinero(job.job_totals.totals.federal_tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.federal, + costCenter: bodyshop.md_responsibility_centers.taxes.federal + } + }), + rome: () => ({ + tax_ty1: { + center: bodyshop.md_responsibility_centers.taxes.tax_ty1.name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty1Tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty1, + costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty1 + }, + tax_ty2: { + center: bodyshop.md_responsibility_centers.taxes.tax_ty2.name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty2Tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty2, + costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty2 + }, + tax_ty3: { + center: bodyshop.md_responsibility_centers.taxes.tax_ty3.name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty3Tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty3, + costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty3 + }, + tax_ty4: { + center: bodyshop.md_responsibility_centers.taxes.tax_ty4.name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty4Tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty4, + costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty4 + }, + tax_ty5: { + center: bodyshop.md_responsibility_centers.taxes.tax_ty5.name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty5Tax), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty5, + costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty5 + } + }) + }); +} + +/** + * Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence. + */ +function buildProfitCenterHash(job, debugLog) { + let hasMapaLine = false; + let hasMashLine = false; + + const profitCenterHash = job.joblines.reduce((acc, val) => { + // MAPA line? + if (val.db_ref === "936008") { + if (!hasMapaLine) { + debugLog("Detected existing MAPA line in joblines", { + joblineId: val.id, + db_ref: val.db_ref + }); + } + hasMapaLine = true; + } + + // MASH line? + if (val.db_ref === "936007") { + if (!hasMashLine) { + debugLog("Detected existing MASH line in joblines", { + joblineId: val.id, + db_ref: val.db_ref + }); + } + hasMashLine = true; + } + + // Parts + if (val.profitcenter_part) { + if (!acc[val.profitcenter_part]) acc[val.profitcenter_part] = Dinero(); + + let dineroAmount = Dinero({ + amount: Math.round(val.act_price * 100) + }).multiply(val.part_qty || 1); + + const hasDiscount = (val.prt_dsmk_m && val.prt_dsmk_m !== 0) || (val.prt_dsmk_p && val.prt_dsmk_p !== 0); + + if (hasDiscount && DiscountNotAlreadyCounted(val, job.joblines)) { + const moneyDiscount = val.prt_dsmk_m + ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) + : Dinero({ + amount: Math.round(val.act_price * 100) + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1); + + dineroAmount = dineroAmount.add(moneyDiscount); + } + + acc[val.profitcenter_part] = acc[val.profitcenter_part].add(dineroAmount); + } + + // Labor + if (val.profitcenter_labor && val.mod_lbr_ty) { + if (!acc[val.profitcenter_labor]) acc[val.profitcenter_labor] = Dinero(); + + const rateKey = `rate_${val.mod_lbr_ty.toLowerCase()}`; + const rate = job[rateKey]; + + acc[val.profitcenter_labor] = acc[val.profitcenter_labor].add( + Dinero({ + amount: Math.round(rate * 100) + }).multiply(val.mod_lb_hrs) + ); + } + + return acc; + }, {}); + + debugLog("profitCenterHash after joblines", { + hasMapaLine, + hasMashLine, + centers: summarizeHash(profitCenterHash) + }); + + return { profitCenterHash, hasMapaLine, hasMashLine }; +} + +/** + * Build costCenterHash from bills and timetickets. + */ +function buildCostCenterHash(job, selectedDmsAllocationConfig, disablebillwip, debugLog) { + let costCenterHash = {}; + + // 1) Bills -> costs + debugLog("disablebillwip flag", { disablebillwip }); + + if (!disablebillwip) { + costCenterHash = job.bills.reduce((billAcc, bill) => { + bill.billlines.forEach((line) => { + const targetCenter = selectedDmsAllocationConfig.costs[line.cost_center]; + if (!targetCenter) return; + + if (!billAcc[targetCenter]) billAcc[targetCenter] = Dinero(); + + const lineDinero = Dinero({ + amount: Math.round((line.actual_cost || 0) * 100) + }) + .multiply(line.quantity) + .multiply(bill.is_credit_memo ? -1 : 1); + + billAcc[targetCenter] = billAcc[targetCenter].add(lineDinero); + }); + return billAcc; + }, {}); + } + + debugLog("costCenterHash after bills (pre-timetickets)", { + centers: summarizeHash(costCenterHash) + }); + + // 2) Timetickets -> costs + job.timetickets.forEach((ticket) => { + const effectiveHours = + ticket.employee && ticket.employee.flat_rate ? ticket.productivehrs || 0 : ticket.actualhrs || 0; + + const ticketTotal = Dinero({ + amount: Math.round(ticket.rate * effectiveHours * 100) + }); + + const targetCenter = selectedDmsAllocationConfig.costs[ticket.ciecacode]; + if (!targetCenter) return; + + if (!costCenterHash[targetCenter]) costCenterHash[targetCenter] = Dinero(); + costCenterHash[targetCenter] = costCenterHash[targetCenter].add(ticketTotal); + }); + + debugLog("costCenterHash after timetickets", { + centers: summarizeHash(costCenterHash) + }); + + return costCenterHash; +} + +/** + * Add manual MAPA / MASH sales where needed. + */ +function applyMapaMashManualLines({ + job, + selectedDmsAllocationConfig, + bodyshop, + profitCenterHash, + hasMapaLine, + hasMashLine, + debugLog +}) { + // MAPA + if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) { + const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA; + const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName); + + if (mapaAccount) { + debugLog("Adding MAPA Line Manually", { + mapaAccountName, + amount: summarizeMoney(Dinero(job.job_totals.rates.mapa.total)) + }); + + if (!profitCenterHash[mapaAccountName]) profitCenterHash[mapaAccountName] = Dinero(); + profitCenterHash[mapaAccountName] = profitCenterHash[mapaAccountName].add( + Dinero(job.job_totals.rates.mapa.total) + ); + } else { + debugLog("NO MAPA ACCOUNT FOUND!!", { mapaAccountName }); + } + } + + // MASH + if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) { + const mashAccountName = selectedDmsAllocationConfig.profits.MASH; + const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName); + + if (mashAccount) { + debugLog("Adding MASH Line Manually", { + mashAccountName, + amount: summarizeMoney(Dinero(job.job_totals.rates.mash.total)) + }); + + if (!profitCenterHash[mashAccountName]) profitCenterHash[mashAccountName] = Dinero(); + profitCenterHash[mashAccountName] = profitCenterHash[mashAccountName].add( + Dinero(job.job_totals.rates.mash.total) + ); + } else { + debugLog("NO MASH ACCOUNT FOUND!!", { mashAccountName }); + } + } + + return profitCenterHash; +} + +/** + * Apply materials costing (MAPA/MASH cost side) when configured. + */ +function applyMaterialsCosting({ job, bodyshop, selectedDmsAllocationConfig, costCenterHash, debugLog }) { + const { cdk_configuration } = bodyshop || {}; + + if (!cdk_configuration?.sendmaterialscosting) return costCenterHash; + + debugLog("sendmaterialscosting enabled", { + sendmaterialscosting: cdk_configuration.sendmaterialscosting, + use_paint_scale_data: job.bodyshop.use_paint_scale_data, + mixdataLength: job.mixdata?.length || 0 + }); + + const percent = cdk_configuration.sendmaterialscosting; + + // Paint Mat (MAPA) + const mapaAccountName = selectedDmsAllocationConfig.costs.MAPA; + const mapaAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mapaAccountName); + + if (mapaAccount) { + if (!costCenterHash[mapaAccountName]) costCenterHash[mapaAccountName] = Dinero(); + + if (job.bodyshop.use_paint_scale_data === true) { + if (job.mixdata.length > 0) { + debugLog("Using mixdata for MAPA cost", { + mapaAccountName, + totalliquidcost: job.mixdata[0] && job.mixdata[0].totalliquidcost + }); + + costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add( + Dinero({ + amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100) + }) + ); + } else { + debugLog("Using percentage of MAPA total (no mixdata)", { mapaAccountName }); + + costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add( + Dinero(job.job_totals.rates.mapa.total).percentage(percent) + ); + } + } else { + debugLog("Using percentage of MAPA total (no paint scale data)", { mapaAccountName }); + + costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add( + Dinero(job.job_totals.rates.mapa.total).percentage(percent) + ); + } + } else { + debugLog("NO MAPA ACCOUNT FOUND (costs)!!", { mapaAccountName }); + } + + // Shop Mat (MASH) + const mashAccountName = selectedDmsAllocationConfig.costs.MASH; + const mashAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mashAccountName); + + if (mashAccount) { + debugLog("Adding MASH material costing", { mashAccountName }); + + if (!costCenterHash[mashAccountName]) costCenterHash[mashAccountName] = Dinero(); + costCenterHash[mashAccountName] = costCenterHash[mashAccountName].add( + Dinero(job.job_totals.rates.mash.total).percentage(percent) + ); + } else { + debugLog("NO MASH ACCOUNT FOUND (costs)!!", { mashAccountName }); + } + + return costCenterHash; +} + +/** + * Apply non-tax extras (PVRT, towing, storage, PAO). + */ +function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterHash, taxAllocations, debugLog }) { + const { ca_bc_pvrt } = job; + + // BC PVRT -> state tax + if (ca_bc_pvrt) { + debugLog("Adding PVRT to state tax allocation", { ca_bc_pvrt }); + + taxAllocations.state.sale = taxAllocations.state.sale.add(Dinero({ amount: Math.round((ca_bc_pvrt || 0) * 100) })); + } + + // Towing + if (job.towing_payable && job.towing_payable !== 0) { + const towAccountName = selectedDmsAllocationConfig.profits.TOW; + const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName); + + if (towAccount) { + debugLog("Adding towing_payable to TOW account", { + towAccountName, + towing_payable: job.towing_payable + }); + + if (!profitCenterHash[towAccountName]) profitCenterHash[towAccountName] = Dinero(); + + profitCenterHash[towAccountName] = profitCenterHash[towAccountName].add( + Dinero({ + amount: Math.round((job.towing_payable || 0) * 100) + }) + ); + } else { + debugLog("NO TOW ACCOUNT FOUND!!", { towAccountName }); + } + } + + // Storage (shares TOW account) + if (job.storage_payable && job.storage_payable !== 0) { + const storageAccountName = selectedDmsAllocationConfig.profits.TOW; + const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName); + + if (towAccount) { + debugLog("Adding storage_payable to TOW account", { + storageAccountName, + storage_payable: job.storage_payable + }); + + if (!profitCenterHash[storageAccountName]) profitCenterHash[storageAccountName] = Dinero(); + + profitCenterHash[storageAccountName] = profitCenterHash[storageAccountName].add( + Dinero({ + amount: Math.round((job.storage_payable || 0) * 100) + }) + ); + } else { + debugLog("NO STORAGE/TOW ACCOUNT FOUND!!", { storageAccountName }); + } + } + + // Bottom line adjustment -> PAO + if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) { + const otherAccountName = selectedDmsAllocationConfig.profits.PAO; + const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName); + + if (otherAccount) { + debugLog("Adding adjustment_bottom_line to PAO", { + otherAccountName, + adjustment_bottom_line: job.adjustment_bottom_line + }); + + if (!profitCenterHash[otherAccountName]) profitCenterHash[otherAccountName] = Dinero(); + + profitCenterHash[otherAccountName] = profitCenterHash[otherAccountName].add( + Dinero({ + amount: Math.round((job.adjustment_bottom_line || 0) * 100) + }) + ); + } else { + debugLog("NO PAO ACCOUNT FOUND!!", { otherAccountName }); + } + } + + return { profitCenterHash, taxAllocations }; +} + +/** + * Apply Rome-specific profile adjustments (parts + rates). + */ +function applyRomeProfileAdjustments({ + job, + bodyshop, + selectedDmsAllocationConfig, + profitCenterHash, + debugLog, + connectionData +}) { + if (!InstanceManager({ rome: true })) return profitCenterHash; + + debugLog("ROME profile adjustments block entered", { + partAdjustmentKeys: Object.keys(job.job_totals.parts.adjustments || {}), + rateKeys: Object.keys(job.job_totals.rates || {}) + }); + + // Parts adjustments + Object.keys(job.job_totals.parts.adjustments).forEach((key) => { + const accountName = selectedDmsAllocationConfig.profits[key]; + const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName); + + if (otherAccount) { + if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero(); + + profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.parts.adjustments[key])); + + debugLog("Added parts adjustment", { + key, + accountName, + adjustment: summarizeMoney(Dinero(job.job_totals.parts.adjustments[key])) + }); + } else { + CreateRRLogEvent( + connectionData, + "ERROR", + "Error encountered in CdkCalculateAllocations. Unable to find parts adjustment account.", + { accountName, key } + ); + debugLog("Missing parts adjustment account", { key, accountName }); + } + }); + + // Labor / materials adjustments + Object.keys(job.job_totals.rates).forEach((key) => { + const rate = job.job_totals.rates[key]; + if (!rate || !rate.adjustment) return; + + if (Dinero(rate.adjustment).isZero()) return; + + const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()]; + const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName); + + if (otherAccount) { + if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero(); + + profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates[key].adjustments)); + + debugLog("Added rate adjustment", { key, accountName }); + } else { + CreateRRLogEvent( + connectionData, + "ERROR", + "Error encountered in CdkCalculateAllocations. Unable to find rate adjustment account.", + { accountName, key } + ); + debugLog("Missing rate adjustment account", { key, accountName }); + } + }); + + return profitCenterHash; +} + +/** + * Build job-level profit/cost allocations for each center. + */ +function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLog) { + const centers = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)); + + const jobAllocations = centers.map((key) => { + const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === key); + const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === key); + + return { + center: key, + sale: profitCenterHash[key] || Dinero(), + cost: costCenterHash[key] || Dinero(), + profitCenter, + costCenter + }; + }); + + debugLog("jobAllocations built", summarizeAllocationsArray(jobAllocations)); + + return jobAllocations; +} + +/** + * Build tax allocations array from taxAllocations hash. + */ +function buildTaxAllocArray(taxAllocations, selectedDmsAllocationConfig, debugLog) { + const taxAllocArray = Object.keys(taxAllocations) + .filter((key) => taxAllocations[key].sale.getAmount() > 0 || taxAllocations[key].cost.getAmount() > 0) + .map((key) => { + if ( + key === "federal" && + selectedDmsAllocationConfig.gst_override && + selectedDmsAllocationConfig.gst_override !== "" + ) { + const ret = { ...taxAllocations[key], tax: key }; + ret.costCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override; + ret.profitCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override; + return ret; + } + return { ...taxAllocations[key], tax: key }; + }); + + debugLog("taxAllocArray built", summarizeAllocationsArray(taxAllocArray)); + return taxAllocArray; +} + +/** + * Build adjustment allocations (ttl_adjustment + ttl_tax_adjustment). + */ +function buildAdjustmentAllocations(job, bodyshop, debugLog) { + const ttlAdjArray = job.job_totals.totals.ttl_adjustment + ? [ + { + center: "SUB ADJ", + sale: Dinero(job.job_totals.totals.ttl_adjustment), + cost: Dinero(), + profitCenter: { + name: "SUB ADJ", + accountdesc: "SUB ADJ", + accountitem: "SUB ADJ", + accountname: "SUB ADJ", + dms_acctnumber: bodyshop.md_responsibility_centers.ttl_adjustment.dms_acctnumber + }, + costCenter: {} + } + ] + : []; + + const ttlTaxAdjArray = job.job_totals.totals.ttl_tax_adjustment + ? [ + { + center: "TAX ADJ", + sale: Dinero(job.job_totals.totals.ttl_tax_adjustment), + cost: Dinero(), + profitCenter: { + name: "TAX ADJ", + accountdesc: "TAX ADJ", + accountitem: "TAX ADJ", + accountname: "TAX ADJ", + dms_acctnumber: bodyshop.md_responsibility_centers.ttl_tax_adjustment.dms_acctnumber + }, + costCenter: {} + } + ] + : []; + + if (ttlAdjArray.length) { + debugLog("ttl_adjustment allocation added", summarizeAllocationsArray(ttlAdjArray)); + } + if (ttlTaxAdjArray.length) { + debugLog("ttl_tax_adjustment allocation added", summarizeAllocationsArray(ttlTaxAdjArray)); + } + + return { ttlAdjArray, ttlTaxAdjArray }; +} + +/** + * Core allocation calculation – Reynolds-only, Reynolds-logging only. + */ +function calculateAllocations(connectionData, job) { + const { bodyshop } = job; + const debugLog = createDebugLogger(connectionData); + + debugLog("ENTER", { + bodyshopId: bodyshop?.id, + bodyshopName: bodyshop?.name, + dms_allocation: job.dms_allocation, + hasBills: Array.isArray(job.bills) ? job.bills.length : 0, + joblines: Array.isArray(job.joblines) ? job.joblines.length : 0, + timetickets: Array.isArray(job.timetickets) ? job.timetickets.length : 0 + }); + + // 1) Tax allocations + let taxAllocations = buildTaxAllocations(bodyshop, job); + debugLog("Initial taxAllocations", summarizeTaxAllocations(taxAllocations)); + + // 2) Profit centers from job lines + MAPA/MASH detection + const { profitCenterHash: initialProfitHash, hasMapaLine, hasMashLine } = buildProfitCenterHash(job, debugLog); + + // 3) DMS allocation config + const selectedDmsAllocationConfig = + bodyshop.md_responsibility_centers.dms_defaults.find((d) => d.name === job.dms_allocation) || null; + + CreateRRLogEvent(connectionData, "DEBUG", "Using DMS Allocation for cost export", { + allocationName: selectedDmsAllocationConfig && selectedDmsAllocationConfig.name + }); + debugLog("Selected DMS allocation config", { + name: selectedDmsAllocationConfig && selectedDmsAllocationConfig.name + }); + + // 4) Cost centers from bills and timetickets + const disablebillwip = !!bodyshop?.pbs_configuration?.disablebillwip; + let costCenterHash = buildCostCenterHash(job, selectedDmsAllocationConfig, disablebillwip, debugLog); + + // 5) Manual MAPA/MASH sales (when needed) + let profitCenterHash = applyMapaMashManualLines({ + job, + selectedDmsAllocationConfig, + bodyshop, + profitCenterHash: initialProfitHash, + hasMapaLine, + hasMashLine, + debugLog + }); + + // 6) Materials costing (MAPA/MASH cost side) + costCenterHash = applyMaterialsCosting({ + job, + bodyshop, + selectedDmsAllocationConfig, + costCenterHash, + debugLog + }); + + // 7) PVRT / towing / storage / PAO extras + ({ profitCenterHash, taxAllocations } = applyExtras({ + job, + bodyshop, + selectedDmsAllocationConfig, + profitCenterHash, + taxAllocations, + debugLog + })); + + // 8) Rome-only profile-level adjustments + profitCenterHash = applyRomeProfileAdjustments({ + job, + bodyshop, + selectedDmsAllocationConfig, + profitCenterHash, + debugLog, + connectionData + }); + + debugLog("profitCenterHash before jobAllocations build", { + centers: summarizeHash(profitCenterHash) + }); + debugLog("costCenterHash before jobAllocations build", { + centers: summarizeHash(costCenterHash) + }); + + // 9) Build job-level allocations & tax allocations + const jobAllocations = buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLog); + const taxAllocArray = buildTaxAllocArray(taxAllocations, selectedDmsAllocationConfig, debugLog); + const { ttlAdjArray, ttlTaxAdjArray } = buildAdjustmentAllocations(job, bodyshop, debugLog); + + // 10) Final combined array + const allocations = [...jobAllocations, ...taxAllocArray, ...ttlAdjArray, ...ttlTaxAdjArray]; + + debugLog("FINAL allocations summary", { + count: allocations.length, + allocations: summarizeAllocationsArray(allocations) + }); + debugLog("EXIT"); + + return allocations; +} + +/** + * HTTP route wrapper (kept for compatibility; still logs via RR logger). + */ +exports.defaultRoute = async function (req, res) { + try { + CreateRRLogEvent(req, "DEBUG", "Received request to calculate allocations", { jobid: req.body.jobid }); + + const jobData = await QueryJobData(req, req.BearerToken, req.body.jobid); + const data = calculateAllocations(req, jobData); + return res.status(200).json({ data }); + } catch (error) { + CreateRRLogEvent(req, "ERROR", "Error encountered in CdkCalculateAllocations.", { + message: error?.message || String(error), + stack: error?.stack + }); + res.status(500).json({ error: `Error encountered in CdkCalculateAllocations. ${error}` }); + } +}; + +/** + * Socket entry point (what rr-job-export & rr-register-socket-events call). + * Reynolds-only: WSS + RR logger. + */ +exports.default = async function (socket, jobid) { + try { + const token = `Bearer ${socket.handshake.auth.token}`; + const jobData = await QueryJobData(socket, token, jobid); + return calculateAllocations(socket, jobData); + } catch (error) { + CreateRRLogEvent(socket, "ERROR", "Error encountered in CdkCalculateAllocations.", { + message: error?.message || String(error), + stack: error?.stack + }); + return null; + } +}; diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index cb143f98b..ff8753f8c 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -2,7 +2,7 @@ const { buildRRRepairOrderPayload } = require("./rr-job-helpers"); const { buildClientAndOpts } = require("./rr-lookup"); const CreateRRLogEvent = require("./rr-logger-event"); const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers"); -const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; +const CdkCalculateAllocations = require("./rr-calculate-allocations").default; /** * Derive RR status information from response object. diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js index 279d6a8d3..2aff26a64 100644 --- a/server/rr/rr-register-socket-events.js +++ b/server/rr/rr-register-socket-events.js @@ -2,7 +2,7 @@ const CreateRRLogEvent = require("./rr-logger-event"); const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup"); const { QueryJobData } = require("./rr-job-helpers"); const { exportJobToRR, finalizeRRRepairOrder } = require("./rr-job-export"); -const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; +const CdkCalculateAllocations = require("./rr-calculate-allocations").default; const { createRRCustomer } = require("./rr-customers"); const { ensureRRServiceVehicle } = require("./rr-service-vehicles"); const { classifyRRVendorError } = require("./rr-errors"); diff --git a/server/rr/rr-service-vehicles.js b/server/rr/rr-service-vehicles.js index b111ca124..ab0651cf9 100644 --- a/server/rr/rr-service-vehicles.js +++ b/server/rr/rr-service-vehicles.js @@ -187,33 +187,20 @@ const ensureRRServiceVehicle = async (args = {}) => { }); } - // --- Attempt insert (idempotent) --- - // IMPORTANT: The current RR lib build validates `vehicleServInfo.customerNo`. - // To be future-proof, we also include top-level `customerNo`. const insertPayload = { - // === Core Vehicle Identity (MANDATORY for success) === vin: vinStr.toUpperCase(), // "1FDWX34Y28EB01395" - // Required: 2-character make code (from v_make_desc → known mapping) + // 2-character make code (from v_make_desc → known mapping) vehicleMake: deriveMakeCode(job.v_make_desc), // → "FR" for Ford - - // Required: 2-digit year (last 2 digits of v_model_yr) year: job?.v_model_yr || undefined, - - // Required: Model number — fallback strategy per ERA behavior + // Model number — fallback strategy per ERA behavior // Most Ford trucks use "T" = Truck. Some systems accept actual code. // CAN BE (P)assenger , (T)ruck, (O)ther mdlNo: undefined, - - // === Descriptive Fields (highly recommended) === modelDesc: job?.v_model_desc?.trim() || undefined, // "F-350 SD" carline: job?.v_model_desc?.trim() || undefined, // Series line extClrDesc: job?.v_color?.trim() || undefined, // "Red" - - // Optional but helpful accentClr: undefined, - - // === VehicleDetail Flags (CRITICAL — cause silent fails or error 303 if missing) === aircond: undefined, // "Y", // Nearly all modern vehicles have A/C pwrstr: undefined, // "Y", // Power steering = yes on 99% of vehicles post-1990 transm: undefined, // "A", // Default to Automatic — change to "M" only if known manual @@ -224,22 +211,16 @@ const ensureRRServiceVehicle = async (args = {}) => { // License plate licNo: license ? String(license).trim() : undefined, - // === VehicleServInfo (attributes on the element) === - customerNo: custNoStr, // fallback (some builds read this) + customerNo: custNoStr, stockId: job.ro_number || undefined, // Use RO as stock# — common pattern - vehicleServInfo: { customerNo: custNoStr, // REQUIRED — this is what toServiceVehicleView() validates - // Optional but increases success rate salesmanNo: undefined, // You don't have advisor yet — omit inServiceDate: undefined, - // Optional — safe to include if you want productionDate: undefined, modelMaintCode: undefined, teamCode: undefined, - // Extended warranty — omit unless you sell contracts vehExtWarranty: undefined, - // Advisor — omit unless you know who the service advisor is advisor: undefined } };