feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -23,6 +23,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { determineDmsType } from "../../utils/determineDmsType";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -168,16 +169,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
if (!jobId || !(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) || bodyshop.rr_dealerid || !data?.jobs_by_pk)
|
if (!jobId || !(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) || !data?.jobs_by_pk)
|
||||||
return <Result status="404" title={t("general.errors.notfound")} />;
|
return <Result status="404" title={t("general.errors.notfound")} />;
|
||||||
|
|
||||||
if (data.jobs_by_pk && data.jobs_by_pk.date_exported)
|
if (data.jobs_by_pk?.date_exported) return <Result status="warning" title={t("dms.errors.alreadyexported")} />;
|
||||||
return <Result status="warning" title={t("dms.errors.alreadyexported")} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{Fortellis.treatment === "on" && (
|
{Fortellis.treatment === "on" && (
|
||||||
<AlertComponent message="Posting to Fortellis" type="warning" showIcon closable />
|
<AlertComponent
|
||||||
|
style={{ marginBottom: 10 }}
|
||||||
|
message={`Posting to ${determineDmsType(bodyshop)}`}
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
closable
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col md={24} lg={10}>
|
<Col md={24} lg={10}>
|
||||||
@@ -185,7 +191,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/manage/jobs/${data && data.jobs_by_pk.id}`}>{`${
|
<Link to={`/manage/jobs/${data && data.jobs_by_pk.id}`}>{`${
|
||||||
data && data.jobs_by_pk && data.jobs_by_pk.ro_number
|
data?.jobs_by_pk && data.jobs_by_pk.ro_number
|
||||||
}`}</Link>
|
}`}</Link>
|
||||||
{` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${
|
{` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${
|
||||||
data.jobs_by_pk.v_model_yr || ""
|
data.jobs_by_pk.v_model_yr || ""
|
||||||
@@ -197,7 +203,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col md={24} lg={14}>
|
<Col md={24} lg={14}>
|
||||||
<DmsPostForm socket={socket} jobId={jobId} job={data && data.jobs_by_pk} logsRef={logsRef} />
|
<DmsPostForm socket={socket} jobId={jobId} job={data?.jobs_by_pk} logsRef={logsRef} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<DmsCustomerSelector jobid={jobId} />
|
<DmsCustomerSelector jobid={jobId} />
|
||||||
|
|||||||
823
package-lock.json
generated
823
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -18,14 +18,14 @@
|
|||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.901.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.908.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.901.0",
|
"@aws-sdk/client-elasticache": "^3.908.0",
|
||||||
"@aws-sdk/client-s3": "^3.901.0",
|
"@aws-sdk/client-s3": "^3.908.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.901.0",
|
"@aws-sdk/client-secrets-manager": "^3.908.0",
|
||||||
"@aws-sdk/client-ses": "^3.901.0",
|
"@aws-sdk/client-ses": "^3.908.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.901.0",
|
"@aws-sdk/credential-provider-node": "^3.908.0",
|
||||||
"@aws-sdk/lib-storage": "^3.903.0",
|
"@aws-sdk/lib-storage": "^3.908.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.901.0",
|
"@aws-sdk/s3-request-presigner": "^3.908.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
@@ -35,13 +35,13 @@
|
|||||||
"axios-curlirize": "^2.0.0",
|
"axios-curlirize": "^2.0.0",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bullmq": "^5.61.0",
|
"bullmq": "^5.61.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.1",
|
||||||
"cloudinary": "^2.7.0",
|
"cloudinary": "^2.7.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"dd-trace": "^5.70.0",
|
"dd-trace": "^5.71.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"firebase-admin": "^13.5.0",
|
"firebase-admin": "^13.5.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.1",
|
||||||
"ioredis": "^5.8.1",
|
"ioredis": "^5.8.1",
|
||||||
"json-2-csv": "^5.5.9",
|
"json-2-csv": "^5.5.9",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.10.2",
|
"twilio": "^5.10.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.18.3",
|
"winston": "^3.18.3",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
|
|||||||
@@ -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)
|
* STAR-only constants for Reynolds & Reynolds (Rome/RCI)
|
||||||
* Used by rr-helpers.js to build and send SOAP requests.
|
* 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({
|
exports.RR_NS = Object.freeze({
|
||||||
@@ -21,6 +26,8 @@ const RR_SOAP_HEADERS = {
|
|||||||
"Content-Type": "text/xml; charset=utf-8",
|
"Content-Type": "text/xml; charset=utf-8",
|
||||||
SOAPAction: RR_STAR_SOAP_ACTION
|
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)
|
// All STAR-supported actions (mapped to Mustache templates)
|
||||||
exports.RR_ACTIONS = Object.freeze({
|
exports.RR_ACTIONS = Object.freeze({
|
||||||
@@ -34,17 +41,37 @@ exports.RR_ACTIONS = Object.freeze({
|
|||||||
UpdateRepairOrder: { template: "UpdateRepairOrder" }
|
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() {
|
exports.getBaseRRConfig = function getBaseRRConfig() {
|
||||||
return {
|
return {
|
||||||
baseUrl: process.env.RR_BASE_URL,
|
baseUrl: process.env.RR_BASE_URL,
|
||||||
username: process.env.RR_USERNAME,
|
username: process.env.RR_USERNAME,
|
||||||
password: process.env.RR_PASSWORD,
|
password: process.env.RR_PASSWORD,
|
||||||
ppsysId: process.env.RR_PPSYSID, // optional legacy identifier
|
ppsysId: process.env.RR_PPSYSID, // optional legacy identifier
|
||||||
|
|
||||||
|
// ❗ These are ONLY for rr-test.js fallback.
|
||||||
dealerNumber: process.env.RR_DEALER_NUMBER,
|
dealerNumber: process.env.RR_DEALER_NUMBER,
|
||||||
storeNumber: process.env.RR_STORE_NUMBER,
|
storeNumber: process.env.RR_STORE_NUMBER,
|
||||||
branchNumber: process.env.RR_BRANCH_NUMBER || "01",
|
branchNumber: process.env.RR_BRANCH_NUMBER || "01",
|
||||||
|
|
||||||
wssePasswordType: process.env.RR_WSSE_PASSWORD_TYPE || "Text",
|
wssePasswordType: process.env.RR_WSSE_PASSWORD_TYPE || "Text",
|
||||||
timeout: Number(process.env.RR_TIMEOUT_MS || 30000)
|
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 mustache = require("mustache");
|
||||||
const { XMLParser } = require("fast-xml-parser");
|
const { XMLParser } = require("fast-xml-parser");
|
||||||
const RRLogger = require("./rr-logger");
|
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 { RrApiError } = require("./rr-error");
|
||||||
const xmlFormatter = require("xml-formatter");
|
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.
|
* Collapse Mustache-induced whitespace and pretty print.
|
||||||
* - strips XML decl (inner)
|
* - strips inner XML decl
|
||||||
* - removes lines that are only whitespace
|
* - removes lines that are only whitespace
|
||||||
* - collapses inter-tag whitespace
|
* - collapses inter-tag whitespace
|
||||||
* - formats with consistent indentation
|
* - formats with consistent indentation
|
||||||
@@ -69,25 +72,37 @@ async function renderXmlTemplate(templateName, data) {
|
|||||||
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
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) {
|
async function resolveRRConfig(_socket, bodyshopConfig) {
|
||||||
const envCfg = getBaseRRConfig();
|
const baseEnv = getBaseRRConfig();
|
||||||
|
|
||||||
if (bodyshopConfig && typeof bodyshopConfig === "object") {
|
const { dealerNumber, storeNumber, branchNumber } = normalizeRRDealerFields(bodyshopConfig || {});
|
||||||
return {
|
if (!dealerNumber || !storeNumber || !branchNumber) {
|
||||||
...envCfg,
|
throw new Error(
|
||||||
baseUrl: bodyshopConfig.baseUrl || envCfg.baseUrl,
|
"Missing dealer/store/branch in RR config. These must be loaded from the database (no env fallback here)."
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
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 ----------
|
// ---------- Response parsing ----------
|
||||||
@@ -161,8 +176,7 @@ function parseRRResponse(xml) {
|
|||||||
|
|
||||||
// ---------- STAR envelope helpers ----------
|
// ---------- STAR envelope helpers ----------
|
||||||
function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, Destination }) {
|
function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, Destination }) {
|
||||||
// Make sure we inject *inside* the STAR root, not before it.
|
// Strip any inner XML declaration (idempotent)
|
||||||
// 1) Strip any XML declaration just in case (idempotent)
|
|
||||||
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||||
|
|
||||||
const appArea = `
|
const appArea = `
|
||||||
@@ -182,8 +196,7 @@ function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, De
|
|||||||
</Destination>
|
</Destination>
|
||||||
</ApplicationArea>`.trim();
|
</ApplicationArea>`.trim();
|
||||||
|
|
||||||
// Inject right after the opening tag of the root element (skip processing instructions)
|
// Inject right after the opening tag of the root element
|
||||||
// e.g. <rey_RomeGetAdvisorsReq ...> ==> insert ApplicationArea here
|
|
||||||
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
|
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
|
||||||
|
|
||||||
return xml;
|
return xml;
|
||||||
@@ -229,7 +242,7 @@ async function MakeRRCall({
|
|||||||
action,
|
action,
|
||||||
body,
|
body,
|
||||||
socket,
|
socket,
|
||||||
dealerConfig, // optional per-shop overrides
|
dealerConfig, // required in runtime code; rr-test.js can still pass env-inflated cfg
|
||||||
retries = 1,
|
retries = 1,
|
||||||
jobid
|
jobid
|
||||||
}) {
|
}) {
|
||||||
@@ -237,6 +250,7 @@ async function MakeRRCall({
|
|||||||
throw new Error(`Invalid RR action: ${action}`);
|
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 cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
|
||||||
const baseUrl = cfg.baseUrl;
|
const baseUrl = cfg.baseUrl;
|
||||||
if (!baseUrl) throw new Error("Missing RR base URL");
|
if (!baseUrl) throw new Error("Missing RR base URL");
|
||||||
@@ -246,8 +260,7 @@ async function MakeRRCall({
|
|||||||
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
|
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
|
||||||
|
|
||||||
// Build STAR envelope
|
// Build STAR envelope
|
||||||
let envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
|
const envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
|
||||||
|
|
||||||
const formattedEnvelope = prettyPrintXml(envelope);
|
const formattedEnvelope = prettyPrintXml(envelope);
|
||||||
|
|
||||||
// Guardrails
|
// Guardrails
|
||||||
@@ -255,11 +268,11 @@ async function MakeRRCall({
|
|||||||
throw new Error("STAR envelope malformed: missing ProcessMessage/ApplicationArea");
|
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`, {
|
RRLogger(socket, "debug", `Sending RR SOAP request`, {
|
||||||
action,
|
action,
|
||||||
soapAction: RR_STAR_SOAP_ACTION,
|
soapAction: RR_SOAP_ACTION,
|
||||||
endpoint: baseUrl,
|
endpoint: baseUrl,
|
||||||
jobid,
|
jobid,
|
||||||
mode: "STAR"
|
mode: "STAR"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
const { normalizeRRDealerFields } = require("./rr-constants");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility: formats date/time to R&R’s preferred format (ISO or yyyy-MM-dd).
|
* 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 toBoolStr = (v) => (v === true ? "true" : v === false ? "false" : undefined);
|
||||||
const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined && v !== null && v !== "");
|
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 =====================
|
// ===================== CUSTOMER =====================
|
||||||
//
|
//
|
||||||
@@ -31,12 +41,13 @@ const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined
|
|||||||
*/
|
*/
|
||||||
function mapCustomerInsert(customer, bodyshopConfig) {
|
function mapCustomerInsert(customer, bodyshopConfig) {
|
||||||
if (!customer) return {};
|
if (!customer) return {};
|
||||||
|
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
DealerNumber: bodyshopConfig?.dealer_number,
|
DealerNumber: dealerNumber,
|
||||||
StoreNumber: bodyshopConfig?.store_number,
|
StoreNumber: storeNumber,
|
||||||
BranchNumber: bodyshopConfig?.branch_number,
|
BranchNumber: branchNumber,
|
||||||
RequestId: `CUST-INSERT-${customer.id}`,
|
RequestId: `CUST-INSERT-${customer.id}`,
|
||||||
Environment: process.env.NODE_ENV,
|
Environment: process.env.NODE_ENV,
|
||||||
|
|
||||||
@@ -116,12 +127,13 @@ function mapCustomerUpdate(customer, bodyshopConfig) {
|
|||||||
*/
|
*/
|
||||||
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
||||||
if (!vehicle) return {};
|
if (!vehicle) return {};
|
||||||
|
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
DealerNumber: bodyshopConfig?.dealer_number,
|
DealerNumber: dealerNumber,
|
||||||
StoreNumber: bodyshopConfig?.store_number,
|
StoreNumber: storeNumber,
|
||||||
BranchNumber: bodyshopConfig?.branch_number,
|
BranchNumber: branchNumber,
|
||||||
RequestId: `VEH-${vehicle.id}`,
|
RequestId: `VEH-${vehicle.id}`,
|
||||||
|
|
||||||
CustomerId: ownerCustomer?.external_id,
|
CustomerId: ownerCustomer?.external_id,
|
||||||
@@ -175,15 +187,16 @@ function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
|||||||
*/
|
*/
|
||||||
function mapRepairOrderCreate(job, bodyshopConfig) {
|
function mapRepairOrderCreate(job, bodyshopConfig) {
|
||||||
if (!job) return {};
|
if (!job) return {};
|
||||||
|
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||||
|
|
||||||
const cust = job.customer || {};
|
const cust = job.customer || {};
|
||||||
const veh = job.vehicle || {};
|
const veh = job.vehicle || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
DealerNumber: bodyshopConfig?.dealer_number,
|
DealerNumber: dealerNumber,
|
||||||
StoreNumber: bodyshopConfig?.store_number,
|
StoreNumber: storeNumber,
|
||||||
BranchNumber: bodyshopConfig?.branch_number,
|
BranchNumber: branchNumber,
|
||||||
RequestId: `RO-${job.id}`,
|
RequestId: `RO-${job.id}`,
|
||||||
Environment: process.env.NODE_ENV,
|
Environment: process.env.NODE_ENV,
|
||||||
|
|
||||||
@@ -302,11 +315,12 @@ function mapRepairOrderUpdate(job, bodyshopConfig) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
function mapAdvisorLookup(criteria, bodyshopConfig) {
|
function mapAdvisorLookup(criteria, bodyshopConfig) {
|
||||||
|
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||||
return {
|
return {
|
||||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
DealerNumber: bodyshopConfig?.dealer_number,
|
DealerNumber: dealerNumber,
|
||||||
StoreNumber: bodyshopConfig?.store_number,
|
StoreNumber: storeNumber,
|
||||||
BranchNumber: bodyshopConfig?.branch_number,
|
BranchNumber: branchNumber,
|
||||||
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
|
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
|
||||||
SearchCriteria: {
|
SearchCriteria: {
|
||||||
Department: criteria.department || "Body Shop",
|
Department: criteria.department || "Body Shop",
|
||||||
@@ -316,11 +330,12 @@ function mapAdvisorLookup(criteria, bodyshopConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapPartsLookup(criteria, bodyshopConfig) {
|
function mapPartsLookup(criteria, bodyshopConfig) {
|
||||||
|
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||||
return {
|
return {
|
||||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
DealerNumber: bodyshopConfig?.dealer_number,
|
DealerNumber: dealerNumber,
|
||||||
StoreNumber: bodyshopConfig?.store_number,
|
StoreNumber: storeNumber,
|
||||||
BranchNumber: bodyshopConfig?.branch_number,
|
BranchNumber: branchNumber,
|
||||||
RequestId: `LOOKUP-PART-${Date.now()}`,
|
RequestId: `LOOKUP-PART-${Date.now()}`,
|
||||||
SearchCriteria: {
|
SearchCriteria: {
|
||||||
PartNumber: criteria.part_number,
|
PartNumber: criteria.part_number,
|
||||||
@@ -335,6 +350,8 @@ function mapPartsLookup(criteria, bodyshopConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
||||||
|
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
|
||||||
|
|
||||||
// accept nested or flat input
|
// accept nested or flat input
|
||||||
const c = criteria || {};
|
const c = criteria || {};
|
||||||
const cust = c.customer || c.Customer || {};
|
const cust = c.customer || c.Customer || {};
|
||||||
@@ -364,14 +381,11 @@ function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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",
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
DealerName: bodyshopConfig?.dealer_name,
|
DealerName: bodyshopConfig?.dealer_name,
|
||||||
DealerNumber: bodyshopConfig?.dealer_number,
|
DealerNumber: dealerNumber,
|
||||||
StoreNumber: bodyshopConfig?.store_number,
|
StoreNumber: storeNumber,
|
||||||
BranchNumber: bodyshopConfig?.branch_number,
|
BranchNumber: branchNumber,
|
||||||
|
|
||||||
RequestId: c.requestId || `COMBINED-${Date.now()}`,
|
RequestId: c.requestId || `COMBINED-${Date.now()}`,
|
||||||
Environment: process.env.NODE_ENV,
|
Environment: process.env.NODE_ENV,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const dotenv = require("dotenv");
|
const dotenv = require("dotenv");
|
||||||
|
const { GraphQLClient, gql } = require("graphql-request");
|
||||||
const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers");
|
const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers");
|
||||||
const { getBaseRRConfig } = require("./rr-constants");
|
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 argv = process.argv.slice(2);
|
||||||
const args = { _: [] };
|
const args = { _: [] };
|
||||||
for (let i = 0; i < argv.length; i++) {
|
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];
|
const next = argv[i + 1];
|
||||||
if (next && !next.startsWith("-")) {
|
if (next && !next.startsWith("-")) {
|
||||||
args[k] = next;
|
args[k] = next;
|
||||||
i++; // consume value
|
i++;
|
||||||
} else {
|
} else {
|
||||||
args[k] = true; // boolean flag
|
args[k] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (a.startsWith("-") && a.length > 1) {
|
} else if (a.startsWith("-") && a.length > 1) {
|
||||||
// simple short flag handling: -a value
|
|
||||||
const k = a.slice(1);
|
const k = a.slice(1);
|
||||||
const next = argv[i + 1];
|
const next = argv[i + 1];
|
||||||
if (next && !next.startsWith("-")) {
|
if (next && !next.startsWith("-")) {
|
||||||
@@ -62,7 +62,65 @@ function toIntOr(defaultVal, maybe) {
|
|||||||
return Number.isFinite(n) ? n : defaultVal;
|
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) {
|
function pickActionName(raw) {
|
||||||
if (!raw || typeof raw !== "string") return "ping";
|
if (!raw || typeof raw !== "string") return "ping";
|
||||||
const x = raw.toLowerCase();
|
const x = raw.toLowerCase();
|
||||||
@@ -142,6 +200,8 @@ function buildBodyForAction(action, args, cfg) {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const action = pickActionName(args.action || args.a || args._[0]);
|
const action = pickActionName(args.action || args.a || args._[0]);
|
||||||
|
const bodyshopId = args.bodyshopId || args.bodyshop || args.b;
|
||||||
|
|
||||||
const rrAction =
|
const rrAction =
|
||||||
action === "ping"
|
action === "ping"
|
||||||
? "GetAdvisors"
|
? "GetAdvisors"
|
||||||
@@ -153,12 +213,28 @@ async function main() {
|
|||||||
? "GetParts"
|
? "GetParts"
|
||||||
: action;
|
: action;
|
||||||
|
|
||||||
|
// Start with env-based defaults…
|
||||||
const cfg = getBaseRRConfig();
|
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 body = buildBodyForAction(action, args, cfg);
|
||||||
const templateName = body.template || rrAction;
|
const templateName = body.template || rrAction;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const xml = await renderXmlTemplate(templateName, body.data);
|
await renderXmlTemplate(templateName, body.data);
|
||||||
console.log("✅ Templates verified.");
|
console.log("✅ Templates verified.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ Template verification failed:", e.message);
|
console.error("❌ Template verification failed:", e.message);
|
||||||
@@ -186,6 +262,4 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch(() => process.exit(1));
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -29,32 +29,57 @@ const { exportJobToRome } = require("./rr-job-export"); // orchestrator
|
|||||||
// Diagnostics
|
// Diagnostics
|
||||||
const { listActions, verifyTemplatesExist } = require("./rr-wsdl");
|
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 = {}) {
|
function ok(res, payload = {}) {
|
||||||
return res.json({ success: true, ...payload });
|
return res.json({ success: true, ...payload });
|
||||||
}
|
}
|
||||||
|
|
||||||
function fail(res, error, status = 400) {
|
function fail(res, error, status = 400) {
|
||||||
const message = error?.message || String(error);
|
const message = error?.message || String(error);
|
||||||
return res.status(status).json({ success: false, error: message, code: error?.code });
|
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) {
|
function socketOf(req) {
|
||||||
// If you stash a socket/logging context on the app, grab it; otherwise null
|
// 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;
|
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 --------------------
|
// -------------------- Customers --------------------
|
||||||
|
|
||||||
router.post("/rr/customer/insert", async (req, res) => {
|
router.post("/rr/customer/insert", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { customer } = req.body || {};
|
const { customer } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!customer) throw new RrApiError("Missing 'customer' in request body", "BAD_REQUEST");
|
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);
|
const result = await customerApi.insertCustomer(socket, customer, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -66,10 +91,10 @@ router.post("/rr/customer/insert", async (req, res) => {
|
|||||||
router.post("/rr/customer/update", async (req, res) => {
|
router.post("/rr/customer/update", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { customer } = req.body || {};
|
const { customer } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!customer) throw new RrApiError("Missing 'customer' in request body", "BAD_REQUEST");
|
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);
|
const result = await customerApi.updateCustomer(socket, customer, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -83,10 +108,10 @@ router.post("/rr/customer/update", async (req, res) => {
|
|||||||
router.post("/rr/repair-order/create", async (req, res) => {
|
router.post("/rr/repair-order/create", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { job } = req.body || {};
|
const { job } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
|
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);
|
const result = await roApi.createRepairOrder(socket, job, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -98,10 +123,10 @@ router.post("/rr/repair-order/create", async (req, res) => {
|
|||||||
router.post("/rr/repair-order/update", async (req, res) => {
|
router.post("/rr/repair-order/update", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { job } = req.body || {};
|
const { job } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
|
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);
|
const result = await roApi.updateRepairOrder(socket, job, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -115,9 +140,9 @@ router.post("/rr/repair-order/update", async (req, res) => {
|
|||||||
router.post("/rr/lookup/advisors", async (req, res) => {
|
router.post("/rr/lookup/advisors", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { criteria = {} } = req.body || {};
|
const { criteria = {} } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const cfg = await resolveRRConfigHttp(req);
|
||||||
const result = await lookupApi.getAdvisors(socket, criteria, cfg);
|
const result = await lookupApi.getAdvisors(socket, criteria, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -129,9 +154,9 @@ router.post("/rr/lookup/advisors", async (req, res) => {
|
|||||||
router.post("/rr/lookup/parts", async (req, res) => {
|
router.post("/rr/lookup/parts", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { criteria = {} } = req.body || {};
|
const { criteria = {} } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const cfg = await resolveRRConfigHttp(req);
|
||||||
const result = await lookupApi.getParts(socket, criteria, cfg);
|
const result = await lookupApi.getParts(socket, criteria, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -143,9 +168,9 @@ router.post("/rr/lookup/parts", async (req, res) => {
|
|||||||
router.post("/rr/lookup/combined-search", async (req, res) => {
|
router.post("/rr/lookup/combined-search", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { criteria = {} } = req.body || {};
|
const { criteria = {} } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const cfg = await resolveRRConfigHttp(req);
|
||||||
const result = await lookupApi.combinedSearch(socket, criteria, cfg);
|
const result = await lookupApi.combinedSearch(socket, criteria, cfg);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -159,10 +184,10 @@ router.post("/rr/lookup/combined-search", async (req, res) => {
|
|||||||
router.post("/rr/export/job", async (req, res) => {
|
router.post("/rr/export/job", async (req, res) => {
|
||||||
const socket = socketOf(req);
|
const socket = socketOf(req);
|
||||||
const { job, options = {} } = req.body || {};
|
const { job, options = {} } = req.body || {};
|
||||||
const cfg = pickConfig(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!job) throw new RrApiError("Missing 'job' in request body", "BAD_REQUEST");
|
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);
|
const result = await exportJobToRome(socket, job, cfg, options);
|
||||||
return ok(res, result);
|
return ok(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,21 +5,7 @@ const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/
|
|||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
const { exportJobToRome } = require("../rr/rr-job-export");
|
const { exportJobToRome } = require("../rr/rr-job-export");
|
||||||
const lookupApi = require("../rr/rr-lookup");
|
const lookupApi = require("../rr/rr-lookup");
|
||||||
|
const { getRRConfigForBodyshop } = require("../rr/rr-config");
|
||||||
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 redisSocketEvents = ({
|
const redisSocketEvents = ({
|
||||||
io,
|
io,
|
||||||
@@ -349,7 +335,7 @@ const redisSocketEvents = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("task-deleted", (payload) => {
|
socket.on("task-deleted", (payload) => {
|
||||||
if (!payload || !payload.id) return;
|
if (!payload?.id) return;
|
||||||
const room = getBodyshopRoom(socket.bodyshopId);
|
const room = getBodyshopRoom(socket.bodyshopId);
|
||||||
io.to(room).emit("bodyshop-message", { type: "task-deleted", payload });
|
io.to(room).emit("bodyshop-message", { type: "task-deleted", payload });
|
||||||
});
|
});
|
||||||
@@ -359,11 +345,13 @@ const redisSocketEvents = ({
|
|||||||
// Orchestrated Export (Customer → Vehicle → Repair Order)
|
// Orchestrated Export (Customer → Vehicle → Repair Order)
|
||||||
socket.on("rr-export-job", async (payload = {}) => {
|
socket.on("rr-export-job", async (payload = {}) => {
|
||||||
try {
|
try {
|
||||||
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, config, options }
|
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, options }
|
||||||
// Prefer direct job/config, otherwise try txEnvelope.{job,config}
|
// Prefer direct job, otherwise try txEnvelope.job
|
||||||
const job = payload.job || payload.txEnvelope?.job;
|
const job = payload.job || payload.txEnvelope?.job;
|
||||||
const options = payload.options || payload.txEnvelope?.options || {};
|
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) {
|
if (!job) {
|
||||||
RRLogger(socket, "error", "RR export missing job payload");
|
RRLogger(socket, "error", "RR export missing job payload");
|
||||||
@@ -383,7 +371,7 @@ const redisSocketEvents = ({
|
|||||||
// Combined search
|
// Combined search
|
||||||
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
||||||
try {
|
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);
|
const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
|
||||||
cb?.(data);
|
cb?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -395,7 +383,7 @@ const redisSocketEvents = ({
|
|||||||
// Get Advisors
|
// Get Advisors
|
||||||
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
|
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
|
||||||
try {
|
try {
|
||||||
const cfg = resolveRRConfigFrom({});
|
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
|
||||||
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
|
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
|
||||||
cb?.(data);
|
cb?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -407,7 +395,7 @@ const redisSocketEvents = ({
|
|||||||
// Get Parts
|
// Get Parts
|
||||||
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
|
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
|
||||||
try {
|
try {
|
||||||
const cfg = resolveRRConfigFrom({});
|
const cfg = await getRRConfigForBodyshop(socket.bodyshopId);
|
||||||
const data = await lookupApi.getParts(socket, params || {}, cfg);
|
const data = await lookupApi.getParts(socket, params || {}, cfg);
|
||||||
cb?.(data);
|
cb?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -419,9 +407,6 @@ const redisSocketEvents = ({
|
|||||||
// (Optional) Selected customer — only keep this if you actually implement it for RR
|
// (Optional) Selected customer — only keep this if you actually implement it for RR
|
||||||
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId } = {}) => {
|
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId } = {}) => {
|
||||||
try {
|
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)", {
|
RRLogger(socket, "info", "rr-selected-customer not implemented for RR (no-op)", {
|
||||||
jobid,
|
jobid,
|
||||||
selectedCustomerId
|
selectedCustomerId
|
||||||
|
|||||||
Reference in New Issue
Block a user