// server/rr/rr-calculate-allocations.js const { GraphQLClient } = require("graphql-request"); const queries = require("../graphql-client/queries"); const RRLogger = require("./rr-logger"); const Dinero = require("dinero.js"); const _ = require("lodash"); const WsLogger = require("../web-sockets/createLogEvent").default || require("../web-sockets/createLogEvent"); // InstanceManager wiring (same as CDK file) const InstanceManager = require("../utils/instanceMgr").default; const { DiscountNotAlreadyCounted } = InstanceManager({ imex: require("../job/job-totals"), rome: require("../job/job-totals-USA") }); /** * HTTP route version (parity with CDK file) */ exports.defaultRoute = async function rrAllocationsHttp(req, res) { try { WsLogger.createLogEvent(req, "DEBUG", `RR: calculate allocations request for ${req.body.jobid}`); const jobData = await queryJobData(req, req.BearerToken, req.body.jobid); return res.status(200).json({ data: calculateAllocations(req, jobData) }); } catch (error) { WsLogger.createLogEvent(req, "ERROR", `RR CalculateAllocations error. ${error}`); res.status(500).json({ error: `RR CalculateAllocations error. ${error}` }); } }; /** * Socket version (parity with CDK file) * @param {Socket} socket * @param {string} jobid * @returns {Promise} allocations */ exports.default = async function rrCalculateAllocations(socket, jobid) { try { const token = "Bearer " + socket.handshake.auth.token; const jobData = await queryJobData(socket, token, jobid, /* isFortellis */ false); return calculateAllocations(socket, jobData); } catch (error) { RRLogger(socket, "ERROR", `RR CalculateAllocations error. ${error}`); return []; } }; async function queryJobData(connectionData, token, jobid /* , isFortellis */) { WsLogger.createLogEvent(connectionData, "DEBUG", `RR: querying job data for id ${jobid}`); const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid }); WsLogger.createLogEvent(connectionData, "DEBUG", `RR: job data query result ${JSON.stringify(result, null, 2)}`); return result.jobs_by_pk; } /** * Core allocation logic – mirrors CDK version, but logs as RR */ function calculateAllocations(connectionData, job) { const { bodyshop } = job; // Build tax allocation maps for US (Rome) and IMEX (Canada) contexts const taxAllocations = 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`] } }) }); // Detect existing MAPA/MASH (Mitchell) lines so we don’t double count let hasMapaLine = false; let hasMashLine = false; const profitCenterHash = job.joblines.reduce((acc, val) => { if (val.db_ref === "936008") hasMapaLine = true; // paint materials (MAPA) if (val.db_ref === "936007") hasMashLine = true; // shop supplies (MASH) 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); // Conditional discount add-on if not already counted elsewhere dineroAmount = dineroAmount.add( ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && DiscountNotAlreadyCounted(val, job.joblines) ? 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) : Dinero() ); acc[val.profitcenter_part] = acc[val.profitcenter_part].add(dineroAmount); } if (val.profitcenter_labor && val.mod_lbr_ty) { if (!acc[val.profitcenter_labor]) acc[val.profitcenter_labor] = Dinero(); acc[val.profitcenter_labor] = acc[val.profitcenter_labor].add( Dinero({ amount: Math.round(job[`rate_${val.mod_lbr_ty.toLowerCase()}`] * 100) }).multiply(val.mod_lb_hrs) ); } return acc; }, {}); const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find( (d) => d.name === job.dms_allocation ); WsLogger.createLogEvent( connectionData, "DEBUG", `RR: Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.` ); // Build cost center totals from bills and time tickets let costCenterHash = {}; const disableBillWip = !!bodyshop?.pbs_configuration?.disablebillwip; if (!disableBillWip) { costCenterHash = job.bills.reduce((billAcc, bill) => { bill.billlines.forEach((line) => { const target = selectedDmsAllocationConfig.costs[line.cost_center]; if (!billAcc[target]) billAcc[target] = Dinero(); let lineDinero = Dinero({ amount: Math.round((line.actual_cost || 0) * 100) }) .multiply(line.quantity) .multiply(bill.is_credit_memo ? -1 : 1); billAcc[target] = billAcc[target].add(lineDinero); }); return billAcc; }, {}); } job.timetickets.forEach((ticket) => { const ticketTotal = Dinero({ amount: Math.round( ticket.rate * (ticket.employee && ticket.employee.flat_rate ? ticket.productivehrs || 0 : ticket.actualhrs || 0) * 100 ) }); const target = selectedDmsAllocationConfig.costs[ticket.ciecacode]; if (!costCenterHash[target]) costCenterHash[target] = Dinero(); costCenterHash[target] = costCenterHash[target].add(ticketTotal); }); // Add MAPA/MASH lines when not explicitly present if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) { const accountName = selectedDmsAllocationConfig.profits.MAPA; const account = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName); if (account) { if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero(); profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates.mapa.total)); } } if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) { const accountName = selectedDmsAllocationConfig.profits.MASH; const account = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName); if (account) { if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero(); profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates.mash.total)); } } // Optional materials costing (CDK setting reused by RR sites if configured) if (bodyshop?.cdk_configuration?.sendmaterialscosting) { const percent = bodyshop.cdk_configuration.sendmaterialscosting; // Paint Mat const mapaCostName = selectedDmsAllocationConfig.costs.MAPA; const mapaCost = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mapaCostName); if (mapaCost) { if (!costCenterHash[mapaCostName]) costCenterHash[mapaCostName] = Dinero(); if (job.bodyshop.use_paint_scale_data === true && job.mixdata.length > 0) { costCenterHash[mapaCostName] = costCenterHash[mapaCostName].add( Dinero({ amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100) }) ); } else { costCenterHash[mapaCostName] = costCenterHash[mapaCostName].add( Dinero(job.job_totals.rates.mapa.total).percentage(percent) ); } } // Shop Mat const mashCostName = selectedDmsAllocationConfig.costs.MASH; const mashCost = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mashCostName); if (mashCost) { if (!costCenterHash[mashCostName]) costCenterHash[mashCostName] = Dinero(); costCenterHash[mashCostName] = costCenterHash[mashCostName].add( Dinero(job.job_totals.rates.mash.total).percentage(percent) ); } } // Provinical PVRT roll-in (Canada only) const { ca_bc_pvrt } = job; if (ca_bc_pvrt) { taxAllocations.state.sale = taxAllocations.state.sale.add(Dinero({ amount: Math.round((ca_bc_pvrt || 0) * 100) })); } // Towing / Storage / Other adjustments if (job.towing_payable && job.towing_payable !== 0) { const name = selectedDmsAllocationConfig.profits.TOW; const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name); if (acct) { if (!profitCenterHash[name]) profitCenterHash[name] = Dinero(); profitCenterHash[name] = profitCenterHash[name].add( Dinero({ amount: Math.round((job.towing_payable || 0) * 100) }) ); } } if (job.storage_payable && job.storage_payable !== 0) { const name = selectedDmsAllocationConfig.profits.TOW; const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name); if (acct) { if (!profitCenterHash[name]) profitCenterHash[name] = Dinero(); profitCenterHash[name] = profitCenterHash[name].add( Dinero({ amount: Math.round((job.storage_payable || 0) * 100) }) ); } } if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) { const name = selectedDmsAllocationConfig.profits.PAO; const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name); if (acct) { if (!profitCenterHash[name]) profitCenterHash[name] = Dinero(); profitCenterHash[name] = profitCenterHash[name].add( Dinero({ amount: Math.round((job.adjustment_bottom_line || 0) * 100) }) ); } } // Rome profile-level adjustments for parts / labor / materials if (InstanceManager({ rome: true })) { Object.keys(job.job_totals.parts.adjustments).forEach((key) => { const name = selectedDmsAllocationConfig.profits[key]; const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name); if (acct) { if (!profitCenterHash[name]) profitCenterHash[name] = Dinero(); profitCenterHash[name] = profitCenterHash[name].add(Dinero(job.job_totals.parts.adjustments[key])); } else { WsLogger.createLogEvent(connectionData, "ERROR", `RR CalculateAllocations: missing parts adj account: ${name}`); } }); Object.keys(job.job_totals.rates).forEach((key) => { const rate = job.job_totals.rates[key]; if (rate && rate.adjustment && Dinero(rate.adjustment).isZero() === false) { const name = selectedDmsAllocationConfig.profits[key.toUpperCase()]; const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name); if (acct) { if (!profitCenterHash[name]) profitCenterHash[name] = Dinero(); // NOTE: the original code had rate.adjustments (plural). If that’s a bug upstream, fix there. profitCenterHash[name] = profitCenterHash[name].add(Dinero(rate.adjustments || rate.adjustment)); } else { WsLogger.createLogEvent( connectionData, "ERROR", `RR CalculateAllocations: missing rate adj account: ${name}` ); } } }); } // Merge profit & cost centers const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).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] ? profitCenterHash[key] : Dinero(), cost: costCenterHash[key] ? costCenterHash[key] : Dinero(), profitCenter, costCenter }; }); // Add tax centers (non-zero only) const taxRows = Object.keys(taxAllocations) .filter((k) => taxAllocations[k].sale.getAmount() > 0 || taxAllocations[k].cost.getAmount() > 0) .map((k) => { const base = { ...taxAllocations[k], tax: k }; // Optional GST override preserved from CDK logic const override = selectedDmsAllocationConfig.gst_override; if (k === "federal" && override) { base.costCenter.dms_acctnumber = override; base.profitCenter.dms_acctnumber = override; } return base; }); // Totals adjustments centers const extra = []; if (job.job_totals.totals.ttl_adjustment) { extra.push({ 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: {} }); } if (job.job_totals.totals.ttl_tax_adjustment) { extra.push({ 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: {} }); } return [...jobAllocations, ...taxRows, ...extra]; }