feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -1,381 +0,0 @@
|
||||
// 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];
|
||||
}
|
||||
81
server/rr/rr-config.js
Normal file
81
server/rr/rr-config.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// server/rr/rr-config.js
|
||||
const { GraphQLClient, gql } = require("graphql-request");
|
||||
|
||||
/**
|
||||
* Fetch the bodyshop row (dealer + per-shop RR json).
|
||||
* No fallback to env for dealer/store/branch.
|
||||
*/
|
||||
async function fetchBodyshopRRRow(bodyshopId) {
|
||||
if (!bodyshopId) throw new Error("Missing bodyshopId for RR config.");
|
||||
|
||||
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
||||
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT env var is required.");
|
||||
|
||||
const headers = {};
|
||||
if (process.env.HASURA_ADMIN_SECRET) {
|
||||
headers["x-hasura-admin-secret"] = process.env.HASURA_ADMIN_SECRET;
|
||||
} else if (process.env.GRAPHQL_BEARER) {
|
||||
headers["authorization"] = `Bearer ${process.env.GRAPHQL_BEARER}`;
|
||||
}
|
||||
|
||||
const client = new GraphQLClient(endpoint, { headers });
|
||||
|
||||
const Q = gql`
|
||||
query BodyshopRR($id: uuid!) {
|
||||
bodyshops_by_pk(id: $id) {
|
||||
rr_dealerid
|
||||
rr_configuration
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { bodyshops_by_pk: bs } = await client.request(Q, { id: bodyshopId });
|
||||
if (!bs) throw new Error("Bodyshop not found.");
|
||||
if (!bs.rr_dealerid) throw new Error("Bodyshop is not configured for RR (missing rr_dealerid).");
|
||||
|
||||
let cfgJson = bs.rr_configuration || {};
|
||||
if (typeof cfgJson === "string") {
|
||||
try {
|
||||
cfgJson = JSON.parse(cfgJson);
|
||||
} catch {
|
||||
cfgJson = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dealerNumber: bs.rr_dealerid,
|
||||
storeNumber: cfgJson.storeNumber,
|
||||
branchNumber: cfgJson.branchNumber
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full RR config object used by downstream code.
|
||||
* - Dealer/Store/Branch: from DB (required)
|
||||
* - Transport/BaseURL/creds/PPSysId: from env (deployment-wide)
|
||||
*/
|
||||
async function getRRConfigForBodyshop(bodyshopId) {
|
||||
const { dealerNumber, storeNumber, branchNumber } = await fetchBodyshopRRRow(bodyshopId);
|
||||
|
||||
const rrTransport = (process.env.RR_TRANSPORT || "STAR").toUpperCase();
|
||||
return {
|
||||
// Per-bodyshop (DB)
|
||||
dealerNumber,
|
||||
storeNumber,
|
||||
branchNumber,
|
||||
|
||||
// Duplicate snake_case for legacy call-sites that expect it
|
||||
dealer_number: dealerNumber,
|
||||
store_number: storeNumber,
|
||||
branch_number: branchNumber,
|
||||
|
||||
// Deployment-wide (env)
|
||||
baseUrl: process.env.RR_BASE_URL,
|
||||
username: process.env.RR_USERNAME,
|
||||
password: process.env.RR_PASSWORD,
|
||||
ppsysId: process.env.RR_PPSYSID,
|
||||
rrTransport
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getRRConfigForBodyshop };
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* STAR-only constants for Reynolds & Reynolds (Rome/RCI)
|
||||
* Used by rr-helpers.js to build and send SOAP requests.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* - Only rr-test.js should fall back to ENV for dealer/store/branch.
|
||||
* - All runtime code (sockets/routes/jobs) must pass per-bodyshop
|
||||
* values from the database (see rr-config.js#getRRConfigForBodyshop).
|
||||
*/
|
||||
|
||||
exports.RR_NS = Object.freeze({
|
||||
@@ -21,6 +26,8 @@ const RR_SOAP_HEADERS = {
|
||||
"Content-Type": "text/xml; charset=utf-8",
|
||||
SOAPAction: RR_STAR_SOAP_ACTION
|
||||
};
|
||||
// Export if other modules need default STAR headers
|
||||
exports.RR_SOAP_HEADERS = RR_SOAP_HEADERS;
|
||||
|
||||
// All STAR-supported actions (mapped to Mustache templates)
|
||||
exports.RR_ACTIONS = Object.freeze({
|
||||
@@ -34,17 +41,37 @@ exports.RR_ACTIONS = Object.freeze({
|
||||
UpdateRepairOrder: { template: "UpdateRepairOrder" }
|
||||
});
|
||||
|
||||
// Base config loader (environment-driven)
|
||||
/**
|
||||
* Base config loader (environment-driven)
|
||||
*
|
||||
* ⚠️ Policy:
|
||||
* - Only rr-test.js should rely on the ENV values for dealer/store/branch.
|
||||
* - All other call sites must inject per-bodyshop values from DB.
|
||||
*/
|
||||
exports.getBaseRRConfig = function getBaseRRConfig() {
|
||||
return {
|
||||
baseUrl: process.env.RR_BASE_URL,
|
||||
username: process.env.RR_USERNAME,
|
||||
password: process.env.RR_PASSWORD,
|
||||
ppsysId: process.env.RR_PPSYSID, // optional legacy identifier
|
||||
|
||||
// ❗ These are ONLY for rr-test.js fallback.
|
||||
dealerNumber: process.env.RR_DEALER_NUMBER,
|
||||
storeNumber: process.env.RR_STORE_NUMBER,
|
||||
branchNumber: process.env.RR_BRANCH_NUMBER || "01",
|
||||
|
||||
wssePasswordType: process.env.RR_WSSE_PASSWORD_TYPE || "Text",
|
||||
timeout: Number(process.env.RR_TIMEOUT_MS || 30000)
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize dealer/store/branch field names (camelCase vs snake_case).
|
||||
* Safe to use in helpers to tolerate mixed callers during migration.
|
||||
*/
|
||||
exports.normalizeRRDealerFields = function normalizeRRDealerFields(cfg = {}) {
|
||||
const dealerNumber = cfg.dealerNumber ?? cfg.dealer_number;
|
||||
const storeNumber = cfg.storeNumber ?? cfg.store_number;
|
||||
const branchNumber = cfg.branchNumber ?? cfg.branch_number;
|
||||
return { dealerNumber, storeNumber, branchNumber };
|
||||
};
|
||||
|
||||
@@ -12,17 +12,20 @@ const axios = require("axios");
|
||||
const mustache = require("mustache");
|
||||
const { XMLParser } = require("fast-xml-parser");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { RR_ACTIONS, RR_SOAP_HEADERS, RR_STAR_SOAP_ACTION, RR_NS, getBaseRRConfig } = require("./rr-constants");
|
||||
const {
|
||||
RR_ACTIONS,
|
||||
RR_SOAP_HEADERS,
|
||||
RR_SOAP_ACTION,
|
||||
RR_NS,
|
||||
getBaseRRConfig,
|
||||
normalizeRRDealerFields
|
||||
} = require("./rr-constants");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
const xmlFormatter = require("xml-formatter");
|
||||
|
||||
/**
|
||||
* Remove XML decl, collapse inter-tag whitespace, strip empty lines,
|
||||
* then pretty-print. Safe for XML because we only touch whitespace
|
||||
* BETWEEN tags, not inside text nodes.
|
||||
/**
|
||||
* Collapse Mustache-induced whitespace and pretty print.
|
||||
* - strips XML decl (inner)
|
||||
* - strips inner XML decl
|
||||
* - removes lines that are only whitespace
|
||||
* - collapses inter-tag whitespace
|
||||
* - formats with consistent indentation
|
||||
@@ -69,25 +72,37 @@ async function renderXmlTemplate(templateName, data) {
|
||||
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
}
|
||||
|
||||
// ---------- Config resolution (STAR only) ----------
|
||||
/**
|
||||
* Resolve RR config for STAR transport.
|
||||
*
|
||||
* Policy:
|
||||
* - Base (transport) settings (baseUrl, username, password, ppsysId, wssePasswordType, timeout) come from env.
|
||||
* - Dealer identifiers (dealer/store/branch) MUST be provided by the caller (DB-driven).
|
||||
* - We DO NOT fall back to env for dealer/store/branch here. Only rr-test.js is allowed to do that.
|
||||
*/
|
||||
async function resolveRRConfig(_socket, bodyshopConfig) {
|
||||
const envCfg = getBaseRRConfig();
|
||||
const baseEnv = getBaseRRConfig();
|
||||
|
||||
if (bodyshopConfig && typeof bodyshopConfig === "object") {
|
||||
return {
|
||||
...envCfg,
|
||||
baseUrl: bodyshopConfig.baseUrl || envCfg.baseUrl,
|
||||
username: bodyshopConfig.username || envCfg.username,
|
||||
password: bodyshopConfig.password || envCfg.password,
|
||||
ppsysId: bodyshopConfig.ppsysId || envCfg.ppsysId,
|
||||
dealerNumber: bodyshopConfig.dealer_number || envCfg.dealerNumber,
|
||||
storeNumber: bodyshopConfig.store_number || envCfg.storeNumber,
|
||||
branchNumber: bodyshopConfig.branch_number || envCfg.branchNumber,
|
||||
wssePasswordType: bodyshopConfig.wssePasswordType || envCfg.wssePasswordType || "Text",
|
||||
timeout: envCfg.timeout
|
||||
};
|
||||
const { dealerNumber, storeNumber, branchNumber } = normalizeRRDealerFields(bodyshopConfig || {});
|
||||
if (!dealerNumber || !storeNumber || !branchNumber) {
|
||||
throw new Error(
|
||||
"Missing dealer/store/branch in RR config. These must be loaded from the database (no env fallback here)."
|
||||
);
|
||||
}
|
||||
return envCfg;
|
||||
|
||||
return {
|
||||
baseUrl: bodyshopConfig?.baseUrl || baseEnv.baseUrl,
|
||||
username: bodyshopConfig?.username || baseEnv.username,
|
||||
password: bodyshopConfig?.password || baseEnv.password,
|
||||
ppsysId: bodyshopConfig?.ppsysId || baseEnv.ppsysId,
|
||||
wssePasswordType: bodyshopConfig?.wssePasswordType || baseEnv.wssePasswordType || "Text",
|
||||
timeout: baseEnv.timeout,
|
||||
|
||||
// canonical identifiers (DB-driven only)
|
||||
dealerNumber,
|
||||
storeNumber,
|
||||
branchNumber
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- Response parsing ----------
|
||||
@@ -161,8 +176,7 @@ function parseRRResponse(xml) {
|
||||
|
||||
// ---------- STAR envelope helpers ----------
|
||||
function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, Destination }) {
|
||||
// Make sure we inject *inside* the STAR root, not before it.
|
||||
// 1) Strip any XML declaration just in case (idempotent)
|
||||
// Strip any inner XML declaration (idempotent)
|
||||
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
|
||||
const appArea = `
|
||||
@@ -182,8 +196,7 @@ function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, De
|
||||
</Destination>
|
||||
</ApplicationArea>`.trim();
|
||||
|
||||
// Inject right after the opening tag of the root element (skip processing instructions)
|
||||
// e.g. <rey_RomeGetAdvisorsReq ...> ==> insert ApplicationArea here
|
||||
// Inject right after the opening tag of the root element
|
||||
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
|
||||
|
||||
return xml;
|
||||
@@ -229,7 +242,7 @@ async function MakeRRCall({
|
||||
action,
|
||||
body,
|
||||
socket,
|
||||
dealerConfig, // optional per-shop overrides
|
||||
dealerConfig, // required in runtime code; rr-test.js can still pass env-inflated cfg
|
||||
retries = 1,
|
||||
jobid
|
||||
}) {
|
||||
@@ -237,6 +250,7 @@ async function MakeRRCall({
|
||||
throw new Error(`Invalid RR action: ${action}`);
|
||||
}
|
||||
|
||||
// Prefer explicit dealerConfig from caller; otherwise enforce DB-provided config via resolveRRConfig
|
||||
const cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
|
||||
const baseUrl = cfg.baseUrl;
|
||||
if (!baseUrl) throw new Error("Missing RR base URL");
|
||||
@@ -246,8 +260,7 @@ async function MakeRRCall({
|
||||
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
|
||||
|
||||
// Build STAR envelope
|
||||
let envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
|
||||
|
||||
const envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
|
||||
const formattedEnvelope = prettyPrintXml(envelope);
|
||||
|
||||
// Guardrails
|
||||
@@ -255,11 +268,11 @@ async function MakeRRCall({
|
||||
throw new Error("STAR envelope malformed: missing ProcessMessage/ApplicationArea");
|
||||
}
|
||||
|
||||
const headers = { ...RR_SOAP_HEADERS, SOAPAction: RR_STAR_SOAP_ACTION };
|
||||
const headers = { ...RR_SOAP_HEADERS, SOAPAction: RR_SOAP_ACTION };
|
||||
|
||||
RRLogger(socket, "debug", `Sending RR SOAP request`, {
|
||||
action,
|
||||
soapAction: RR_STAR_SOAP_ACTION,
|
||||
soapAction: RR_SOAP_ACTION,
|
||||
endpoint: baseUrl,
|
||||
jobid,
|
||||
mode: "STAR"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
const dayjs = require("dayjs");
|
||||
const { normalizeRRDealerFields } = require("./rr-constants");
|
||||
|
||||
/**
|
||||
* Utility: formats date/time to R&R’s preferred format (ISO or yyyy-MM-dd).
|
||||
@@ -22,6 +23,15 @@ const num = (val) => (val != null ? String(val) : undefined);
|
||||
const toBoolStr = (v) => (v === true ? "true" : v === false ? "false" : undefined);
|
||||
const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined && v !== null && v !== "");
|
||||
|
||||
/**
|
||||
* Pull canonical Dealer/Store/Branch fields from cfg (tolerate snake_case during migration).
|
||||
* Enforces DB-provided values upstream (no env fallback here).
|
||||
*/
|
||||
function getDSB(cfg) {
|
||||
const { dealerNumber, storeNumber, branchNumber } = normalizeRRDealerFields(cfg || {});
|
||||
return { dealerNumber, storeNumber, branchNumber };
|
||||
}
|
||||
|
||||
//
|
||||
// ===================== CUSTOMER =====================
|
||||
//
|
||||
@@ -31,12 +41,13 @@ const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined
|
||||
*/
|
||||
function mapCustomerInsert(customer, bodyshopConfig) {
|
||||
if (!customer) return {};
|
||||
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
DealerNumber: dealerNumber,
|
||||
StoreNumber: storeNumber,
|
||||
BranchNumber: branchNumber,
|
||||
RequestId: `CUST-INSERT-${customer.id}`,
|
||||
Environment: process.env.NODE_ENV,
|
||||
|
||||
@@ -116,12 +127,13 @@ function mapCustomerUpdate(customer, bodyshopConfig) {
|
||||
*/
|
||||
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
||||
if (!vehicle) return {};
|
||||
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
DealerNumber: dealerNumber,
|
||||
StoreNumber: storeNumber,
|
||||
BranchNumber: branchNumber,
|
||||
RequestId: `VEH-${vehicle.id}`,
|
||||
|
||||
CustomerId: ownerCustomer?.external_id,
|
||||
@@ -175,15 +187,16 @@ function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
||||
*/
|
||||
function mapRepairOrderCreate(job, bodyshopConfig) {
|
||||
if (!job) return {};
|
||||
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||
|
||||
const cust = job.customer || {};
|
||||
const veh = job.vehicle || {};
|
||||
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
DealerNumber: dealerNumber,
|
||||
StoreNumber: storeNumber,
|
||||
BranchNumber: branchNumber,
|
||||
RequestId: `RO-${job.id}`,
|
||||
Environment: process.env.NODE_ENV,
|
||||
|
||||
@@ -302,11 +315,12 @@ function mapRepairOrderUpdate(job, bodyshopConfig) {
|
||||
//
|
||||
|
||||
function mapAdvisorLookup(criteria, bodyshopConfig) {
|
||||
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
DealerNumber: dealerNumber,
|
||||
StoreNumber: storeNumber,
|
||||
BranchNumber: branchNumber,
|
||||
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
|
||||
SearchCriteria: {
|
||||
Department: criteria.department || "Body Shop",
|
||||
@@ -316,11 +330,12 @@ function mapAdvisorLookup(criteria, bodyshopConfig) {
|
||||
}
|
||||
|
||||
function mapPartsLookup(criteria, bodyshopConfig) {
|
||||
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
DealerNumber: dealerNumber,
|
||||
StoreNumber: storeNumber,
|
||||
BranchNumber: branchNumber,
|
||||
RequestId: `LOOKUP-PART-${Date.now()}`,
|
||||
SearchCriteria: {
|
||||
PartNumber: criteria.part_number,
|
||||
@@ -335,6 +350,8 @@ function mapPartsLookup(criteria, bodyshopConfig) {
|
||||
}
|
||||
|
||||
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
||||
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||
|
||||
// accept nested or flat input
|
||||
const c = criteria || {};
|
||||
const cust = c.customer || c.Customer || {};
|
||||
@@ -364,14 +381,11 @@ function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
||||
};
|
||||
|
||||
return {
|
||||
// Dealer / routing (aligns with your other mappers)
|
||||
STAR_NS: require("./rr-constants").RR_NS.STAR,
|
||||
MaxRecs: criteria.maxResults || criteria.MaxResults || 50,
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerName: bodyshopConfig?.dealer_name,
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
DealerNumber: dealerNumber,
|
||||
StoreNumber: storeNumber,
|
||||
BranchNumber: branchNumber,
|
||||
|
||||
RequestId: c.requestId || `COMBINED-${Date.now()}`,
|
||||
Environment: process.env.NODE_ENV,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const dotenv = require("dotenv");
|
||||
const { GraphQLClient, gql } = require("graphql-request");
|
||||
const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers");
|
||||
const { getBaseRRConfig } = require("./rr-constants");
|
||||
|
||||
@@ -20,7 +21,7 @@ if (fs.existsSync(defaultEnvPath)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI args
|
||||
// ---- CLI args parsing ----
|
||||
const argv = process.argv.slice(2);
|
||||
const args = { _: [] };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
@@ -37,13 +38,12 @@ for (let i = 0; i < argv.length; i++) {
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith("-")) {
|
||||
args[k] = next;
|
||||
i++; // consume value
|
||||
i++;
|
||||
} else {
|
||||
args[k] = true; // boolean flag
|
||||
args[k] = true;
|
||||
}
|
||||
}
|
||||
} else if (a.startsWith("-") && a.length > 1) {
|
||||
// simple short flag handling: -a value
|
||||
const k = a.slice(1);
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith("-")) {
|
||||
@@ -62,7 +62,65 @@ function toIntOr(defaultVal, maybe) {
|
||||
return Number.isFinite(n) ? n : defaultVal;
|
||||
}
|
||||
|
||||
// ✅ fixed guard clause
|
||||
// ---------------- GraphQL helpers ----------------
|
||||
|
||||
function buildGqlClient() {
|
||||
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
||||
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT env var is required when using --bodyshopId.");
|
||||
|
||||
const headers = {};
|
||||
if (process.env.HASURA_ADMIN_SECRET) {
|
||||
headers["x-hasura-admin-secret"] = process.env.HASURA_ADMIN_SECRET;
|
||||
} else if (process.env.GRAPHQL_BEARER) {
|
||||
headers["authorization"] = `Bearer ${process.env.GRAPHQL_BEARER}`;
|
||||
}
|
||||
|
||||
return new GraphQLClient(endpoint, { headers });
|
||||
}
|
||||
|
||||
const Q_BODYSHOPS_BY_PK = gql`
|
||||
query BodyshopRR($id: uuid!) {
|
||||
bodyshops_by_pk(id: $id) {
|
||||
rr_dealerid
|
||||
rr_configuration
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function normalizeConfigRecord(rec) {
|
||||
if (!rec) return null;
|
||||
let cfg = rec.rr_configuration || {};
|
||||
if (typeof cfg === "string") {
|
||||
try {
|
||||
cfg = JSON.parse(cfg);
|
||||
} catch {
|
||||
cfg = {};
|
||||
}
|
||||
}
|
||||
return {
|
||||
dealerNumber: rec.rr_dealerid || undefined,
|
||||
storeNumber: cfg.storeNumber || undefined,
|
||||
branchNumber: cfg.branchNumber || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load RR config overrides from DB (bodyshops_by_pk only).
|
||||
*/
|
||||
async function loadBodyshopRRConfig(bodyshopId) {
|
||||
if (!bodyshopId) return null;
|
||||
|
||||
const client = buildGqlClient();
|
||||
const { bodyshops_by_pk: bs } = await client.request(Q_BODYSHOPS_BY_PK, { id: bodyshopId });
|
||||
|
||||
if (!bs) throw new Error("Bodyshop not found.");
|
||||
if (!bs.rr_dealerid) throw new Error("Bodyshop is not configured for RR (missing rr_dealerid).");
|
||||
|
||||
return normalizeConfigRecord(bs);
|
||||
}
|
||||
|
||||
// ---------------- rr-test logic ----------------
|
||||
|
||||
function pickActionName(raw) {
|
||||
if (!raw || typeof raw !== "string") return "ping";
|
||||
const x = raw.toLowerCase();
|
||||
@@ -142,6 +200,8 @@ function buildBodyForAction(action, args, cfg) {
|
||||
|
||||
async function main() {
|
||||
const action = pickActionName(args.action || args.a || args._[0]);
|
||||
const bodyshopId = args.bodyshopId || args.bodyshop || args.b;
|
||||
|
||||
const rrAction =
|
||||
action === "ping"
|
||||
? "GetAdvisors"
|
||||
@@ -153,12 +213,28 @@ async function main() {
|
||||
? "GetParts"
|
||||
: action;
|
||||
|
||||
// Start with env-based defaults…
|
||||
const cfg = getBaseRRConfig();
|
||||
|
||||
// …then override with per-bodyshop values if provided.
|
||||
if (bodyshopId) {
|
||||
try {
|
||||
const overrides = await loadBodyshopRRConfig(bodyshopId);
|
||||
if (overrides?.dealerNumber) cfg.dealerNumber = overrides.dealerNumber;
|
||||
if (overrides?.storeNumber) cfg.storeNumber = overrides.storeNumber;
|
||||
if (overrides?.branchNumber) cfg.branchNumber = overrides.branchNumber;
|
||||
console.log("ℹ️ RR config loaded from DB via: bodyshops_by_pk");
|
||||
} catch (e) {
|
||||
console.error("❌ Failed to load per-bodyshop RR config:", e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const body = buildBodyForAction(action, args, cfg);
|
||||
const templateName = body.template || rrAction;
|
||||
|
||||
try {
|
||||
const xml = await renderXmlTemplate(templateName, body.data);
|
||||
await renderXmlTemplate(templateName, body.data);
|
||||
console.log("✅ Templates verified.");
|
||||
} catch (e) {
|
||||
console.error("❌ Template verification failed:", e.message);
|
||||
@@ -186,6 +262,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch(() => process.exit(1));
|
||||
|
||||
@@ -29,32 +29,57 @@ const { exportJobToRome } = require("./rr-job-export"); // orchestrator
|
||||
// Diagnostics
|
||||
const { listActions, verifyTemplatesExist } = require("./rr-wsdl");
|
||||
|
||||
// Helpers
|
||||
// DB-driven RR config (no env fallback for dealer/store/branch here)
|
||||
const { getRRConfigForBodyshop } = require("./rr-config");
|
||||
|
||||
// -------------------- Helpers --------------------
|
||||
|
||||
function ok(res, payload = {}) {
|
||||
return res.json({ success: true, ...payload });
|
||||
}
|
||||
|
||||
function fail(res, error, status = 400) {
|
||||
const message = error?.message || String(error);
|
||||
return res.status(status).json({ success: false, error: message, code: error?.code });
|
||||
}
|
||||
function pickConfig(req) {
|
||||
// Accept config in either { config } or { bodyshopConfig }
|
||||
return req.body?.config || req.body?.bodyshopConfig || {};
|
||||
}
|
||||
|
||||
function socketOf(req) {
|
||||
// If you stash a socket/logging context on the app, grab it; otherwise null
|
||||
return (req.app && req.app.get && req.app.get("socket")) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the per-bodyshop RR config strictly from DB.
|
||||
* Looks for bodyshopId in:
|
||||
* - req.body.bodyshopId
|
||||
* - req.body.job?.shopid
|
||||
* - x-bodyshop-id header
|
||||
* Throws if not found.
|
||||
*/
|
||||
async function resolveRRConfigHttp(req) {
|
||||
const candidateHeader = req.get && req.get("x-bodyshop-id");
|
||||
const body = req.body || {};
|
||||
const bodyshopId = body.bodyshopId || (body.job && (body.job.shopid || body.job.bodyshopId)) || candidateHeader;
|
||||
|
||||
if (!bodyshopId) {
|
||||
throw new RrApiError(
|
||||
"Missing bodyshopId (expected in body.bodyshopId, body.job.shopid, or x-bodyshop-id header)",
|
||||
"BAD_REQUEST"
|
||||
);
|
||||
}
|
||||
|
||||
return getRRConfigForBodyshop(bodyshopId);
|
||||
}
|
||||
|
||||
// -------------------- Customers --------------------
|
||||
|
||||
router.post("/rr/customer/insert", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { customer } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
if (!customer) throw new RrApiError("Missing 'customer' in request body", "BAD_REQUEST");
|
||||
const cfg = await resolveRRConfigHttp(req); // DB-driven, required
|
||||
const result = await customerApi.insertCustomer(socket, customer, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -66,10 +91,10 @@ router.post("/rr/customer/insert", async (req, res) => {
|
||||
router.post("/rr/customer/update", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { customer } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
if (!customer) throw new RrApiError("Missing 'customer' in request body", "BAD_REQUEST");
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await customerApi.updateCustomer(socket, customer, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -83,10 +108,10 @@ router.post("/rr/customer/update", async (req, res) => {
|
||||
router.post("/rr/repair-order/create", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { job } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await roApi.createRepairOrder(socket, job, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -98,10 +123,10 @@ router.post("/rr/repair-order/create", async (req, res) => {
|
||||
router.post("/rr/repair-order/update", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { job } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await roApi.updateRepairOrder(socket, job, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -115,9 +140,9 @@ router.post("/rr/repair-order/update", async (req, res) => {
|
||||
router.post("/rr/lookup/advisors", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { criteria = {} } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await lookupApi.getAdvisors(socket, criteria, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -129,9 +154,9 @@ router.post("/rr/lookup/advisors", async (req, res) => {
|
||||
router.post("/rr/lookup/parts", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { criteria = {} } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await lookupApi.getParts(socket, criteria, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -143,9 +168,9 @@ router.post("/rr/lookup/parts", async (req, res) => {
|
||||
router.post("/rr/lookup/combined-search", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { criteria = {} } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await lookupApi.combinedSearch(socket, criteria, cfg);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
@@ -159,10 +184,10 @@ router.post("/rr/lookup/combined-search", async (req, res) => {
|
||||
router.post("/rr/export/job", async (req, res) => {
|
||||
const socket = socketOf(req);
|
||||
const { job, options = {} } = req.body || {};
|
||||
const cfg = pickConfig(req);
|
||||
|
||||
try {
|
||||
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
|
||||
const cfg = await resolveRRConfigHttp(req);
|
||||
const result = await exportJobToRome(socket, job, cfg, options);
|
||||
return ok(res, result);
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,21 +5,7 @@ const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/
|
||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
const { exportJobToRome } = require("../rr/rr-job-export");
|
||||
const lookupApi = require("../rr/rr-lookup");
|
||||
|
||||
function resolveRRConfigFrom(payload = {}) {
|
||||
// Back-compat: allow txEnvelope.config from old callers
|
||||
const cfg = payload.config || payload.bodyshopConfig || payload.txEnvelope?.config || {};
|
||||
return {
|
||||
baseUrl: cfg.baseUrl || process.env.RR_BASE_URL,
|
||||
username: cfg.username || process.env.RR_USERNAME,
|
||||
password: cfg.password || process.env.RR_PASSWORD,
|
||||
ppsysId: cfg.ppsysId || process.env.RR_PPSYSID,
|
||||
dealer_number: cfg.dealer_number || process.env.RR_DEALER_NUMBER,
|
||||
store_number: cfg.store_number || process.env.RR_STORE_NUMBER,
|
||||
branch_number: cfg.branch_number || process.env.RR_BRANCH_NUMBER,
|
||||
rrTransport: (cfg.rrTransport || process.env.RR_TRANSPORT || "STAR").toUpperCase()
|
||||
};
|
||||
}
|
||||
const { getRRConfigForBodyshop } = require("../rr/rr-config");
|
||||
|
||||
const redisSocketEvents = ({
|
||||
io,
|
||||
@@ -349,7 +335,7 @@ const redisSocketEvents = ({
|
||||
});
|
||||
|
||||
socket.on("task-deleted", (payload) => {
|
||||
if (!payload || !payload.id) return;
|
||||
if (!payload?.id) return;
|
||||
const room = getBodyshopRoom(socket.bodyshopId);
|
||||
io.to(room).emit("bodyshop-message", { type: "task-deleted", payload });
|
||||
});
|
||||
@@ -359,11 +345,13 @@ const redisSocketEvents = ({
|
||||
// Orchestrated Export (Customer → Vehicle → Repair Order)
|
||||
socket.on("rr-export-job", async (payload = {}) => {
|
||||
try {
|
||||
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, config, options }
|
||||
// Prefer direct job/config, otherwise try txEnvelope.{job,config}
|
||||
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, options }
|
||||
// Prefer direct job, otherwise try txEnvelope.job
|
||||
const job = payload.job || payload.txEnvelope?.job;
|
||||
const options = payload.options || payload.txEnvelope?.options || {};
|
||||
const cfg = resolveRRConfigFrom(payload);
|
||||
// Resolve per-bodyshop RR config strictly from DB:
|
||||
const bodyshopId = payload.bodyshopId || socket.bodyshopId || job?.shopid;
|
||||
const cfg = await getRRConfigForBodyshop(bodyshopId);
|
||||
|
||||
if (!job) {
|
||||
RRLogger(socket, "error", "RR export missing job payload");
|
||||
@@ -383,7 +371,7 @@ const redisSocketEvents = ({
|
||||
// Combined search
|
||||
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
||||
try {
|
||||
const cfg = resolveRRConfigFrom({}); // if you want per-call overrides, pass them in the payload and merge here
|
||||
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
|
||||
const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
|
||||
cb?.(data);
|
||||
} catch (e) {
|
||||
@@ -395,7 +383,7 @@ const redisSocketEvents = ({
|
||||
// Get Advisors
|
||||
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
|
||||
try {
|
||||
const cfg = resolveRRConfigFrom({});
|
||||
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
|
||||
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
|
||||
cb?.(data);
|
||||
} catch (e) {
|
||||
@@ -407,7 +395,7 @@ const redisSocketEvents = ({
|
||||
// Get Parts
|
||||
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
|
||||
try {
|
||||
const cfg = resolveRRConfigFrom({});
|
||||
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
|
||||
const data = await lookupApi.getParts(socket, params || {}, cfg);
|
||||
cb?.(data);
|
||||
} catch (e) {
|
||||
@@ -419,9 +407,6 @@ const redisSocketEvents = ({
|
||||
// (Optional) Selected customer — only keep this if you actually implement it for RR
|
||||
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId } = {}) => {
|
||||
try {
|
||||
// If you don’t have an RRSelectedCustomer implementation now, either:
|
||||
// 1) no-op with a log, or
|
||||
// 2) emit a structured event UI can handle as "not supported".
|
||||
RRLogger(socket, "info", "rr-selected-customer not implemented for RR (no-op)", {
|
||||
jobid,
|
||||
selectedCustomerId
|
||||
|
||||
Reference in New Issue
Block a user