Files
bodyshop/server/rr/rr-calculate-allocations.js

790 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
};