feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
381
server/rr/rr-calculate-allocations.js
Normal file
381
server/rr/rr-calculate-allocations.js
Normal file
@@ -0,0 +1,381 @@
|
||||
// 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<Array>} 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];
|
||||
}
|
||||
Reference in New Issue
Block a user