feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -65,6 +65,7 @@
|
|||||||
"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",
|
||||||
|
"xml-formatter": "^3.6.7",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1",
|
"xmlbuilder2": "^3.1.1",
|
||||||
"yazl": "^3.3.1"
|
"yazl": "^3.3.1"
|
||||||
@@ -11621,6 +11622,27 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml-formatter": {
|
||||||
|
"version": "3.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.7.tgz",
|
||||||
|
"integrity": "sha512-IsfFYJQuoDqtUlKhm4EzeoBOb+fQwzQVeyxxAQ0sThn/nFnQmyLPTplqq4yRhaOENH/tAyujD2TBfIYzUKB6hg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-parser-xo": "^4.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-parser-xo": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml2js": {
|
"node_modules/xml2js": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
"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",
|
||||||
|
"xml-formatter": "^3.6.7",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1",
|
"xmlbuilder2": "^3.1.1",
|
||||||
"yazl": "^3.3.1"
|
"yazl": "^3.3.1"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const applyRoutes = ({ app }) => {
|
|||||||
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
||||||
app.use("/sso", require("./server/routes/ssoRoutes"));
|
app.use("/sso", require("./server/routes/ssoRoutes"));
|
||||||
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
||||||
app.use("/rr", require("./server/routes/rrRoutes"));
|
app.use("/rr", require("./server/rr/rrRoutes"));
|
||||||
|
|
||||||
// Default route for forbidden access
|
// Default route for forbidden access
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file rrRoutes.js
|
|
||||||
* @description Express Router for Reynolds & Reynolds (Rome) DMS integration.
|
|
||||||
* Provides endpoints for lookup, customer management, repair orders, and full job export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const express = require("express");
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
|
||||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
|
||||||
|
|
||||||
const { RrCombinedSearch, RrGetAdvisors, RrGetParts } = require("../rr/rr-lookup");
|
|
||||||
const { RrCustomerInsert, RrCustomerUpdate } = require("../rr/rr-customer");
|
|
||||||
const { CreateRepairOrder, UpdateRepairOrder } = require("../rr/rr-repair-orders");
|
|
||||||
const { ExportJobToRR } = require("../rr/rr-job-export");
|
|
||||||
const RRLogger = require("../rr/rr-logger");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply global middlewares:
|
|
||||||
* - Firebase token validation (auth)
|
|
||||||
* - GraphQL client injection (Hasura access)
|
|
||||||
*/
|
|
||||||
router.use(validateFirebaseIdTokenMiddleware);
|
|
||||||
router.use(withUserGraphQLClientMiddleware);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Health check / diagnostic route
|
|
||||||
*/
|
|
||||||
router.get("/", async (req, res) => {
|
|
||||||
res.status(200).json({ provider: "Reynolds & Reynolds (Rome)", status: "OK" });
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full DMS export for a single job
|
|
||||||
* POST /rr/job/export
|
|
||||||
* Body: { JobData: {...} }
|
|
||||||
*/
|
|
||||||
router.post("/job/export", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { JobData } = req.body;
|
|
||||||
RRLogger(req, "info", "RR /job/export initiated", { jobid: JobData?.id });
|
|
||||||
|
|
||||||
const result = await ExportJobToRR({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
JobData
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(result.success ? 200 : 500).json(result);
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /job/export failed: ${error.message}`);
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customer insert
|
|
||||||
* POST /rr/customer/insert
|
|
||||||
*/
|
|
||||||
router.post("/customer/insert", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { JobData } = req.body;
|
|
||||||
const data = await RrCustomerInsert({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
JobData
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /customer/insert failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customer update
|
|
||||||
* PUT /rr/customer/update/:id
|
|
||||||
*/
|
|
||||||
router.put("/customer/update/:id", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { JobData, existingCustomer, patch } = req.body;
|
|
||||||
const data = await RrCustomerUpdate({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
JobData,
|
|
||||||
existingCustomer,
|
|
||||||
patch
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /customer/update failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Repair Order
|
|
||||||
* POST /rr/repair-order/create
|
|
||||||
*/
|
|
||||||
router.post("/repair-order/create", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { JobData, txEnvelope } = req.body;
|
|
||||||
const data = await CreateRepairOrder({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
JobData,
|
|
||||||
txEnvelope
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /repair-order/create failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update Repair Order
|
|
||||||
* PUT /rr/repair-order/update/:id
|
|
||||||
*/
|
|
||||||
router.put("/repair-order/update/:id", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { JobData, txEnvelope } = req.body;
|
|
||||||
const data = await UpdateRepairOrder({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
JobData,
|
|
||||||
txEnvelope
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /repair-order/update failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combined search (customer + service vehicle)
|
|
||||||
* GET /rr/lookup/combined?vin=XXX&lastname=DOE
|
|
||||||
*/
|
|
||||||
router.get("/lookup/combined", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const params = Object.entries(req.query);
|
|
||||||
const data = await RrCombinedSearch({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
jobid: "ad-hoc",
|
|
||||||
params
|
|
||||||
});
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /lookup/combined failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Advisors
|
|
||||||
* GET /rr/advisors
|
|
||||||
*/
|
|
||||||
router.get("/advisors", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const params = Object.entries(req.query);
|
|
||||||
const data = await RrGetAdvisors({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
jobid: "ad-hoc",
|
|
||||||
params
|
|
||||||
});
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /advisors failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Parts
|
|
||||||
* GET /rr/parts
|
|
||||||
*/
|
|
||||||
router.get("/parts", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const params = Object.entries(req.query);
|
|
||||||
const data = await RrGetParts({
|
|
||||||
socket: req,
|
|
||||||
redisHelpers: req.sessionUtils,
|
|
||||||
jobid: "ad-hoc",
|
|
||||||
params
|
|
||||||
});
|
|
||||||
res.status(200).json({ success: true, data });
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(req, "error", `RR /parts failed: ${error.message}`);
|
|
||||||
res.status(500).json({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not implemented placeholder (for future expansion)
|
|
||||||
*/
|
|
||||||
router.post("/calculate-allocations", async (req, res) => {
|
|
||||||
res.status(501).json({ error: "RR calculate-allocations not yet implemented" });
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
381
server/rr/rr-calculate-allocations.js
Normal file
381
server/rr/rr-calculate-allocations.js
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
// server/rr/rr-calculate-allocations.js
|
||||||
|
const { GraphQLClient } = require("graphql-request");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
const Dinero = require("dinero.js");
|
||||||
|
const _ = require("lodash");
|
||||||
|
const WsLogger = require("../web-sockets/createLogEvent").default || require("../web-sockets/createLogEvent");
|
||||||
|
|
||||||
|
// InstanceManager wiring (same as CDK file)
|
||||||
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
|
const { DiscountNotAlreadyCounted } = InstanceManager({
|
||||||
|
imex: require("../job/job-totals"),
|
||||||
|
rome: require("../job/job-totals-USA")
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP route version (parity with CDK file)
|
||||||
|
*/
|
||||||
|
exports.defaultRoute = async function rrAllocationsHttp(req, res) {
|
||||||
|
try {
|
||||||
|
WsLogger.createLogEvent(req, "DEBUG", `RR: calculate allocations request for ${req.body.jobid}`);
|
||||||
|
const jobData = await queryJobData(req, req.BearerToken, req.body.jobid);
|
||||||
|
return res.status(200).json({ data: calculateAllocations(req, jobData) });
|
||||||
|
} catch (error) {
|
||||||
|
WsLogger.createLogEvent(req, "ERROR", `RR CalculateAllocations error. ${error}`);
|
||||||
|
res.status(500).json({ error: `RR CalculateAllocations error. ${error}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket version (parity with CDK file)
|
||||||
|
* @param {Socket} socket
|
||||||
|
* @param {string} jobid
|
||||||
|
* @returns {Promise<Array>} allocations
|
||||||
|
*/
|
||||||
|
exports.default = async function rrCalculateAllocations(socket, jobid) {
|
||||||
|
try {
|
||||||
|
const token = "Bearer " + socket.handshake.auth.token;
|
||||||
|
const jobData = await queryJobData(socket, token, jobid, /* isFortellis */ false);
|
||||||
|
return calculateAllocations(socket, jobData);
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "ERROR", `RR CalculateAllocations error. ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function queryJobData(connectionData, token, jobid /* , isFortellis */) {
|
||||||
|
WsLogger.createLogEvent(connectionData, "DEBUG", `RR: querying job data for id ${jobid}`);
|
||||||
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||||
|
const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid });
|
||||||
|
WsLogger.createLogEvent(connectionData, "DEBUG", `RR: job data query result ${JSON.stringify(result, null, 2)}`);
|
||||||
|
return result.jobs_by_pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core allocation logic – mirrors CDK version, but logs as RR
|
||||||
|
*/
|
||||||
|
function calculateAllocations(connectionData, job) {
|
||||||
|
const { bodyshop } = job;
|
||||||
|
|
||||||
|
// Build tax allocation maps for US (Rome) and IMEX (Canada) contexts
|
||||||
|
const taxAllocations = InstanceManager({
|
||||||
|
executeFunction: true,
|
||||||
|
deubg: true,
|
||||||
|
args: [],
|
||||||
|
imex: () => ({
|
||||||
|
state: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes.state.name,
|
||||||
|
sale: Dinero(job.job_totals.totals.state_tax),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes.state,
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes.state
|
||||||
|
},
|
||||||
|
federal: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes.federal.name,
|
||||||
|
sale: Dinero(job.job_totals.totals.federal_tax),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes.federal,
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes.federal
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
rome: () => ({
|
||||||
|
tax_ty1: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes[`tax_ty1`].name,
|
||||||
|
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty1Tax`]),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`],
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`]
|
||||||
|
},
|
||||||
|
tax_ty2: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes[`tax_ty2`].name,
|
||||||
|
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty2Tax`]),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`],
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`]
|
||||||
|
},
|
||||||
|
tax_ty3: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes[`tax_ty3`].name,
|
||||||
|
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty3Tax`]),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`],
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`]
|
||||||
|
},
|
||||||
|
tax_ty4: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes[`tax_ty4`].name,
|
||||||
|
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty4Tax`]),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`],
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`]
|
||||||
|
},
|
||||||
|
tax_ty5: {
|
||||||
|
center: bodyshop.md_responsibility_centers.taxes[`tax_ty5`].name,
|
||||||
|
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty5Tax`]),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`],
|
||||||
|
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect existing MAPA/MASH (Mitchell) lines so we don’t double count
|
||||||
|
let hasMapaLine = false;
|
||||||
|
let hasMashLine = false;
|
||||||
|
|
||||||
|
const profitCenterHash = job.joblines.reduce((acc, val) => {
|
||||||
|
if (val.db_ref === "936008") hasMapaLine = true; // paint materials (MAPA)
|
||||||
|
if (val.db_ref === "936007") hasMashLine = true; // shop supplies (MASH)
|
||||||
|
|
||||||
|
if (val.profitcenter_part) {
|
||||||
|
if (!acc[val.profitcenter_part]) acc[val.profitcenter_part] = Dinero();
|
||||||
|
|
||||||
|
let dineroAmount = Dinero({ amount: Math.round(val.act_price * 100) }).multiply(val.part_qty || 1);
|
||||||
|
|
||||||
|
// Conditional discount add-on if not already counted elsewhere
|
||||||
|
dineroAmount = dineroAmount.add(
|
||||||
|
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
|
||||||
|
DiscountNotAlreadyCounted(val, job.joblines)
|
||||||
|
? val.prt_dsmk_m
|
||||||
|
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
|
||||||
|
: Dinero({ amount: Math.round(val.act_price * 100) })
|
||||||
|
.multiply(val.part_qty || 0)
|
||||||
|
.percentage(Math.abs(val.prt_dsmk_p || 0))
|
||||||
|
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
|
||||||
|
: Dinero()
|
||||||
|
);
|
||||||
|
|
||||||
|
acc[val.profitcenter_part] = acc[val.profitcenter_part].add(dineroAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.profitcenter_labor && val.mod_lbr_ty) {
|
||||||
|
if (!acc[val.profitcenter_labor]) acc[val.profitcenter_labor] = Dinero();
|
||||||
|
acc[val.profitcenter_labor] = acc[val.profitcenter_labor].add(
|
||||||
|
Dinero({ amount: Math.round(job[`rate_${val.mod_lbr_ty.toLowerCase()}`] * 100) }).multiply(val.mod_lb_hrs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find(
|
||||||
|
(d) => d.name === job.dms_allocation
|
||||||
|
);
|
||||||
|
|
||||||
|
WsLogger.createLogEvent(
|
||||||
|
connectionData,
|
||||||
|
"DEBUG",
|
||||||
|
`RR: Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build cost center totals from bills and time tickets
|
||||||
|
let costCenterHash = {};
|
||||||
|
const disableBillWip = !!bodyshop?.pbs_configuration?.disablebillwip;
|
||||||
|
if (!disableBillWip) {
|
||||||
|
costCenterHash = job.bills.reduce((billAcc, bill) => {
|
||||||
|
bill.billlines.forEach((line) => {
|
||||||
|
const target = selectedDmsAllocationConfig.costs[line.cost_center];
|
||||||
|
if (!billAcc[target]) billAcc[target] = Dinero();
|
||||||
|
|
||||||
|
let lineDinero = Dinero({ amount: Math.round((line.actual_cost || 0) * 100) })
|
||||||
|
.multiply(line.quantity)
|
||||||
|
.multiply(bill.is_credit_memo ? -1 : 1);
|
||||||
|
|
||||||
|
billAcc[target] = billAcc[target].add(lineDinero);
|
||||||
|
});
|
||||||
|
return billAcc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
job.timetickets.forEach((ticket) => {
|
||||||
|
const ticketTotal = Dinero({
|
||||||
|
amount: Math.round(
|
||||||
|
ticket.rate *
|
||||||
|
(ticket.employee && ticket.employee.flat_rate ? ticket.productivehrs || 0 : ticket.actualhrs || 0) *
|
||||||
|
100
|
||||||
|
)
|
||||||
|
});
|
||||||
|
const target = selectedDmsAllocationConfig.costs[ticket.ciecacode];
|
||||||
|
if (!costCenterHash[target]) costCenterHash[target] = Dinero();
|
||||||
|
costCenterHash[target] = costCenterHash[target].add(ticketTotal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add MAPA/MASH lines when not explicitly present
|
||||||
|
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
|
||||||
|
const accountName = selectedDmsAllocationConfig.profits.MAPA;
|
||||||
|
const account = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
|
||||||
|
if (account) {
|
||||||
|
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
|
||||||
|
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates.mapa.total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
|
||||||
|
const accountName = selectedDmsAllocationConfig.profits.MASH;
|
||||||
|
const account = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
|
||||||
|
if (account) {
|
||||||
|
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
|
||||||
|
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates.mash.total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional materials costing (CDK setting reused by RR sites if configured)
|
||||||
|
if (bodyshop?.cdk_configuration?.sendmaterialscosting) {
|
||||||
|
const percent = bodyshop.cdk_configuration.sendmaterialscosting;
|
||||||
|
// Paint Mat
|
||||||
|
const mapaCostName = selectedDmsAllocationConfig.costs.MAPA;
|
||||||
|
const mapaCost = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mapaCostName);
|
||||||
|
if (mapaCost) {
|
||||||
|
if (!costCenterHash[mapaCostName]) costCenterHash[mapaCostName] = Dinero();
|
||||||
|
if (job.bodyshop.use_paint_scale_data === true && job.mixdata.length > 0) {
|
||||||
|
costCenterHash[mapaCostName] = costCenterHash[mapaCostName].add(
|
||||||
|
Dinero({ amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100) })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
costCenterHash[mapaCostName] = costCenterHash[mapaCostName].add(
|
||||||
|
Dinero(job.job_totals.rates.mapa.total).percentage(percent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Shop Mat
|
||||||
|
const mashCostName = selectedDmsAllocationConfig.costs.MASH;
|
||||||
|
const mashCost = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mashCostName);
|
||||||
|
if (mashCost) {
|
||||||
|
if (!costCenterHash[mashCostName]) costCenterHash[mashCostName] = Dinero();
|
||||||
|
costCenterHash[mashCostName] = costCenterHash[mashCostName].add(
|
||||||
|
Dinero(job.job_totals.rates.mash.total).percentage(percent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provinical PVRT roll-in (Canada only)
|
||||||
|
const { ca_bc_pvrt } = job;
|
||||||
|
if (ca_bc_pvrt) {
|
||||||
|
taxAllocations.state.sale = taxAllocations.state.sale.add(Dinero({ amount: Math.round((ca_bc_pvrt || 0) * 100) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Towing / Storage / Other adjustments
|
||||||
|
if (job.towing_payable && job.towing_payable !== 0) {
|
||||||
|
const name = selectedDmsAllocationConfig.profits.TOW;
|
||||||
|
const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name);
|
||||||
|
if (acct) {
|
||||||
|
if (!profitCenterHash[name]) profitCenterHash[name] = Dinero();
|
||||||
|
profitCenterHash[name] = profitCenterHash[name].add(
|
||||||
|
Dinero({ amount: Math.round((job.towing_payable || 0) * 100) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (job.storage_payable && job.storage_payable !== 0) {
|
||||||
|
const name = selectedDmsAllocationConfig.profits.TOW;
|
||||||
|
const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name);
|
||||||
|
if (acct) {
|
||||||
|
if (!profitCenterHash[name]) profitCenterHash[name] = Dinero();
|
||||||
|
profitCenterHash[name] = profitCenterHash[name].add(
|
||||||
|
Dinero({ amount: Math.round((job.storage_payable || 0) * 100) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
|
||||||
|
const name = selectedDmsAllocationConfig.profits.PAO;
|
||||||
|
const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name);
|
||||||
|
if (acct) {
|
||||||
|
if (!profitCenterHash[name]) profitCenterHash[name] = Dinero();
|
||||||
|
profitCenterHash[name] = profitCenterHash[name].add(
|
||||||
|
Dinero({ amount: Math.round((job.adjustment_bottom_line || 0) * 100) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rome profile-level adjustments for parts / labor / materials
|
||||||
|
if (InstanceManager({ rome: true })) {
|
||||||
|
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
|
||||||
|
const name = selectedDmsAllocationConfig.profits[key];
|
||||||
|
const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name);
|
||||||
|
if (acct) {
|
||||||
|
if (!profitCenterHash[name]) profitCenterHash[name] = Dinero();
|
||||||
|
profitCenterHash[name] = profitCenterHash[name].add(Dinero(job.job_totals.parts.adjustments[key]));
|
||||||
|
} else {
|
||||||
|
WsLogger.createLogEvent(connectionData, "ERROR", `RR CalculateAllocations: missing parts adj account: ${name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(job.job_totals.rates).forEach((key) => {
|
||||||
|
const rate = job.job_totals.rates[key];
|
||||||
|
if (rate && rate.adjustment && Dinero(rate.adjustment).isZero() === false) {
|
||||||
|
const name = selectedDmsAllocationConfig.profits[key.toUpperCase()];
|
||||||
|
const acct = bodyshop.md_responsibility_centers.profits.find((c) => c.name === name);
|
||||||
|
if (acct) {
|
||||||
|
if (!profitCenterHash[name]) profitCenterHash[name] = Dinero();
|
||||||
|
// NOTE: the original code had rate.adjustments (plural). If that’s a bug upstream, fix there.
|
||||||
|
profitCenterHash[name] = profitCenterHash[name].add(Dinero(rate.adjustments || rate.adjustment));
|
||||||
|
} else {
|
||||||
|
WsLogger.createLogEvent(
|
||||||
|
connectionData,
|
||||||
|
"ERROR",
|
||||||
|
`RR CalculateAllocations: missing rate adj account: ${name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge profit & cost centers
|
||||||
|
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {
|
||||||
|
const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === key);
|
||||||
|
const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === key);
|
||||||
|
return {
|
||||||
|
center: key,
|
||||||
|
sale: profitCenterHash[key] ? profitCenterHash[key] : Dinero(),
|
||||||
|
cost: costCenterHash[key] ? costCenterHash[key] : Dinero(),
|
||||||
|
profitCenter,
|
||||||
|
costCenter
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add tax centers (non-zero only)
|
||||||
|
const taxRows = Object.keys(taxAllocations)
|
||||||
|
.filter((k) => taxAllocations[k].sale.getAmount() > 0 || taxAllocations[k].cost.getAmount() > 0)
|
||||||
|
.map((k) => {
|
||||||
|
const base = { ...taxAllocations[k], tax: k };
|
||||||
|
// Optional GST override preserved from CDK logic
|
||||||
|
const override = selectedDmsAllocationConfig.gst_override;
|
||||||
|
if (k === "federal" && override) {
|
||||||
|
base.costCenter.dms_acctnumber = override;
|
||||||
|
base.profitCenter.dms_acctnumber = override;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Totals adjustments centers
|
||||||
|
const extra = [];
|
||||||
|
if (job.job_totals.totals.ttl_adjustment) {
|
||||||
|
extra.push({
|
||||||
|
center: "SUB ADJ",
|
||||||
|
sale: Dinero(job.job_totals.totals.ttl_adjustment),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: {
|
||||||
|
name: "SUB ADJ",
|
||||||
|
accountdesc: "SUB ADJ",
|
||||||
|
accountitem: "SUB ADJ",
|
||||||
|
accountname: "SUB ADJ",
|
||||||
|
dms_acctnumber: bodyshop.md_responsibility_centers.ttl_adjustment.dms_acctnumber
|
||||||
|
},
|
||||||
|
costCenter: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (job.job_totals.totals.ttl_tax_adjustment) {
|
||||||
|
extra.push({
|
||||||
|
center: "TAX ADJ",
|
||||||
|
sale: Dinero(job.job_totals.totals.ttl_tax_adjustment),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: {
|
||||||
|
name: "TAX ADJ",
|
||||||
|
accountdesc: "TAX ADJ",
|
||||||
|
accountitem: "TAX ADJ",
|
||||||
|
accountname: "TAX ADJ",
|
||||||
|
dms_acctnumber: bodyshop.md_responsibility_centers.ttl_tax_adjustment.dms_acctnumber
|
||||||
|
},
|
||||||
|
costCenter: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...jobAllocations, ...taxRows, ...extra];
|
||||||
|
}
|
||||||
@@ -1,79 +1,50 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-constants.js
|
* STAR-only constants for Reynolds & Reynolds (Rome/RCI)
|
||||||
* @description Central constants and configuration for Reynolds & Reynolds (R&R) integration.
|
* Used by rr-helpers.js to build and send SOAP requests.
|
||||||
* Platform-level secrets (API base URL, username, password, ppsysId, dealer/store/branch) are loaded from .env
|
|
||||||
* Dealer-specific values (overrides) come from bodyshop.rr_configuration.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RR_TIMEOUT_MS = 30000; // 30-second SOAP call timeout
|
exports.RR_NS = Object.freeze({
|
||||||
const RR_NAMESPACE_URI = "http://reynoldsandrey.com/";
|
SOAP_ENV: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||||
const RR_DEFAULT_MAX_RESULTS = 25;
|
SOAP_ENC: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||||
|
XSD: "http://www.w3.org/2001/XMLSchema",
|
||||||
/**
|
XSI: "http://www.w3.org/2001/XMLSchema-instance",
|
||||||
* Maps internal operation names to Reynolds & Reynolds SOAP actions.
|
WSSE: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
|
||||||
* soapAction is sent as the SOAPAction header; URL selection happens in rr-helpers.
|
WSU: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
|
||||||
*/
|
STAR_TRANSPORT: "http://www.starstandards.org/webservices/2005/10/transport",
|
||||||
const RR_ACTIONS = {
|
STAR_BUSINESS: "http://www.starstandards.org/STAR"
|
||||||
GetAdvisors: { soapAction: "GetAdvisors" },
|
|
||||||
GetParts: { soapAction: "GetParts" },
|
|
||||||
CombinedSearch: { soapAction: "CombinedSearch" },
|
|
||||||
InsertCustomer: { soapAction: "CustomerInsert" },
|
|
||||||
UpdateCustomer: { soapAction: "CustomerUpdate" },
|
|
||||||
InsertServiceVehicle: { soapAction: "ServiceVehicleInsert" },
|
|
||||||
CreateRepairOrder: { soapAction: "RepairOrderInsert" },
|
|
||||||
UpdateRepairOrder: { soapAction: "RepairOrderUpdate" }
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default SOAP HTTP headers. SOAPAction is dynamically set per request.
|
|
||||||
*/
|
|
||||||
const RR_SOAP_HEADERS = {
|
|
||||||
"Content-Type": "text/xml; charset=utf-8",
|
|
||||||
SOAPAction: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps the rendered XML body inside a SOAP envelope.
|
|
||||||
* @param {string} xmlBody - Inner request XML
|
|
||||||
* @param {string} [headerXml] - Optional header XML (already namespaced)
|
|
||||||
*/
|
|
||||||
const buildSoapEnvelope = (xmlBody, headerXml = "") => `
|
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="${RR_NAMESPACE_URI}">
|
|
||||||
<soapenv:Header>
|
|
||||||
${headerXml}
|
|
||||||
</soapenv:Header>
|
|
||||||
<soapenv:Body>
|
|
||||||
${xmlBody}
|
|
||||||
</soapenv:Body>
|
|
||||||
</soapenv:Envelope>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads base configuration for R&R requests from environment variables.
|
|
||||||
* Dealer-specific overrides come from bodyshop.rr_configuration in the DB.
|
|
||||||
*/
|
|
||||||
const getBaseRRConfig = () => ({
|
|
||||||
// IMPORTANT: RCI Receive endpoint ends with .ashx
|
|
||||||
baseUrl: process.env.RR_API_BASE_URL || "https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx",
|
|
||||||
username: process.env.RR_API_USER || "",
|
|
||||||
password: process.env.RR_API_PASS || "",
|
|
||||||
ppsysId: process.env.RR_PPSYS_ID || "",
|
|
||||||
|
|
||||||
// Welcome Kit often provides these (used in SOAP header)
|
|
||||||
dealerNumber: process.env.RR_DEALER_NUMBER || "",
|
|
||||||
storeNumber: process.env.RR_STORE_NUMBER || "",
|
|
||||||
branchNumber: process.env.RR_BRANCH_NUMBER || "",
|
|
||||||
|
|
||||||
dealerDefault: process.env.RR_DEFAULT_DEALER || "ROME",
|
|
||||||
timeout: RR_TIMEOUT_MS
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
const RR_STAR_SOAP_ACTION = "http://www.starstandards.org/webservices/2005/10/transport/ProcessMessage";
|
||||||
RR_TIMEOUT_MS,
|
exports.RR_SOAP_ACTION = RR_STAR_SOAP_ACTION;
|
||||||
RR_NAMESPACE_URI,
|
|
||||||
RR_DEFAULT_MAX_RESULTS,
|
const RR_SOAP_HEADERS = {
|
||||||
RR_ACTIONS,
|
"Content-Type": "text/xml; charset=utf-8",
|
||||||
RR_SOAP_HEADERS,
|
SOAPAction: RR_STAR_SOAP_ACTION
|
||||||
buildSoapEnvelope,
|
};
|
||||||
getBaseRRConfig
|
|
||||||
|
// All STAR-supported actions (mapped to Mustache templates)
|
||||||
|
exports.RR_ACTIONS = Object.freeze({
|
||||||
|
CombinedSearch: { template: "CombinedSearch" },
|
||||||
|
GetAdvisors: { template: "GetAdvisors" },
|
||||||
|
GetParts: { template: "GetParts" },
|
||||||
|
InsertCustomer: { template: "InsertCustomer" },
|
||||||
|
InsertServiceVehicle: { template: "InsertServiceVehicle" },
|
||||||
|
CreateRepairOrder: { template: "CreateRepairOrder" },
|
||||||
|
UpdateCustomer: { template: "UpdateCustomer" },
|
||||||
|
UpdateRepairOrder: { template: "UpdateRepairOrder" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Base config loader (environment-driven)
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,137 +1,99 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-customer.js
|
* @file rr-customer.js
|
||||||
* @description Reynolds & Reynolds (Rome) Customer Insert/Update integration.
|
* @description Rome (Reynolds & Reynolds) Customer Insert / Update integration.
|
||||||
* Builds request payloads using rr-mappers and executes via rr-helpers.
|
* Maps internal customer objects to Rome XML schemas and executes RCI calls.
|
||||||
* All dealer-specific data (DealerNumber, LocationId, etc.) is read from the DB (bodyshop.rr_configuration).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
const { MakeRRCall } = require("./rr-helpers");
|
||||||
const { assertRrOk } = require("./rr-error");
|
|
||||||
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
|
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
const { client } = require("../graphql-client/graphql-client");
|
const { RrApiError } = require("./rr-error");
|
||||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch rr_configuration for the current bodyshop directly from DB.
|
* Insert a new customer into Rome.
|
||||||
* This ensures we always have the latest Dealer/Location mapping.
|
* @param {Socket} socket - WebSocket connection for logging context
|
||||||
|
* @param {Object} customer - Hasura customer record
|
||||||
|
* @param {Object} bodyshopConfig - DMS configuration
|
||||||
|
* @returns {Promise<Object>} result
|
||||||
*/
|
*/
|
||||||
async function getDealerConfigFromDB(bodyshopId, logger) {
|
async function insertCustomer(socket, customer, bodyshopConfig) {
|
||||||
|
const action = "InsertCustomer";
|
||||||
|
const template = "InsertCustomer";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
|
||||||
const config = result?.bodyshops_by_pk?.rr_configuration || null;
|
|
||||||
|
|
||||||
if (!config) {
|
const data = mapCustomerInsert(customer, bodyshopConfig);
|
||||||
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config);
|
const resultXml = await MakeRRCall({
|
||||||
return config;
|
action,
|
||||||
|
body: { template, data },
|
||||||
|
socket,
|
||||||
|
dealerConfig: bodyshopConfig,
|
||||||
|
jobid: customer.id
|
||||||
|
});
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
dms: "Rome",
|
||||||
|
action,
|
||||||
|
customerId: customer.id,
|
||||||
|
xml: resultXml
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, {
|
RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
|
||||||
bodyshopId,
|
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
throw error;
|
throw new RrApiError(`RR InsertCustomer failed: ${error.message}`, "INSERT_CUSTOMER_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CUSTOMER INSERT (Rome Customer Insert Specification 1.2)
|
* Update an existing customer in Rome.
|
||||||
* Creates a new customer record in the DMS.
|
* @param {Socket} socket
|
||||||
*
|
* @param {Object} customer
|
||||||
* @param {object} options
|
* @param {Object} bodyshopConfig
|
||||||
* @param {object} options.socket - socket.io connection or express req
|
* @returns {Promise<Object>}
|
||||||
* @param {object} options.redisHelpers
|
|
||||||
* @param {object} options.JobData - normalized job record
|
|
||||||
*/
|
*/
|
||||||
async function RrCustomerInsert({ socket, redisHelpers, JobData }) {
|
async function updateCustomer(socket, customer, bodyshopConfig) {
|
||||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
const action = "UpdateCustomer";
|
||||||
const logger = socket?.logger || console;
|
const template = "UpdateCustomer";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RRLogger(socket, "info", "RR Customer Insert started", { jobid: JobData?.id, bodyshopId });
|
RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
|
||||||
|
|
||||||
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
const data = mapCustomerUpdate(customer, bodyshopConfig);
|
||||||
|
|
||||||
// Build Mustache variables for the InsertCustomer.xml template
|
const resultXml = await MakeRRCall({
|
||||||
const vars = mapCustomerInsert(JobData, dealerConfig);
|
action,
|
||||||
|
body: { template, data },
|
||||||
const data = await MakeRRCall({
|
|
||||||
action: RRActions.CreateCustomer, // resolves to SOAPAction + URL
|
|
||||||
body: { template: "InsertCustomer", data: vars }, // render server/rr/xml-templates/InsertCustomer.xml
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
socket,
|
||||||
jobid: JobData.id
|
dealerConfig: bodyshopConfig,
|
||||||
|
jobid: customer.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = assertRrOk(data, { apiName: "RR Create Customer" });
|
RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
|
||||||
RRLogger(socket, "debug", "RR Customer Insert success", {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
dealer: dealerConfig?.dealerCode || dealerConfig?.dealer_code
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
return {
|
||||||
|
success: true,
|
||||||
|
dms: "Rome",
|
||||||
|
action,
|
||||||
|
customerId: customer.id,
|
||||||
|
xml: resultXml
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `RR Customer Insert failed: ${error.message}`, { jobid: JobData?.id });
|
RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
|
||||||
throw error;
|
message: error.message,
|
||||||
}
|
stack: error.stack
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CUSTOMER UPDATE (Rome Customer Update Specification 1.2)
|
|
||||||
* Updates an existing RR customer record.
|
|
||||||
*
|
|
||||||
* @param {object} options
|
|
||||||
* @param {object} options.socket
|
|
||||||
* @param {object} options.redisHelpers
|
|
||||||
* @param {object} options.JobData
|
|
||||||
* @param {object} options.existingCustomer - current RR customer record (from Combined Search)
|
|
||||||
* @param {object} options.patch - updated fields from frontend
|
|
||||||
*/
|
|
||||||
async function RrCustomerUpdate({ socket, redisHelpers, JobData, existingCustomer, patch }) {
|
|
||||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
|
||||||
const logger = socket?.logger || console;
|
|
||||||
|
|
||||||
try {
|
|
||||||
RRLogger(socket, "info", "RR Customer Update started", {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
bodyshopId,
|
|
||||||
existingCustomerId: existingCustomer?.CustomerId
|
|
||||||
});
|
});
|
||||||
|
throw new RrApiError(`RR UpdateCustomer failed: ${error.message}`, "UPDATE_CUSTOMER_ERROR");
|
||||||
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
|
||||||
|
|
||||||
// Build Mustache variables for the UpdateCustomer.xml template
|
|
||||||
const vars = mapCustomerUpdate(existingCustomer, patch, dealerConfig);
|
|
||||||
|
|
||||||
const data = await MakeRRCall({
|
|
||||||
action: RRActions.UpdateCustomer, // resolves to SOAPAction + URL
|
|
||||||
body: { template: "UpdateCustomer", data: vars }, // render server/rr/xml-templates/UpdateCustomer.xml
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = assertRrOk(data, { apiName: "RR Update Customer" });
|
|
||||||
RRLogger(socket, "debug", "RR Customer Update success", {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
customerId: existingCustomer?.CustomerId
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(socket, "error", `RR Customer Update failed: ${error.message}`, {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
customerId: existingCustomer?.CustomerId
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
RrCustomerInsert,
|
insertCustomer,
|
||||||
RrCustomerUpdate,
|
updateCustomer
|
||||||
getDealerConfigFromDB
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,103 +1,48 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-error.js
|
* @file rr-error.js
|
||||||
* @description Centralized error class and assertion logic for Reynolds & Reynolds API calls.
|
* @description Custom error types for the Reynolds & Reynolds (Rome) integration.
|
||||||
* Provides consistent handling across all RR modules (customer, repair order, lookups, etc.)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Error type for RR API responses
|
* Base RR API Error class — always structured with a message and a code.
|
||||||
*/
|
*/
|
||||||
class RrApiError extends Error {
|
class RrApiError extends Error {
|
||||||
constructor(message, { reqId, url, apiName, errorData, status, statusText } = {}) {
|
/**
|
||||||
|
* @param {string} message - Human-readable message
|
||||||
|
* @param {string} [code="RR_ERROR"] - Short machine-readable error code
|
||||||
|
* @param {Object} [details] - Optional structured metadata
|
||||||
|
*/
|
||||||
|
constructor(message, code = "RR_ERROR", details = {}) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "RrApiError";
|
this.name = "RrApiError";
|
||||||
this.reqId = reqId || null;
|
this.code = code;
|
||||||
this.url = url || null;
|
this.details = details;
|
||||||
this.apiName = apiName || null;
|
}
|
||||||
this.errorData = errorData || null;
|
|
||||||
this.status = status || null;
|
toJSON() {
|
||||||
this.statusText = statusText || null;
|
return {
|
||||||
|
name: this.name,
|
||||||
|
code: this.code,
|
||||||
|
message: this.message,
|
||||||
|
details: this.details
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that a Reynolds & Reynolds response is successful.
|
* Helper to normalize thrown errors into a consistent RrApiError instance.
|
||||||
*
|
|
||||||
* Expected success structure (based on Rome RR specs):
|
|
||||||
* {
|
|
||||||
* "SuccessFlag": true,
|
|
||||||
* "ErrorCode": "0",
|
|
||||||
* "ErrorMessage": "",
|
|
||||||
* "Data": { ... }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Or if SOAP/XML-based:
|
|
||||||
* {
|
|
||||||
* "Envelope": {
|
|
||||||
* "Body": {
|
|
||||||
* "Response": {
|
|
||||||
* "SuccessFlag": true,
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* This helper unwraps and normalizes the response to detect any error cases.
|
|
||||||
*/
|
*/
|
||||||
function assertRrOk(data, { apiName = "RR API Call", allowEmpty = false } = {}) {
|
function toRrError(err, defaultCode = "RR_ERROR") {
|
||||||
if (!data && !allowEmpty) {
|
if (!err) return new RrApiError("Unknown RR error", defaultCode);
|
||||||
throw new RrApiError(`${apiName} returned no data`, { apiName });
|
if (err instanceof RrApiError) return err;
|
||||||
}
|
if (typeof err === "string") return new RrApiError(err, defaultCode);
|
||||||
|
const msg = err.message || "Unspecified RR error";
|
||||||
// Normalize envelope
|
const code = err.code || defaultCode;
|
||||||
const response =
|
const details = err.details || {};
|
||||||
data?.Envelope?.Body?.Response ||
|
return new RrApiError(msg, code, details);
|
||||||
data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]] ||
|
|
||||||
data?.Response ||
|
|
||||||
data;
|
|
||||||
|
|
||||||
// Handle array of errors or error objects
|
|
||||||
const errorBlock = response?.Errors || response?.Error || response?.Fault || null;
|
|
||||||
|
|
||||||
// Basic success conditions per RR documentation
|
|
||||||
const success =
|
|
||||||
response?.SuccessFlag === true ||
|
|
||||||
response?.ErrorCode === "0" ||
|
|
||||||
response?.ResultCode === "0" ||
|
|
||||||
(Array.isArray(errorBlock) && errorBlock.length === 0);
|
|
||||||
|
|
||||||
// If success, return normalized response
|
|
||||||
if (success || allowEmpty) {
|
|
||||||
return response?.Data || response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct contextual error info
|
|
||||||
const errorMessage = response?.ErrorMessage || response?.FaultString || response?.Message || "Unknown RR API error";
|
|
||||||
|
|
||||||
throw new RrApiError(`${apiName} failed: ${errorMessage}`, {
|
|
||||||
apiName,
|
|
||||||
errorData: response,
|
|
||||||
status: response?.ErrorCode || response?.ResultCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely unwrap nested RR API responses for consistency across handlers.
|
|
||||||
*/
|
|
||||||
function extractRrResponseData(data) {
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
data?.Envelope?.Body?.Response?.Data ||
|
|
||||||
data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]]?.Data ||
|
|
||||||
data?.Data ||
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
RrApiError,
|
RrApiError,
|
||||||
assertRrOk,
|
toRrError
|
||||||
extractRrResponseData
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,257 +1,319 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-helpers.js
|
* STAR-only SOAP transport + template rendering for Reynolds & Reynolds (Rome/RCI).
|
||||||
* @description Core helper functions for Reynolds & Reynolds integration.
|
* - Renders Mustache STAR business templates (rey_*Req rooted with STAR ns)
|
||||||
* Handles XML rendering, SOAP communication, and configuration merging.
|
* - Builds STAR SOAP envelope (ProcessMessage/payload/content + ApplicationArea)
|
||||||
|
* - Posts to RCI endpoint with STAR SOAPAction (full URI)
|
||||||
|
* - Parses XML response (faults + STAR payload result)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs/promises");
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const mustache = require("mustache");
|
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { v4: uuidv4 } = require("uuid");
|
const mustache = require("mustache");
|
||||||
const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants");
|
const { XMLParser } = require("fast-xml-parser");
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
const { client } = require("../graphql-client/graphql-client");
|
const { RR_ACTIONS, RR_SOAP_HEADERS, RR_STAR_SOAP_ACTION, RR_NS, getBaseRRConfig } = require("./rr-constants");
|
||||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
const { RrApiError } = require("./rr-error");
|
||||||
|
const xmlFormatter = require("xml-formatter");
|
||||||
/* ------------------------------------------------------------------------------------------------
|
|
||||||
* Configuration
|
|
||||||
* ----------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the rr_configuration JSON for a given bodyshop directly from the database.
|
* Remove XML decl, collapse inter-tag whitespace, strip empty lines,
|
||||||
* Dealer-level settings only. Platform/secret defaults come from getBaseRRConfig().
|
* then pretty-print. Safe for XML because we only touch whitespace
|
||||||
* @param {string} bodyshopId
|
* BETWEEN tags, not inside text nodes.
|
||||||
* @returns {Promise<object>} rr_configuration
|
/**
|
||||||
|
* Collapse Mustache-induced whitespace and pretty print.
|
||||||
|
* - strips XML decl (inner)
|
||||||
|
* - removes lines that are only whitespace
|
||||||
|
* - collapses inter-tag whitespace
|
||||||
|
* - formats with consistent indentation
|
||||||
*/
|
*/
|
||||||
async function getDealerConfig(bodyshopId) {
|
function prettyPrintXml(xml) {
|
||||||
try {
|
let s = xml;
|
||||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
|
||||||
const cfg = result?.bodyshops_by_pk?.rr_configuration || {};
|
// strip any inner XML declaration
|
||||||
return cfg;
|
s = s.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||||
} catch (err) {
|
|
||||||
console.error(`[RR] Failed to load rr_configuration for bodyshop ${bodyshopId}:`, err.message);
|
// remove lines that are only whitespace
|
||||||
return {};
|
s = s.replace(/^[\t ]*(?:\r?\n)/gm, "");
|
||||||
}
|
|
||||||
|
// collapse whitespace strictly between tags (not inside text nodes)
|
||||||
|
s = s.replace(/>\s+</g, "><");
|
||||||
|
|
||||||
|
// final pretty print
|
||||||
|
return xmlFormatter(s, {
|
||||||
|
indentation: " ",
|
||||||
|
collapseContent: true, // keep short elements on one line
|
||||||
|
lineSeparator: "\n",
|
||||||
|
strictMode: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---------- Public action map (compat with rr-test.js) ----------
|
||||||
* Helper to retrieve combined configuration (env + dealer) for calls.
|
const RRActions = Object.fromEntries(Object.entries(RR_ACTIONS).map(([k]) => [k, { action: k }]));
|
||||||
* NOTE: This does not hit Redis. DB only (dealer overrides) + env secrets.
|
|
||||||
* @param {object} socket - Either a real socket or an Express req carrying bodyshopId on .bodyshopId
|
// ---------- Template cache ----------
|
||||||
* @returns {Promise<object>} configuration
|
const templateCache = new Map();
|
||||||
*/
|
|
||||||
async function resolveRRConfig(socket) {
|
async function loadTemplate(templateName) {
|
||||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
if (templateCache.has(templateName)) return templateCache.get(templateName);
|
||||||
const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
const filePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||||
return { ...getBaseRRConfig(), ...dealerCfg };
|
const tpl = await fs.readFile(filePath, "utf8");
|
||||||
|
templateCache.set(templateName, tpl);
|
||||||
|
return tpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------------------------------
|
|
||||||
* Template rendering
|
|
||||||
* ----------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads and renders a Mustache XML template with provided data.
|
|
||||||
* @param {string} templateName - Name of XML file under server/rr/xml-templates/ (without .xml)
|
|
||||||
* @param {object} data - Template substitution object
|
|
||||||
* @returns {Promise<string>} Rendered XML string
|
|
||||||
*/
|
|
||||||
async function renderXmlTemplate(templateName, data) {
|
async function renderXmlTemplate(templateName, data) {
|
||||||
const templatePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
const tpl = await loadTemplate(templateName);
|
||||||
const xmlTemplate = await fs.readFile(templatePath, "utf8");
|
// Render and strip any XML declaration to keep a single root element for the BOD
|
||||||
return mustache.render(xmlTemplate, data);
|
const rendered = mustache.render(tpl, data || {});
|
||||||
|
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---------- Config resolution (STAR only) ----------
|
||||||
* Build a SOAP envelope with a rendered header + body.
|
async function resolveRRConfig(_socket, bodyshopConfig) {
|
||||||
* Header comes from xml-templates/_EnvelopeHeader.xml.
|
const envCfg = getBaseRRConfig();
|
||||||
* @param {string} renderedBodyXml
|
|
||||||
* @param {object} headerVars - values for header mustache
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async function buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) {
|
|
||||||
const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars);
|
|
||||||
|
|
||||||
return `
|
if (bodyshopConfig && typeof bodyshopConfig === "object") {
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return envCfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Response parsing ----------
|
||||||
|
function parseRRResponse(xml) {
|
||||||
|
const parser = new XMLParser({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
removeNSPrefix: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const doc = parser.parse(xml);
|
||||||
|
|
||||||
|
// Envelope/Body
|
||||||
|
const body =
|
||||||
|
doc?.Envelope?.Body ||
|
||||||
|
doc?.["soapenv:Envelope"]?.["soapenv:Body"] ||
|
||||||
|
doc?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
|
||||||
|
doc?.["S:Envelope"]?.["S:Body"] ||
|
||||||
|
doc?.Body ||
|
||||||
|
doc;
|
||||||
|
|
||||||
|
// SOAP Fault?
|
||||||
|
const fault = body?.Fault || body?.["soap:Fault"];
|
||||||
|
if (fault) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
code: fault.faultcode || "SOAP_FAULT",
|
||||||
|
message: fault.faultstring || "Unknown SOAP Fault",
|
||||||
|
raw: xml
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// STAR transport path: ProcessMessage/payload/content
|
||||||
|
const processMessage = body?.ProcessMessage || body?.["ns0:ProcessMessage"] || body?.["ProcessMessageResponse"];
|
||||||
|
|
||||||
|
if (processMessage?.payload?.content) {
|
||||||
|
const content = processMessage.payload.content;
|
||||||
|
if (content && typeof content === "object") {
|
||||||
|
const keys = Object.keys(content).filter((k) => k !== "@_id");
|
||||||
|
const respKey = keys.find((k) => /Resp$/.test(k)) || (keys[0] === "ApplicationArea" && keys[1]) || keys[0];
|
||||||
|
|
||||||
|
const respNode = respKey ? content[respKey] : content;
|
||||||
|
|
||||||
|
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || respNode?.StatusCode || "OK";
|
||||||
|
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || respNode?.StatusMessage || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: ["OK", "Success"].includes(String(resultCode)),
|
||||||
|
code: resultCode,
|
||||||
|
message: resultMessage,
|
||||||
|
raw: xml,
|
||||||
|
parsed: respNode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: first element under Body (just in case)
|
||||||
|
const keys = body && typeof body === "object" ? Object.keys(body) : [];
|
||||||
|
const respNode = keys.length ? body[keys[0]] : body;
|
||||||
|
|
||||||
|
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || "OK";
|
||||||
|
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: resultCode === "OK" || resultCode === "Success",
|
||||||
|
code: resultCode,
|
||||||
|
message: resultMessage,
|
||||||
|
raw: xml,
|
||||||
|
parsed: respNode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 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)
|
||||||
|
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||||
|
|
||||||
|
const appArea = `
|
||||||
|
<ApplicationArea>
|
||||||
|
<CreationDateTime>${CreationDateTime}</CreationDateTime>
|
||||||
|
<BODId>${BODId}</BODId>
|
||||||
|
<Sender>
|
||||||
|
${Sender?.Component ? `<Component>${Sender.Component}</Component>` : ""}
|
||||||
|
${Sender?.Task ? `<Task>${Sender.Task}</Task>` : ""}
|
||||||
|
${Sender?.ReferenceId ? `<ReferenceId>${Sender.ReferenceId}</ReferenceId>` : ""}
|
||||||
|
</Sender>
|
||||||
|
<Destination>
|
||||||
|
<DestinationNameCode>RR</DestinationNameCode>
|
||||||
|
${Destination?.DealerNumber ? `<DealerNumber>${Destination.DealerNumber}</DealerNumber>` : ""}
|
||||||
|
${Destination?.StoreNumber ? `<StoreNumber>${Destination.StoreNumber}</StoreNumber>` : ""}
|
||||||
|
${Destination?.AreaNumber ? `<AreaNumber>${Destination.AreaNumber}</AreaNumber>` : ""}
|
||||||
|
</Destination>
|
||||||
|
</ApplicationArea>`.trim();
|
||||||
|
|
||||||
|
// Inject right after the opening tag of the root element (skip processing instructions)
|
||||||
|
// e.g. <rey_RomeGetAdvisorsReq ...> ==> insert ApplicationArea here
|
||||||
|
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
|
||||||
|
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const payloadWithAppArea = wrapWithApplicationArea(innerBusinessXml, {
|
||||||
|
CreationDateTime: appArea.CreationDateTime || now,
|
||||||
|
BODId: appArea.BODId || `BOD-${Date.now()}`,
|
||||||
|
Sender: appArea.Sender || { Component: "Rome", Task: "SV", ReferenceId: "Update" },
|
||||||
|
Destination: appArea.Destination || {
|
||||||
|
DealerNumber: creds.dealerNumber,
|
||||||
|
StoreNumber: String(creds.storeNumber ?? "").padStart(2, "0"),
|
||||||
|
AreaNumber: String(creds.branchNumber || "01").padStart(2, "0")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<soapenv:Envelope xmlns:soapenc="${RR_NS.SOAP_ENC}" xmlns:soapenv="${RR_NS.SOAP_ENV}" xmlns:xsd="${RR_NS.XSD}" xmlns:xsi="${RR_NS.XSI}">
|
||||||
<soapenv:Header>
|
<soapenv:Header>
|
||||||
${headerXml}
|
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="${RR_NS.WSSE}">
|
||||||
|
<wsse:UsernameToken>
|
||||||
|
<wsse:Username>${creds.username}</wsse:Username>
|
||||||
|
<wsse:Password>${creds.password}</wsse:Password>
|
||||||
|
</wsse:UsernameToken>
|
||||||
|
</wsse:Security>
|
||||||
</soapenv:Header>
|
</soapenv:Header>
|
||||||
<soapenv:Body>
|
<soapenv:Body>
|
||||||
${renderedBodyXml}
|
<ProcessMessage xmlns="${RR_NS.STAR_TRANSPORT}">
|
||||||
|
<payload xmlns:soap="${RR_NS.SOAP_ENV}" xmlns:xsi="${RR_NS.XSI}" xmlns:xsd="${RR_NS.XSD}" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wsse="${RR_NS.WSSE}" xmlns:wsu="${RR_NS.WSU}" xmlns="${RR_NS.STAR_TRANSPORT}">
|
||||||
|
<content id="content0">
|
||||||
|
${payloadWithAppArea}
|
||||||
|
</content>
|
||||||
|
</payload>
|
||||||
|
</ProcessMessage>
|
||||||
</soapenv:Body>
|
</soapenv:Body>
|
||||||
</soapenv:Envelope>
|
</soapenv:Envelope>`;
|
||||||
`.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------------------------------
|
// ---------- Main transport (STAR only) ----------
|
||||||
* Core SOAP caller
|
|
||||||
* ----------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the full URL and SOAPAction for a given action spec.
|
|
||||||
* Allows either:
|
|
||||||
* - action: a key into RR_ACTIONS (e.g. "GetAdvisors")
|
|
||||||
* - action: a raw URL/spec
|
|
||||||
*/
|
|
||||||
function resolveActionTarget(action, baseUrl) {
|
|
||||||
if (typeof action === "string" && RR_ACTIONS[action]) {
|
|
||||||
const spec = RR_ACTIONS[action];
|
|
||||||
const soapAction = spec.soapAction || spec.action || action;
|
|
||||||
const cleanedBase = (spec.baseUrl || baseUrl || "").replace(/\/+$/, "");
|
|
||||||
const url = spec.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
|
|
||||||
return { url, soapAction };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action && typeof action === "object" && (action.url || action.soapAction || action.action)) {
|
|
||||||
const soapAction = action.soapAction || action.action || "";
|
|
||||||
const cleanedBase = (action.baseUrl || baseUrl || "").replace(/\/+$/, "");
|
|
||||||
const url = action.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
|
|
||||||
return { url, soapAction };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof action === "string") {
|
|
||||||
return { url: action, soapAction: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Invalid RR action. Must be a known RR_ACTIONS key, an action spec, or a URL string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs and sends a SOAP call to the Reynolds & Reynolds endpoint.
|
|
||||||
*
|
|
||||||
* Body can be one of:
|
|
||||||
* - string (already-rendered XML body)
|
|
||||||
* - { template: "TemplateName", data: {...} } to render server/rr/xml-templates/TemplateName.xml
|
|
||||||
*
|
|
||||||
* @param {object} params
|
|
||||||
* @param {string|object} params.action - RR action key (RR_ACTIONS) or a raw URL/spec
|
|
||||||
* @param {string|{template:string,data:object}} params.body - Rendered XML or template descriptor
|
|
||||||
* @param {object} params.socket - The socket or req object for context (used to resolve config & logging)
|
|
||||||
* @param {object} [params.redisHelpers]
|
|
||||||
* @param {string|number} [params.jobid]
|
|
||||||
* @param {object} [params.dealerConfig]
|
|
||||||
* @param {number} [params.retries=1]
|
|
||||||
* @returns {Promise<string>} Raw SOAP response text
|
|
||||||
*/
|
|
||||||
async function MakeRRCall({
|
async function MakeRRCall({
|
||||||
action,
|
action,
|
||||||
body,
|
body,
|
||||||
socket,
|
socket,
|
||||||
// redisHelpers,
|
dealerConfig, // optional per-shop overrides
|
||||||
jobid,
|
retries = 1,
|
||||||
dealerConfig,
|
jobid
|
||||||
retries = 1
|
|
||||||
}) {
|
}) {
|
||||||
const correlationId = uuidv4();
|
if (!action || !RR_ACTIONS[action]) {
|
||||||
|
throw new Error(`Invalid RR action: ${action}`);
|
||||||
const effectiveConfig = dealerConfig || (await resolveRRConfig(socket));
|
|
||||||
const { url, soapAction } = resolveActionTarget(action, effectiveConfig.baseUrl);
|
|
||||||
|
|
||||||
// Render body if given by template descriptor
|
|
||||||
let renderedBody = body;
|
|
||||||
if (body && typeof body === "object" && body.template) {
|
|
||||||
renderedBody = await renderXmlTemplate(body.template, body.data || {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build header vars (from env + rr_configuration)
|
const cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
|
||||||
const headerVars = {
|
const baseUrl = cfg.baseUrl;
|
||||||
PPSysId: effectiveConfig.ppsysid || process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID,
|
if (!baseUrl) throw new Error("Missing RR base URL");
|
||||||
DealerNumber: effectiveConfig.dealer_number || effectiveConfig.dealer_id || process.env.RR_DEALER_NUMBER,
|
|
||||||
StoreNumber: effectiveConfig.store_number || process.env.RR_STORE_NUMBER,
|
|
||||||
BranchNumber: effectiveConfig.branch_number || process.env.RR_BRANCH_NUMBER,
|
|
||||||
Username: effectiveConfig.username || process.env.RR_API_USER || process.env.RR_USERNAME,
|
|
||||||
Password: effectiveConfig.password || process.env.RR_API_PASS || process.env.RR_PASSWORD,
|
|
||||||
CorrelationId: correlationId
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build full SOAP envelope with proper header
|
// Render STAR business body
|
||||||
const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars);
|
const templateName = body?.template || action;
|
||||||
|
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
|
||||||
|
|
||||||
RRLogger(socket, "info", `RR → ${soapAction || "SOAP"} request`, {
|
// Build STAR envelope
|
||||||
|
let envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
|
||||||
|
|
||||||
|
const formattedEnvelope = prettyPrintXml(envelope);
|
||||||
|
|
||||||
|
// Guardrails
|
||||||
|
if (!formattedEnvelope.includes("<ProcessMessage") || !formattedEnvelope.includes("<ApplicationArea>")) {
|
||||||
|
throw new Error("STAR envelope malformed: missing ProcessMessage/ApplicationArea");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = { ...RR_SOAP_HEADERS, SOAPAction: RR_STAR_SOAP_ACTION };
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", `Sending RR SOAP request`, {
|
||||||
|
action,
|
||||||
|
soapAction: RR_STAR_SOAP_ACTION,
|
||||||
|
endpoint: baseUrl,
|
||||||
jobid,
|
jobid,
|
||||||
url,
|
mode: "STAR"
|
||||||
correlationId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = {
|
try {
|
||||||
...RR_SOAP_HEADERS,
|
const { data: responseXml } = await axios.post(baseUrl, formattedEnvelope, {
|
||||||
SOAPAction: soapAction,
|
headers,
|
||||||
"Content-Type": "text/xml; charset=utf-8",
|
timeout: cfg.timeout
|
||||||
"X-Request-Id": correlationId
|
// Some RCI tenants require Basic in addition to WSSE
|
||||||
};
|
// auth: { username: cfg.username, password: cfg.password }
|
||||||
|
});
|
||||||
|
|
||||||
let attempt = 0;
|
const parsed = parseRRResponse(responseXml);
|
||||||
while (attempt <= retries) {
|
|
||||||
attempt += 1;
|
if (!parsed.success) {
|
||||||
try {
|
RRLogger(socket, "error", `RR ${action} failed`, {
|
||||||
const response = await axios.post(url, soapEnvelope, {
|
code: parsed.code,
|
||||||
headers,
|
message: parsed.message
|
||||||
timeout: effectiveConfig.timeout || 30000,
|
|
||||||
responseType: "text",
|
|
||||||
validateStatus: () => true
|
|
||||||
});
|
});
|
||||||
|
throw new RrApiError(parsed.message || `RR ${action} failed`, parsed.code || "RR_ERROR");
|
||||||
const text = response.data;
|
|
||||||
|
|
||||||
if (response.status >= 400) {
|
|
||||||
RRLogger(socket, "error", `RR HTTP ${response.status} on ${soapAction || url}`, {
|
|
||||||
status: response.status,
|
|
||||||
jobid,
|
|
||||||
correlationId,
|
|
||||||
snippet: text?.slice?.(0, 512)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status >= 500 && attempt <= retries) {
|
|
||||||
RRLogger(socket, "warn", `RR transient HTTP error; retrying (${attempt}/${retries})`, {
|
|
||||||
correlationId
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`RR HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
RRLogger(socket, "debug", `RR ← ${soapAction || "SOAP"} response`, {
|
|
||||||
jobid,
|
|
||||||
correlationId,
|
|
||||||
bytes: Buffer.byteLength(text || "", "utf8")
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
} catch (err) {
|
|
||||||
const transient = /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up|network error/i.test(
|
|
||||||
err?.message || ""
|
|
||||||
);
|
|
||||||
if (transient && attempt <= retries) {
|
|
||||||
RRLogger(socket, "warn", `RR transient network error; retrying (${attempt}/${retries})`, {
|
|
||||||
error: err.message,
|
|
||||||
correlationId
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RRLogger(socket, "error", `RR ${soapAction || "SOAP"} failed`, {
|
|
||||||
error: err.message,
|
|
||||||
jobid,
|
|
||||||
correlationId
|
|
||||||
});
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RRLogger(socket, "info", `RR ${action} success`, {
|
||||||
|
result: parsed.code,
|
||||||
|
message: parsed.message
|
||||||
|
});
|
||||||
|
|
||||||
|
return responseXml;
|
||||||
|
} catch (err) {
|
||||||
|
if (retries > 0) {
|
||||||
|
RRLogger(socket, "warn", `Retrying RR ${action} (${retries - 1} left)`, {
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
return MakeRRCall({
|
||||||
|
action,
|
||||||
|
body,
|
||||||
|
socket,
|
||||||
|
dealerConfig: cfg,
|
||||||
|
retries: retries - 1,
|
||||||
|
jobid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RRLogger(socket, "error", `RR ${action} failed permanently`, { error: err.message });
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------------------------------
|
|
||||||
* Exports
|
|
||||||
* ----------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
const RRActions = RR_ACTIONS;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
MakeRRCall,
|
MakeRRCall,
|
||||||
getDealerConfig,
|
|
||||||
renderXmlTemplate,
|
renderXmlTemplate,
|
||||||
resolveRRConfig,
|
resolveRRConfig,
|
||||||
|
parseRRResponse,
|
||||||
|
buildStarEnvelope,
|
||||||
RRActions
|
RRActions
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,133 +1,158 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-job-export.js
|
* @file rr-job-export.js
|
||||||
* @description Orchestrates the full Reynolds & Reynolds DMS export flow.
|
* @description End-to-end export of a Hasura "job" to Reynolds & Reynolds (Rome).
|
||||||
* Creates/updates customers, vehicles, and repair orders according to Rome specs.
|
* Orchestrates Customer (insert/update), optional Vehicle insert, and RO (create/update),
|
||||||
|
* mirroring behavior of PBS/Fortellis exporters for parity.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { RrCustomerInsert, RrCustomerUpdate } = require("./rr-customer");
|
|
||||||
const { CreateRepairOrder, UpdateRepairOrder } = require("./rr-repair-orders");
|
|
||||||
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers");
|
|
||||||
const { assertRrOkXml, extractRrResponseData } = require("./rr-error");
|
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
const { mapServiceVehicleInsert } = require("./rr-mappers");
|
const { RrApiError } = require("./rr-error");
|
||||||
|
|
||||||
|
const customerApi = require("./rr-customer");
|
||||||
|
const roApi = require("./rr-repair-orders");
|
||||||
|
const { MakeRRCall } = require("./rr-helpers"); // for optional vehicle insert
|
||||||
|
const { mapServiceVehicle } = require("./rr-mappers");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a service vehicle record for the repair order.
|
* Decide if we should CREATE or UPDATE an entity in Rome based on external IDs
|
||||||
* Follows the "Rome Insert Service Vehicle Interface Specification" via SOAP/XML.
|
|
||||||
*/
|
*/
|
||||||
async function RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig }) {
|
function decideAction({ customer, vehicle, job }) {
|
||||||
try {
|
const hasCustId = !!(customer?.external_id || customer?.rr_customer_id);
|
||||||
RRLogger(socket, "info", "RR Insert Service Vehicle started", { jobid: JobData?.id });
|
const hasVehId = !!(vehicle?.external_id || vehicle?.rr_vehicle_id);
|
||||||
|
const hasRoId = !!(job?.external_id || job?.rr_repair_order_id || job?.dms_repair_order_id);
|
||||||
|
|
||||||
// Build Mustache variables for server/rr/xml-templates/InsertServiceVehicle.xml
|
return {
|
||||||
const variables = mapServiceVehicleInsert(JobData, dealerConfig);
|
customerAction: hasCustId ? "update" : "insert",
|
||||||
|
vehicleAction: hasVehId ? "skip" : "insert", // Rome often generates vehicle IDs on RO create; we insert only if we have enough data and no id
|
||||||
const xml = await MakeRRCall({
|
repairOrderAction: hasRoId ? "update" : "create"
|
||||||
action: RRActions.InsertServiceVehicle,
|
};
|
||||||
body: { template: "InsertServiceVehicle", data: variables },
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id,
|
|
||||||
dealerConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
const ok = assertRrOkXml(xml, { apiName: "RR Insert Service Vehicle" });
|
|
||||||
const normalized = extractRrResponseData(ok, { action: "InsertServiceVehicle" });
|
|
||||||
|
|
||||||
RRLogger(socket, "debug", "RR Insert Service Vehicle success", {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
vehicleId: normalized?.VehicleId || normalized?.vehicleId
|
|
||||||
});
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(socket, "error", `RR Insert Service Vehicle failed: ${error.message}`, { jobid: JobData?.id });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full DMS export sequence for Reynolds & Reynolds.
|
* Normalize a stage result to a consistent structure.
|
||||||
*
|
|
||||||
* 1. Ensure customer exists (insert or update)
|
|
||||||
* 2. Ensure vehicle exists/linked
|
|
||||||
* 3. Create or update repair order
|
|
||||||
*/
|
*/
|
||||||
async function ExportJobToRR({ socket, redisHelpers, JobData }) {
|
function stageOk(name, extra = {}) {
|
||||||
const jobid = JobData?.id;
|
return { stage: name, success: true, ...extra };
|
||||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
}
|
||||||
|
function stageFail(name, error) {
|
||||||
|
return { stage: name, success: false, error: error?.message || String(error) };
|
||||||
|
}
|
||||||
|
|
||||||
RRLogger(socket, "info", "Starting RR job export", { jobid, bodyshopId });
|
/**
|
||||||
|
* Export a job into Rome (Customer → Vehicle → RepairOrder).
|
||||||
|
* @param {Socket} socket - logging context (may be null in batch)
|
||||||
|
* @param {Object} job - Hasura job object (must include customer, vehicle, lines, totals)
|
||||||
|
* @param {Object} bodyshopConfig - per-shop RR config (dealer/store/branch + creds)
|
||||||
|
* @param {Object} options - { insertVehicleIfMissing: boolean }
|
||||||
|
* @returns {Promise<Object>} normalized result
|
||||||
|
*/
|
||||||
|
async function exportJobToRome(socket, job, bodyshopConfig, options = {}) {
|
||||||
|
const { customer = {}, vehicle = {} } = job || {};
|
||||||
|
const { insertVehicleIfMissing = true } = options;
|
||||||
|
|
||||||
|
const actions = decideAction({ customer, vehicle, job });
|
||||||
|
|
||||||
|
const stages = [];
|
||||||
|
const summary = {
|
||||||
|
dms: "Rome",
|
||||||
|
jobid: job?.id,
|
||||||
|
ro_action: actions.repairOrderAction,
|
||||||
|
customer_action: actions.customerAction,
|
||||||
|
vehicle_action: insertVehicleIfMissing ? actions.vehicleAction : "skip"
|
||||||
|
};
|
||||||
|
|
||||||
|
RRLogger(socket, "info", `RR Export start`, summary);
|
||||||
|
|
||||||
|
// ---- 1) Customer ----
|
||||||
try {
|
try {
|
||||||
// Pull dealer-level overrides once (DB), env/platform secrets come from rr-helpers internally.
|
if (actions.customerAction === "insert") {
|
||||||
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
const res = await customerApi.insertCustomer(socket, customer, bodyshopConfig);
|
||||||
|
stages.push(stageOk("customer.insert"));
|
||||||
//
|
summary.customer_xml = res.xml;
|
||||||
// STEP 1: CUSTOMER
|
|
||||||
//
|
|
||||||
RRLogger(socket, "info", "RR Step 1: Customer check/insert", { jobid });
|
|
||||||
let rrCustomerResult;
|
|
||||||
|
|
||||||
if (JobData?.rr_customer_id) {
|
|
||||||
rrCustomerResult = await RrCustomerUpdate({
|
|
||||||
socket,
|
|
||||||
redisHelpers,
|
|
||||||
JobData,
|
|
||||||
existingCustomer: { CustomerId: JobData.rr_customer_id },
|
|
||||||
patch: JobData.customer_patch
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
rrCustomerResult = await RrCustomerInsert({ socket, redisHelpers, JobData });
|
const res = await customerApi.updateCustomer(socket, customer, bodyshopConfig);
|
||||||
|
stages.push(stageOk("customer.update"));
|
||||||
|
summary.customer_xml = res.xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// STEP 2: VEHICLE
|
|
||||||
//
|
|
||||||
RRLogger(socket, "info", "RR Step 2: Vehicle insert", { jobid });
|
|
||||||
const rrVehicleResult = await RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig });
|
|
||||||
|
|
||||||
//
|
|
||||||
// STEP 3: REPAIR ORDER
|
|
||||||
//
|
|
||||||
RRLogger(socket, "info", "RR Step 3: Repair Order create/update", { jobid });
|
|
||||||
let rrRepairOrderResult;
|
|
||||||
|
|
||||||
if (JobData?.rr_ro_id) {
|
|
||||||
rrRepairOrderResult = await UpdateRepairOrder({ socket, redisHelpers, JobData });
|
|
||||||
} else {
|
|
||||||
rrRepairOrderResult = await CreateRepairOrder({ socket, redisHelpers, JobData });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// FINALIZE
|
|
||||||
//
|
|
||||||
RRLogger(socket, "info", "RR Export completed successfully", {
|
|
||||||
jobid,
|
|
||||||
rr_customer_id: rrCustomerResult?.CustomerId || rrCustomerResult?.customerId,
|
|
||||||
rr_vehicle_id: rrVehicleResult?.VehicleId || rrVehicleResult?.vehicleId,
|
|
||||||
rr_ro_id: rrRepairOrderResult?.RepairOrderId || rrRepairOrderResult?.repairOrderId
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
customer: rrCustomerResult,
|
|
||||||
vehicle: rrVehicleResult,
|
|
||||||
repairOrder: rrRepairOrderResult
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `RR job export failed: ${error.message}`, { jobid });
|
stages.push(stageFail(`customer.${actions.customerAction}`, error));
|
||||||
return {
|
RRLogger(socket, "error", `RR customer ${actions.customerAction} failed`, {
|
||||||
success: false,
|
jobid: job?.id,
|
||||||
error: error.message,
|
error: error.message
|
||||||
stack: error.stack
|
});
|
||||||
};
|
throw new RrApiError(`Customer ${actions.customerAction} failed: ${error.message}`, "RR_CUSTOMER_ERROR");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 2) Vehicle (optional explicit insert) ----
|
||||||
|
if (insertVehicleIfMissing && actions.vehicleAction === "insert") {
|
||||||
|
try {
|
||||||
|
// Only insert when we have at least VIN or plate+state/year
|
||||||
|
const hasMinimumIdentity = !!(vehicle?.vin || (vehicle?.license_plate && vehicle?.license_state));
|
||||||
|
if (hasMinimumIdentity) {
|
||||||
|
const data = mapServiceVehicle(vehicle, customer, bodyshopConfig);
|
||||||
|
const xml = await MakeRRCall({
|
||||||
|
action: "InsertServiceVehicle",
|
||||||
|
body: { template: "InsertServiceVehicle", data },
|
||||||
|
socket,
|
||||||
|
dealerConfig: bodyshopConfig,
|
||||||
|
jobid: job?.id
|
||||||
|
});
|
||||||
|
stages.push(stageOk("vehicle.insert"));
|
||||||
|
summary.vehicle_xml = xml;
|
||||||
|
} else {
|
||||||
|
stages.push(stageOk("vehicle.skip", { reason: "insufficient_identity" }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
stages.push(stageFail("vehicle.insert", error));
|
||||||
|
RRLogger(socket, "error", `RR vehicle insert failed`, {
|
||||||
|
jobid: job?.id,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
// Non-fatal for the overall export — many flows let RO creation create/associate vehicle.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stages.push(stageOk("vehicle.skip", { reason: actions.vehicleAction === "skip" ? "already_has_id" : "disabled" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 3) Repair Order ----
|
||||||
|
try {
|
||||||
|
let res;
|
||||||
|
if (actions.repairOrderAction === "create") {
|
||||||
|
res = await roApi.createRepairOrder(socket, job, bodyshopConfig);
|
||||||
|
stages.push(stageOk("ro.create"));
|
||||||
|
} else {
|
||||||
|
res = await roApi.updateRepairOrder(socket, job, bodyshopConfig);
|
||||||
|
stages.push(stageOk("ro.update"));
|
||||||
|
}
|
||||||
|
summary.ro_xml = res.xml;
|
||||||
|
} catch (error) {
|
||||||
|
stages.push(stageFail(`ro.${actions.repairOrderAction}`, error));
|
||||||
|
RRLogger(socket, "error", `RR RO ${actions.repairOrderAction} failed`, {
|
||||||
|
jobid: job?.id,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
throw new RrApiError(`RepairOrder ${actions.repairOrderAction} failed: ${error.message}`, "RR_RO_ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
...summary,
|
||||||
|
stages
|
||||||
|
};
|
||||||
|
|
||||||
|
RRLogger(socket, "info", `RR Export finished`, {
|
||||||
|
jobid: job?.id,
|
||||||
|
result: {
|
||||||
|
success: result.success,
|
||||||
|
customer_action: summary.customer_action,
|
||||||
|
vehicle_action: summary.vehicle_action,
|
||||||
|
ro_action: summary.ro_action
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ExportJobToRR,
|
exportJobToRome
|
||||||
RrServiceVehicleInsert
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,50 +1,55 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-logger.js
|
* @file rr-logger.js
|
||||||
* @description Centralized logger for Reynolds & Reynolds (RR) integrations.
|
* @description Structured logger for Reynolds & Reynolds (Rome) integration.
|
||||||
* Emits logs to CloudWatch via logger util, and back to client sockets for live visibility.
|
* Mirrors PBS/Fortellis log shape for consistent log parsing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const logger = require("../utils/logger");
|
const util = require("util");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a structured RR log event.
|
* @typedef {Object} LogContext
|
||||||
*
|
* @property {string} [jobid]
|
||||||
* @param {object} socket - The socket or Express request (both supported).
|
* @property {string} [action]
|
||||||
* @param {"debug"|"info"|"warn"|"error"} level - Log level.
|
* @property {string} [stage]
|
||||||
* @param {string} message - Human-readable log message.
|
* @property {string} [endpoint]
|
||||||
* @param {object} [txnDetails] - Optional additional details (payloads, responses, etc.)
|
* @property {Object} [meta]
|
||||||
*/
|
*/
|
||||||
const RRLogger = (socket, level = "info", message, txnDetails = {}) => {
|
|
||||||
try {
|
|
||||||
// Normalize level to uppercase for CloudWatch
|
|
||||||
const levelUpper = level.toUpperCase();
|
|
||||||
|
|
||||||
// Safe email and job correlation
|
/**
|
||||||
const userEmail =
|
* Emit a structured log event to console, Socket.IO, or upstream logger.
|
||||||
socket?.user?.email || socket?.request?.user?.email || socket?.handshake?.auth?.email || "unknown@user";
|
* @param {Socket|null} socket - Optional socket for WsLogger passthrough
|
||||||
|
* @param {"info"|"debug"|"warn"|"error"} level
|
||||||
|
* @param {string} message - Primary log message
|
||||||
|
* @param {LogContext|any} [context]
|
||||||
|
*/
|
||||||
|
function RRLogger(socket, level, message, context = {}) {
|
||||||
|
const logEvent = {
|
||||||
|
source: "RR",
|
||||||
|
level,
|
||||||
|
timestamp: dayjs().toISOString(),
|
||||||
|
message,
|
||||||
|
...context
|
||||||
|
};
|
||||||
|
|
||||||
const jobid = socket?.JobData?.id || txnDetails?.jobid || null;
|
// Console log (stdout/stderr)
|
||||||
|
const serialized = `[RR] ${logEvent.timestamp} [${level.toUpperCase()}] ${message}`;
|
||||||
// Main logging entry (to CloudWatch / file)
|
if (level === "error" || level === "warn") {
|
||||||
logger.log("rr-log-event", levelUpper, userEmail, jobid, {
|
console.error(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
|
||||||
wsmessage: message,
|
} else {
|
||||||
txnDetails
|
console.log(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
|
||||||
});
|
|
||||||
|
|
||||||
// Emit to live Socket.IO client if available
|
|
||||||
if (typeof socket.emit === "function") {
|
|
||||||
socket.emit("rr-log-event", {
|
|
||||||
level: levelUpper,
|
|
||||||
message,
|
|
||||||
txnDetails,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// As a fallback, log directly to console
|
|
||||||
console.error("RRLogger internal error:", err);
|
|
||||||
console.error("Original message:", message, txnDetails);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
// Optional: forward to WsLogger (if your socket is configured that way)
|
||||||
|
try {
|
||||||
|
if (socket && typeof socket.emit === "function") {
|
||||||
|
socket.emit("rr-log-event", logEvent);
|
||||||
|
} else if (global.WsLogger && typeof global.WsLogger.createLogEvent === "function") {
|
||||||
|
global.WsLogger.createLogEvent(socket, level.toUpperCase(), message, context.jobid, context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[RRLogger] forwarding error", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = RRLogger;
|
module.exports = RRLogger;
|
||||||
|
|||||||
@@ -1,143 +1,136 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-lookup.js
|
* @file rr-lookup.js
|
||||||
* @description Reynolds & Reynolds lookup operations
|
* @description Rome (Reynolds & Reynolds) lookup operations — Advisors, Parts, and CombinedSearch
|
||||||
* (Combined Search, Get Advisors, Get Parts) via SOAP/XML templates.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers");
|
const { MakeRRCall, parseRRResponse } = require("./rr-helpers");
|
||||||
const { assertRrOkXml, extractRrResponseData } = require("./rr-error");
|
const { mapAdvisorLookup, mapPartsLookup, mapCombinedSearch } = require("./rr-mappers");
|
||||||
const { mapCombinedSearchVars, mapGetAdvisorsVars, mapGetPartsVars } = require("./rr-mappers");
|
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
|
const { RrApiError } = require("./rr-error");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combined Search
|
* Get a list of service advisors from Rome.
|
||||||
* Maps to "Search Customer Service Vehicle Combined" spec (Rome)
|
|
||||||
*
|
|
||||||
* @param {object} options
|
|
||||||
* @param {object} options.socket - Socket or Express req (used for auth + bodyshopId)
|
|
||||||
* @param {object} options.redisHelpers - (unused, kept for parity)
|
|
||||||
* @param {string} options.jobid - Job reference for correlation
|
|
||||||
* @param {Array<[string, string]>} [options.params] - e.g. [["VIN","1HG..."],["LastName","DOE"]]
|
|
||||||
*/
|
*/
|
||||||
async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) {
|
async function getAdvisors(socket, criteria = {}, bodyshopConfig) {
|
||||||
|
const action = "GetAdvisors";
|
||||||
|
const template = "GetAdvisors";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RRLogger(socket, "info", "Starting RR Combined Search", { jobid, params });
|
RRLogger(socket, "info", `Starting RR ${action} lookup`);
|
||||||
|
const data = mapAdvisorLookup(criteria, bodyshopConfig);
|
||||||
|
|
||||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
const resultXml = await MakeRRCall({
|
||||||
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
action,
|
||||||
|
body: { template, data },
|
||||||
// Build Mustache variables for server/rr/xml-templates/CombinedSearch.xml
|
|
||||||
const variables = mapCombinedSearchVars({ params, dealerConfig });
|
|
||||||
|
|
||||||
const xml = await MakeRRCall({
|
|
||||||
action: RRActions.CombinedSearch,
|
|
||||||
body: { template: "CombinedSearch", data: variables },
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
socket,
|
||||||
jobid
|
dealerConfig: bodyshopConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate + normalize
|
const parsed = parseRRResponse(resultXml);
|
||||||
const ok = assertRrOkXml(xml, { apiName: "RR Combined Search", allowEmpty: true });
|
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
|
||||||
const normalized = extractRrResponseData(ok, { action: "CombinedSearch" });
|
|
||||||
|
|
||||||
RRLogger(socket, "debug", "RR Combined Search complete", {
|
const advisors = parsed.parsed?.Advisors?.Advisor || parsed.parsed?.AdvisorList?.Advisor || [];
|
||||||
jobid,
|
const advisorList = Array.isArray(advisors) ? advisors : [advisors];
|
||||||
count: Array.isArray(normalized) ? normalized.length : 0
|
|
||||||
});
|
RRLogger(socket, "debug", `${action} lookup returned ${advisorList.length} advisors`);
|
||||||
return normalized;
|
return { success: true, dms: "Rome", action, advisors: advisorList };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `RR Combined Search failed: ${error.message}`, { jobid });
|
RRLogger(socket, "error", `Error in ${action} lookup`, {
|
||||||
throw error;
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_ADVISORS_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Advisors
|
* Get parts information from Rome.
|
||||||
* Maps to "Get Advisors Specification" (Rome)
|
|
||||||
*
|
|
||||||
* @param {object} options
|
|
||||||
* @param {object} options.socket
|
|
||||||
* @param {object} options.redisHelpers
|
|
||||||
* @param {string} options.jobid
|
|
||||||
* @param {Array<[string, string]>} [options.params]
|
|
||||||
*/
|
*/
|
||||||
async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) {
|
async function getParts(socket, criteria = {}, bodyshopConfig) {
|
||||||
|
const action = "GetParts";
|
||||||
|
const template = "GetParts";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RRLogger(socket, "info", "Starting RR Get Advisors", { jobid, params });
|
RRLogger(socket, "info", `Starting RR ${action} lookup`);
|
||||||
|
const data = mapPartsLookup(criteria, bodyshopConfig);
|
||||||
|
|
||||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
const resultXml = await MakeRRCall({
|
||||||
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
action,
|
||||||
|
body: { template, data },
|
||||||
// Build Mustache variables for server/rr/xml-templates/GetAdvisors.xml
|
|
||||||
const variables = mapGetAdvisorsVars({ params, dealerConfig });
|
|
||||||
|
|
||||||
const xml = await MakeRRCall({
|
|
||||||
action: RRActions.GetAdvisors,
|
|
||||||
body: { template: "GetAdvisors", data: variables },
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
socket,
|
||||||
jobid
|
dealerConfig: bodyshopConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
const ok = assertRrOkXml(xml, { apiName: "RR Get Advisors", allowEmpty: true });
|
const parsed = parseRRResponse(resultXml);
|
||||||
const normalized = extractRrResponseData(ok, { action: "GetAdvisors" });
|
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
|
||||||
|
|
||||||
RRLogger(socket, "debug", "RR Get Advisors complete", {
|
const parts = parsed.parsed?.Parts?.Part || parsed.parsed?.PartList?.Part || [];
|
||||||
jobid,
|
const partList = Array.isArray(parts) ? parts : [parts];
|
||||||
count: Array.isArray(normalized) ? normalized.length : 0
|
|
||||||
});
|
RRLogger(socket, "debug", `${action} lookup returned ${partList.length} parts`);
|
||||||
return normalized;
|
return { success: true, dms: "Rome", action, parts: partList };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `RR Get Advisors failed: ${error.message}`, { jobid });
|
RRLogger(socket, "error", `Error in ${action} lookup`, {
|
||||||
throw error;
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_PARTS_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Parts
|
* Perform a combined customer / vehicle / company search.
|
||||||
* Maps to "Get Part Specification" (Rome)
|
* Equivalent to Rome CombinedSearchRq / Resp.
|
||||||
*
|
* @param {Socket} socket
|
||||||
* @param {object} options
|
* @param {Object} criteria - { VIN, LicensePlate, CustomerName, Phone, Email }
|
||||||
* @param {object} options.socket
|
* @param {Object} bodyshopConfig
|
||||||
* @param {object} options.redisHelpers
|
* @returns {Promise<Object>} { customers, vehicles, companies }
|
||||||
* @param {string} options.jobid
|
|
||||||
* @param {Array<[string, string]>} [options.params]
|
|
||||||
*/
|
*/
|
||||||
async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) {
|
async function combinedSearch(socket, criteria = {}, bodyshopConfig) {
|
||||||
|
const action = "CombinedSearch";
|
||||||
|
const template = "CombinedSearch";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RRLogger(socket, "info", "Starting RR Get Parts", { jobid, params });
|
RRLogger(socket, "info", `Starting RR ${action} request`);
|
||||||
|
const data = mapCombinedSearch(criteria, bodyshopConfig);
|
||||||
|
|
||||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
const resultXml = await MakeRRCall({
|
||||||
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
action,
|
||||||
|
body: { template, data },
|
||||||
// Build Mustache variables for server/rr/xml-templates/GetParts.xml
|
|
||||||
const variables = mapGetPartsVars({ params, dealerConfig });
|
|
||||||
|
|
||||||
const xml = await MakeRRCall({
|
|
||||||
action: RRActions.GetParts,
|
|
||||||
body: { template: "GetParts", data: variables },
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
socket,
|
||||||
jobid
|
dealerConfig: bodyshopConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
const ok = assertRrOkXml(xml, { apiName: "RR Get Parts", allowEmpty: true });
|
const parsed = parseRRResponse(resultXml);
|
||||||
const normalized = extractRrResponseData(ok, { action: "GetParts" });
|
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
|
||||||
|
|
||||||
RRLogger(socket, "debug", "RR Get Parts complete", {
|
const customers = parsed.parsed?.Customers?.Customer || [];
|
||||||
jobid,
|
const vehicles = parsed.parsed?.Vehicles?.ServiceVehicle || [];
|
||||||
count: Array.isArray(normalized) ? normalized.length : 0
|
const companies = parsed.parsed?.Companies?.Company || [];
|
||||||
});
|
|
||||||
return normalized;
|
const result = {
|
||||||
|
customers: Array.isArray(customers) ? customers : [customers],
|
||||||
|
vehicles: Array.isArray(vehicles) ? vehicles : [vehicles],
|
||||||
|
companies: Array.isArray(companies) ? companies : [companies]
|
||||||
|
};
|
||||||
|
|
||||||
|
RRLogger(
|
||||||
|
socket,
|
||||||
|
"debug",
|
||||||
|
`${action} returned ${result.customers.length} customers, ${result.vehicles.length} vehicles, ${result.companies.length} companies`
|
||||||
|
);
|
||||||
|
return { success: true, dms: "Rome", action, ...result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `RR Get Parts failed: ${error.message}`, { jobid });
|
RRLogger(socket, "error", `Error in ${action}`, {
|
||||||
throw error;
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw new RrApiError(`RR ${action} failed: ${error.message}`, "COMBINED_SEARCH_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
RrCombinedSearch,
|
getAdvisors,
|
||||||
RrGetAdvisors,
|
getParts,
|
||||||
RrGetParts
|
combinedSearch
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,424 +1,412 @@
|
|||||||
// server/rr/rr-mappers.js
|
/**
|
||||||
// -----------------------------------------------------------------------------
|
* @file rr-mappers.js
|
||||||
// Centralized mapping for Reynolds & Reynolds (RR) XML templates.
|
* @description Maps internal ImEX (Hasura) entities into Rome (Reynolds & Reynolds) XML structures.
|
||||||
// These functions take our domain objects (JobData, txEnvelope, current/patch)
|
* Each function returns a plain JS object that matches Mustache templates in xml-templates/.
|
||||||
// and produce the Mustache variable objects expected by the RR XML templates in
|
*/
|
||||||
// /server/rr/xml-templates.
|
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility: formats date/time to R&R’s preferred format (ISO or yyyy-MM-dd).
|
||||||
|
*/
|
||||||
|
const formatDate = (val) => {
|
||||||
|
if (!val) return undefined;
|
||||||
|
return dayjs(val).format("YYYY-MM-DD");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility: safely pick numeric values and stringify for XML.
|
||||||
|
*/
|
||||||
|
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 !== "");
|
||||||
|
|
||||||
//
|
//
|
||||||
// NOTE: This is still scaffolding. Where “TODO (spec)” appears, fill in the
|
// ===================== CUSTOMER =====================
|
||||||
// exact RR field semantics (type restrictions, enums, required/optional) based
|
|
||||||
// on the Rome RR PDFs you shared.
|
|
||||||
//
|
//
|
||||||
// Templates these map into (variable names must match):
|
|
||||||
// - InsertCustomer.xml: <rr:CustomerInsertRq/>
|
|
||||||
// - UpdateCustomer.xml: <rr:CustomerUpdateRq/>
|
|
||||||
// - InsertServiceVehicle.xml: <rr:ServiceVehicleAddRq/>
|
|
||||||
// - CreateRepairOrder.xml: <rr:RepairOrderInsertRq/>
|
|
||||||
// - UpdateRepairOrder.xml: <rr:RepairOrderChgRq/>
|
|
||||||
//
|
|
||||||
// All map* functions below return a plain object shaped for Mustache rendering.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const _ = require("lodash");
|
/**
|
||||||
|
* Map internal customer record to Rome CustomerInsertRq.
|
||||||
|
*/
|
||||||
|
function mapCustomerInsert(customer, bodyshopConfig) {
|
||||||
|
if (!customer) return {};
|
||||||
|
|
||||||
// Keep this consistent with other providers (sanitize strings for XML)
|
return {
|
||||||
const REPLACE_SPECIAL = /[^a-zA-Z0-9 .,\n#\-()/]+/g;
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
|
DealerNumber: bodyshopConfig?.dealer_number,
|
||||||
|
StoreNumber: bodyshopConfig?.store_number,
|
||||||
|
BranchNumber: bodyshopConfig?.branch_number,
|
||||||
|
RequestId: `CUST-INSERT-${customer.id}`,
|
||||||
|
Environment: process.env.NODE_ENV,
|
||||||
|
|
||||||
function sanitize(v) {
|
CustomerId: customer.external_id || undefined,
|
||||||
if (v === null || v === undefined) return null;
|
CustomerType: customer.type || "RETAIL",
|
||||||
return String(v).replace(REPLACE_SPECIAL, "").trim();
|
CompanyName: customer.company_name,
|
||||||
}
|
FirstName: customer.first_name,
|
||||||
|
MiddleName: customer.middle_name,
|
||||||
|
LastName: customer.last_name,
|
||||||
|
PreferredName: customer.display_name || customer.first_name,
|
||||||
|
ActiveFlag: customer.active ? "true" : "false",
|
||||||
|
|
||||||
function upper(v) {
|
CustomerGroup: customer.group_name,
|
||||||
const s = sanitize(v);
|
TaxExempt: customer.tax_exempt ? "true" : "false",
|
||||||
return s ? s.toUpperCase() : null;
|
DiscountLevel: num(customer.discount_level),
|
||||||
}
|
PreferredLanguage: customer.language || "EN",
|
||||||
|
|
||||||
function asNumberOrNull(v) {
|
Addresses: (customer.addresses || []).map((a) => ({
|
||||||
if (v === null || v === undefined || v === "") return null;
|
AddressType: a.type || "BILLING",
|
||||||
const n = Number(v);
|
AddressLine1: a.line1,
|
||||||
return Number.isFinite(n) ? n : null;
|
AddressLine2: a.line2,
|
||||||
}
|
City: a.city,
|
||||||
|
State: a.state,
|
||||||
|
PostalCode: a.postal_code,
|
||||||
|
Country: a.country || "US"
|
||||||
|
})),
|
||||||
|
|
||||||
function normalizePostal(raw) {
|
Phones: (customer.phones || []).map((p) => ({
|
||||||
if (!raw) return null;
|
PhoneNumber: p.number,
|
||||||
const s = String(raw).toUpperCase().replace(/\s+/g, "");
|
PhoneType: p.type || "MOBILE",
|
||||||
// If Canadian format (A1A1A1), keep as-is. Otherwise return raw sanitized.
|
Preferred: p.preferred ? "true" : "false"
|
||||||
return s.length === 6 ? `${s.slice(0, 3)} ${s.slice(3)}` : sanitize(raw);
|
})),
|
||||||
|
|
||||||
|
Emails: (customer.emails || []).map((e) => ({
|
||||||
|
EmailAddress: e.address,
|
||||||
|
EmailType: e.type || "WORK",
|
||||||
|
Preferred: e.preferred ? "true" : "false"
|
||||||
|
})),
|
||||||
|
|
||||||
|
Insurance: customer.insurance
|
||||||
|
? {
|
||||||
|
CompanyName: customer.insurance.company,
|
||||||
|
PolicyNumber: customer.insurance.policy,
|
||||||
|
ExpirationDate: formatDate(customer.insurance.expiration_date),
|
||||||
|
ContactName: customer.insurance.contact_name,
|
||||||
|
ContactPhone: customer.insurance.contact_phone
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
LinkedAccounts: (customer.linked_accounts || []).map((a) => ({
|
||||||
|
Type: a.type,
|
||||||
|
AccountNumber: a.account_number,
|
||||||
|
CreditLimit: num(a.credit_limit)
|
||||||
|
})),
|
||||||
|
|
||||||
|
Notes: customer.notes?.length ? { Items: customer.notes.map((n) => n.text || n) } : undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compose the dealer section used by every template.
|
* Map internal customer record to Rome CustomerUpdateRq.
|
||||||
* We prefer dealer-level rr_configuration first; fallback to env.
|
|
||||||
*/
|
*/
|
||||||
function buildDealerVars(dealerCfg = {}) {
|
function mapCustomerUpdate(customer, bodyshopConfig) {
|
||||||
|
if (!customer) return {};
|
||||||
return {
|
return {
|
||||||
DealerCode: dealerCfg.dealerCode || process.env.RR_DEALER_CODE || null,
|
...mapCustomerInsert(customer, bodyshopConfig),
|
||||||
DealerName: dealerCfg.dealerName || process.env.RR_DEALER_NAME || null,
|
RequestId: `CUST-UPDATE-${customer.id}`
|
||||||
DealerNumber: dealerCfg.dealerNumber || process.env.RR_DEALER_NUMBER || null,
|
|
||||||
StoreNumber: dealerCfg.storeNumber || process.env.RR_STORE_NUMBER || null,
|
|
||||||
BranchNumber: dealerCfg.branchNumber || process.env.RR_BRANCH_NUMBER || null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------- Phones/Emails ------------------------------- */
|
//
|
||||||
|
// ===================== VEHICLE =====================
|
||||||
|
//
|
||||||
|
|
||||||
function mapPhones({ ph1, ph2, mobile }) {
|
/**
|
||||||
// TODO (spec): adjust PhoneType enumerations if RR requires strict codes.
|
* Map vehicle to Rome ServiceVehicleAddRq.
|
||||||
const out = [];
|
*/
|
||||||
if (ph1) out.push({ PhoneNumber: sanitize(ph1), PhoneType: "HOME" });
|
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
||||||
if (ph2) out.push({ PhoneNumber: sanitize(ph2), PhoneType: "WORK" });
|
if (!vehicle) return {};
|
||||||
if (mobile) out.push({ PhoneNumber: sanitize(mobile), PhoneType: "MOBILE" });
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapEmails({ email }) {
|
|
||||||
if (!email) return [];
|
|
||||||
// TODO (spec): include EmailType (e.g., PERSONAL/WORK) if RR mandates it.
|
|
||||||
return [{ EmailAddress: sanitize(email), EmailType: "PERSONAL" }];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------- Addresses -------------------------------- */
|
|
||||||
|
|
||||||
function mapPostalAddressFromJob(job) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
AddressLine1: sanitize(job.ownr_addr1),
|
|
||||||
AddressLine2: sanitize(job.ownr_addr2),
|
|
||||||
City: upper(job.ownr_city),
|
|
||||||
State: upper(job.ownr_st || job.ownr_state),
|
|
||||||
PostalCode: normalizePostal(job.ownr_zip),
|
|
||||||
Country: upper(job.ownr_ctry) || "USA"
|
|
||||||
}
|
|
||||||
].filter((addr) => Object.values(addr).some(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------------------- Customer -------------------------------- */
|
|
||||||
|
|
||||||
function mapCustomerInsert(job, dealerCfg = {}) {
|
|
||||||
const dealer = buildDealerVars(dealerCfg);
|
|
||||||
const isCompany = Boolean(job?.ownr_co_nm && String(job.ownr_co_nm).trim() !== "");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dealer,
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
// Envelope metadata (optional)
|
DealerNumber: bodyshopConfig?.dealer_number,
|
||||||
RequestId: job?.id || null,
|
StoreNumber: bodyshopConfig?.store_number,
|
||||||
Environment: process.env.NODE_ENV || "development",
|
BranchNumber: bodyshopConfig?.branch_number,
|
||||||
|
RequestId: `VEH-${vehicle.id}`,
|
||||||
|
|
||||||
// Customer node (see InsertCustomer.xml)
|
CustomerId: ownerCustomer?.external_id,
|
||||||
CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
|
|
||||||
CompanyName: isCompany ? upper(job.ownr_co_nm) : null,
|
|
||||||
FirstName: !isCompany ? upper(job.ownr_fn) : null,
|
|
||||||
LastName: !isCompany ? upper(job.ownr_ln) : null,
|
|
||||||
ActiveFlag: "Y",
|
|
||||||
|
|
||||||
Addresses: mapPostalAddressFromJob(job),
|
VIN: vehicle.vin,
|
||||||
Phones: mapPhones({ ph1: job.ownr_ph1, ph2: job.ownr_ph2, mobile: job.ownr_mobile }),
|
UnitNumber: vehicle.unit_number,
|
||||||
Emails: mapEmails({ email: job.ownr_ea }),
|
StockNumber: vehicle.stock_number,
|
||||||
|
Year: num(vehicle.year),
|
||||||
|
Make: vehicle.make,
|
||||||
|
Model: vehicle.model,
|
||||||
|
Trim: vehicle.trim,
|
||||||
|
BodyStyle: vehicle.body_style,
|
||||||
|
Transmission: vehicle.transmission,
|
||||||
|
Engine: vehicle.engine,
|
||||||
|
FuelType: vehicle.fuel_type,
|
||||||
|
DriveType: vehicle.drive_type,
|
||||||
|
Color: vehicle.color,
|
||||||
|
LicensePlate: vehicle.license_plate,
|
||||||
|
LicenseState: vehicle.license_state,
|
||||||
|
Odometer: num(vehicle.odometer),
|
||||||
|
OdometerUnits: vehicle.odometer_units || "KM",
|
||||||
|
InServiceDate: formatDate(vehicle.in_service_date),
|
||||||
|
|
||||||
// Optional blocks (keep null unless you truly have values)
|
Insurance: vehicle.insurance
|
||||||
DriverLicense: null, // { LicenseNumber, LicenseState, ExpirationDate }
|
? {
|
||||||
Insurance: null, // { CompanyName, PolicyNumber, ExpirationDate }
|
CompanyName: vehicle.insurance.company,
|
||||||
Notes: null // { Note }
|
PolicyNumber: vehicle.insurance.policy,
|
||||||
|
ExpirationDate: formatDate(vehicle.insurance.expiration_date)
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
Warranty: vehicle.warranty
|
||||||
|
? {
|
||||||
|
WarrantyCompany: vehicle.warranty.company,
|
||||||
|
WarrantyNumber: vehicle.warranty.number,
|
||||||
|
WarrantyType: vehicle.warranty.type,
|
||||||
|
ExpirationDate: formatDate(vehicle.warranty.expiration_date)
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
VehicleNotes: vehicle.notes?.length ? { Items: vehicle.notes.map((n) => n.text || n) } : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCustomerUpdate(existingCustomer, patch = {}, dealerCfg = {}) {
|
//
|
||||||
const dealer = buildDealerVars(dealerCfg);
|
// ===================== REPAIR ORDER =====================
|
||||||
// We merge and normalize so callers can pass minimal deltas
|
//
|
||||||
const merged = _.merge({}, existingCustomer || {}, patch || {});
|
|
||||||
const id =
|
|
||||||
merged?.CustomerId ||
|
|
||||||
merged?.customerId ||
|
|
||||||
merged?.id ||
|
|
||||||
merged?.customer?.id ||
|
|
||||||
patch?.CustomerId ||
|
|
||||||
patch?.customerId ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
// Derive company vs individual
|
/**
|
||||||
const isCompany = Boolean(merged?.CompanyName || merged?.customerName?.companyName);
|
* Map internal job to Rome RepairOrderInsertRq.
|
||||||
|
*/
|
||||||
|
function mapRepairOrderCreate(job, bodyshopConfig) {
|
||||||
|
if (!job) return {};
|
||||||
|
|
||||||
const nameBlock = {
|
const cust = job.customer || {};
|
||||||
CompanyName: isCompany ? upper(merged?.CompanyName || merged?.customerName?.companyName) : null,
|
const veh = job.vehicle || {};
|
||||||
FirstName: !isCompany ? upper(merged?.FirstName || merged?.customerName?.firstName) : null,
|
|
||||||
LastName: !isCompany ? upper(merged?.LastName || merged?.customerName?.lastName) : null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Addresses
|
return {
|
||||||
const addr =
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
merged?.Addresses ||
|
DealerNumber: bodyshopConfig?.dealer_number,
|
||||||
merged?.postalAddress ||
|
StoreNumber: bodyshopConfig?.store_number,
|
||||||
(merged?.addressLine1 || merged?.addressLine2 || merged?.city
|
BranchNumber: bodyshopConfig?.branch_number,
|
||||||
? [
|
RequestId: `RO-${job.id}`,
|
||||||
{
|
Environment: process.env.NODE_ENV,
|
||||||
AddressLine1: sanitize(merged?.addressLine1),
|
|
||||||
AddressLine2: sanitize(merged?.addressLine2),
|
RepairOrderNumber: job.ro_number,
|
||||||
City: upper(merged?.city),
|
DmsRepairOrderId: job.external_id,
|
||||||
State: upper(merged?.state || merged?.province),
|
|
||||||
PostalCode: normalizePostal(merged?.postalCode),
|
OpenDate: formatDate(job.open_date),
|
||||||
Country: upper(merged?.country) || "USA"
|
PromisedDate: formatDate(job.promised_date),
|
||||||
|
CloseDate: formatDate(job.close_date),
|
||||||
|
|
||||||
|
ServiceAdvisorId: job.advisor_id,
|
||||||
|
TechnicianId: job.technician_id,
|
||||||
|
Department: job.department,
|
||||||
|
ProfitCenter: job.profit_center,
|
||||||
|
|
||||||
|
ROType: job.ro_type,
|
||||||
|
Status: job.status,
|
||||||
|
IsBodyShop: "true",
|
||||||
|
DRPFlag: job.drp_flag ? "true" : "false",
|
||||||
|
|
||||||
|
Customer: {
|
||||||
|
CustomerId: cust.external_id,
|
||||||
|
CustomerName: cust.full_name,
|
||||||
|
PhoneNumber: cust.phone,
|
||||||
|
EmailAddress: cust.email
|
||||||
|
},
|
||||||
|
|
||||||
|
Vehicle: {
|
||||||
|
VehicleId: veh.external_id,
|
||||||
|
VIN: veh.vin,
|
||||||
|
LicensePlate: veh.license_plate,
|
||||||
|
Year: num(veh.year),
|
||||||
|
Make: veh.make,
|
||||||
|
Model: veh.model,
|
||||||
|
Odometer: num(veh.odometer),
|
||||||
|
Color: veh.color
|
||||||
|
},
|
||||||
|
|
||||||
|
JobLines: (job.joblines || []).map((l, i) => ({
|
||||||
|
Sequence: i + 1,
|
||||||
|
ParentSequence: l.parent_sequence,
|
||||||
|
LineType: l.line_type,
|
||||||
|
Category: l.category,
|
||||||
|
OpCode: l.op_code,
|
||||||
|
Description: l.description,
|
||||||
|
LaborHours: num(l.labor_hours),
|
||||||
|
LaborRate: num(l.labor_rate),
|
||||||
|
PartNumber: l.part_number,
|
||||||
|
PartDescription: l.part_description,
|
||||||
|
Quantity: num(l.quantity),
|
||||||
|
UnitPrice: num(l.unit_price),
|
||||||
|
ExtendedPrice: num(l.extended_price),
|
||||||
|
DiscountAmount: num(l.discount_amount),
|
||||||
|
TaxCode: l.tax_code,
|
||||||
|
GLAccount: l.gl_account,
|
||||||
|
ControlNumber: l.control_number,
|
||||||
|
Taxes: l.taxes?.length
|
||||||
|
? {
|
||||||
|
Items: l.taxes.map((t) => ({
|
||||||
|
Code: t.code,
|
||||||
|
Amount: num(t.amount),
|
||||||
|
Rate: num(t.rate)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
]
|
: undefined
|
||||||
: null);
|
|
||||||
|
|
||||||
// Phones & Emails
|
|
||||||
const phones = merged?.Phones || merged?.contactMethods?.phones || [];
|
|
||||||
const emails = merged?.Emails || merged?.contactMethods?.emailAddresses || [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dealer,
|
|
||||||
RequestId: merged?.RequestId || null,
|
|
||||||
Environment: process.env.NODE_ENV || "development",
|
|
||||||
|
|
||||||
CustomerId: id,
|
|
||||||
CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
|
|
||||||
...nameBlock,
|
|
||||||
ActiveFlag: merged?.ActiveFlag || "Y",
|
|
||||||
|
|
||||||
Addresses: addr,
|
|
||||||
Phones: phones.map((p) => ({ PhoneNumber: sanitize(p.PhoneNumber || p.number), PhoneType: p.PhoneType || p.type })),
|
|
||||||
Emails: emails.map((e) => ({
|
|
||||||
EmailAddress: sanitize(e.EmailAddress || e.address),
|
|
||||||
EmailType: e.EmailType || e.type || "PERSONAL"
|
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// Optional
|
Totals: {
|
||||||
DriverLicense: merged?.DriverLicense || null,
|
Currency: job.currency || "CAD",
|
||||||
Insurance: merged?.Insurance || null,
|
LaborTotal: num(job.totals?.labor),
|
||||||
Notes: merged?.Notes || null
|
PartsTotal: num(job.totals?.parts),
|
||||||
};
|
MiscTotal: num(job.totals?.misc),
|
||||||
}
|
DiscountTotal: num(job.totals?.discount),
|
||||||
|
TaxTotal: num(job.totals?.tax),
|
||||||
|
GrandTotal: num(job.totals?.grand)
|
||||||
|
},
|
||||||
|
|
||||||
/* --------------------------------- Vehicle --------------------------------- */
|
Payments: job.payments?.length
|
||||||
|
|
||||||
function mapVehicleInsertFromJob(job, dealerCfg = {}, opts = {}) {
|
|
||||||
// opts: { customerId }
|
|
||||||
const dealer = buildDealerVars(dealerCfg);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dealer,
|
|
||||||
RequestId: job?.id || null,
|
|
||||||
Environment: process.env.NODE_ENV || "development",
|
|
||||||
|
|
||||||
CustomerId: opts?.customerId || null,
|
|
||||||
|
|
||||||
VIN: upper(job?.v_vin),
|
|
||||||
Year: asNumberOrNull(job?.v_model_yr),
|
|
||||||
Make: upper(job?.v_make),
|
|
||||||
Model: upper(job?.v_model),
|
|
||||||
Trim: upper(job?.v_trim),
|
|
||||||
BodyStyle: upper(job?.v_body),
|
|
||||||
Transmission: upper(job?.v_transmission),
|
|
||||||
Engine: upper(job?.v_engine),
|
|
||||||
FuelType: upper(job?.v_fuel),
|
|
||||||
Color: upper(job?.v_color),
|
|
||||||
Odometer: asNumberOrNull(job?.odometer_out || job?.kmout),
|
|
||||||
LicensePlate: upper(job?.plate_no),
|
|
||||||
LicenseState: upper(job?.plate_state),
|
|
||||||
|
|
||||||
Ownership: null,
|
|
||||||
Insurance: null,
|
|
||||||
VehicleNotes: null,
|
|
||||||
Warranty: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------- Repair Orders ------------------------------ */
|
|
||||||
|
|
||||||
function mapRepairOrderAddFromJob(job, txEnvelope = {}, dealerCfg = {}) {
|
|
||||||
const dealer = buildDealerVars(dealerCfg);
|
|
||||||
|
|
||||||
const customerVars = {
|
|
||||||
CustomerId: job?.customer?.id || txEnvelope?.customerId || null,
|
|
||||||
CustomerName:
|
|
||||||
upper(job?.ownr_co_nm) || [upper(job?.ownr_fn), upper(job?.ownr_ln)].filter(Boolean).join(" ").trim() || null,
|
|
||||||
PhoneNumber: sanitize(job?.ownr_ph1 || job?.ownr_mobile || job?.ownr_ph2),
|
|
||||||
EmailAddress: sanitize(job?.ownr_ea)
|
|
||||||
};
|
|
||||||
|
|
||||||
const vehicleVars = {
|
|
||||||
VIN: upper(job?.v_vin),
|
|
||||||
LicensePlate: upper(job?.plate_no),
|
|
||||||
Year: asNumberOrNull(job?.v_model_yr),
|
|
||||||
Make: upper(job?.v_make),
|
|
||||||
Model: upper(job?.v_model),
|
|
||||||
Odometer: asNumberOrNull(job?.odometer_out || job?.kmout),
|
|
||||||
Color: upper(job?.v_color)
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dealer,
|
|
||||||
RequestId: job?.id || null,
|
|
||||||
Environment: process.env.NODE_ENV || "development",
|
|
||||||
|
|
||||||
RepairOrderNumber: sanitize(job?.ro_number) || sanitize(txEnvelope?.reference) || null,
|
|
||||||
OpenDate: txEnvelope?.openedAt || job?.actual_in || null,
|
|
||||||
PromisedDate: txEnvelope?.promisedAt || job?.promise_date || null,
|
|
||||||
CloseDate: txEnvelope?.closedAt || job?.invoice_date || null,
|
|
||||||
ServiceAdvisorId: txEnvelope?.advisorId || job?.service_advisor_id || null,
|
|
||||||
TechnicianId: txEnvelope?.technicianId || job?.technician_id || null,
|
|
||||||
ROType: txEnvelope?.roType || "CUSTOMER_PAY", // TODO (spec): map from our job type(s)
|
|
||||||
Status: txEnvelope?.status || "OPEN",
|
|
||||||
|
|
||||||
CustomerId: customerVars.CustomerId,
|
|
||||||
CustomerName: customerVars.CustomerName,
|
|
||||||
PhoneNumber: customerVars.PhoneNumber,
|
|
||||||
EmailAddress: customerVars.EmailAddress,
|
|
||||||
|
|
||||||
VIN: vehicleVars.VIN,
|
|
||||||
LicensePlate: vehicleVars.LicensePlate,
|
|
||||||
Year: vehicleVars.Year,
|
|
||||||
Make: vehicleVars.Make,
|
|
||||||
Model: vehicleVars.Model,
|
|
||||||
Odometer: vehicleVars.Odometer,
|
|
||||||
Color: vehicleVars.Color,
|
|
||||||
|
|
||||||
JobLines: (job?.joblines || txEnvelope?.lines || []).map((ln, idx) => mapJobLineToRRLine(ln, idx + 1)),
|
|
||||||
|
|
||||||
Totals: txEnvelope?.totals
|
|
||||||
? {
|
? {
|
||||||
LaborTotal: asNumberOrNull(txEnvelope.totals.labor),
|
Items: job.payments.map((p) => ({
|
||||||
PartsTotal: asNumberOrNull(txEnvelope.totals.parts),
|
PayerType: p.payer_type,
|
||||||
MiscTotal: asNumberOrNull(txEnvelope.totals.misc),
|
PayerName: p.payer_name,
|
||||||
TaxTotal: asNumberOrNull(txEnvelope.totals.tax),
|
Amount: num(p.amount),
|
||||||
GrandTotal: asNumberOrNull(txEnvelope.totals.total)
|
Method: p.method,
|
||||||
|
Reference: p.reference,
|
||||||
|
ControlNumber: p.control_number
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
: null,
|
: undefined,
|
||||||
|
|
||||||
Insurance: txEnvelope?.insurance
|
Insurance: job.insurance
|
||||||
? {
|
? {
|
||||||
CompanyName: upper(txEnvelope.insurance.company),
|
CompanyName: job.insurance.company,
|
||||||
ClaimNumber: sanitize(txEnvelope.insurance.claim),
|
ClaimNumber: job.insurance.claim_number,
|
||||||
AdjusterName: upper(txEnvelope.insurance.adjuster),
|
AdjusterName: job.insurance.adjuster_name,
|
||||||
AdjusterPhone: sanitize(txEnvelope.insurance.phone)
|
AdjusterPhone: job.insurance.adjuster_phone
|
||||||
}
|
}
|
||||||
: null,
|
: undefined,
|
||||||
|
|
||||||
Notes: txEnvelope?.story ? { Note: sanitize(txEnvelope.story) } : null
|
Notes: job.notes?.length ? { Items: job.notes.map((n) => n.text || n) } : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRepairOrderChangeFromJob(current, delta = {}, dealerCfg = {}) {
|
/**
|
||||||
// current: existing RO (our cached shape)
|
* Map for repair order updates.
|
||||||
// delta: patch object describing header fields and line changes
|
*/
|
||||||
const dealer = buildDealerVars(dealerCfg);
|
function mapRepairOrderUpdate(job, bodyshopConfig) {
|
||||||
|
return {
|
||||||
|
...mapRepairOrderCreate(job, bodyshopConfig),
|
||||||
|
RequestId: `RO-UPDATE-${job.id}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const added = (delta.addedLines || []).map((ln, i) =>
|
//
|
||||||
mapJobLineToRRLine(ln, ln.Sequence || ln.seq || i + 1, { includePayType: true })
|
// ===================== LOOKUPS =====================
|
||||||
);
|
//
|
||||||
const updated = (delta.updatedLines || []).map((ln) => ({
|
|
||||||
...mapJobLineToRRLine(ln, ln.Sequence || ln.seq, { includePayType: true }),
|
|
||||||
ChangeType: ln.ChangeType || ln.change || null,
|
|
||||||
LineId: ln.LineId || null
|
|
||||||
}));
|
|
||||||
const removed = (delta.removedLines || []).map((ln) => ({
|
|
||||||
LineId: ln.LineId || null,
|
|
||||||
Sequence: ln.Sequence || ln.seq || null,
|
|
||||||
OpCode: upper(ln.OpCode || ln.opCode) || null,
|
|
||||||
Reason: sanitize(ln.Reason || ln.reason) || null
|
|
||||||
}));
|
|
||||||
|
|
||||||
const totals = delta?.totals
|
function mapAdvisorLookup(criteria, bodyshopConfig) {
|
||||||
? {
|
return {
|
||||||
LaborTotal: asNumberOrNull(delta.totals.labor),
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
PartsTotal: asNumberOrNull(delta.totals.parts),
|
DealerNumber: bodyshopConfig?.dealer_number,
|
||||||
MiscTotal: asNumberOrNull(delta.totals.misc),
|
StoreNumber: bodyshopConfig?.store_number,
|
||||||
TaxTotal: asNumberOrNull(delta.totals.tax),
|
BranchNumber: bodyshopConfig?.branch_number,
|
||||||
GrandTotal: asNumberOrNull(delta.totals.total)
|
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
|
||||||
}
|
SearchCriteria: {
|
||||||
: null;
|
Department: criteria.department || "Body Shop",
|
||||||
|
Status: criteria.status || "ACTIVE"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const insurance = delta?.insurance
|
function mapPartsLookup(criteria, bodyshopConfig) {
|
||||||
? {
|
return {
|
||||||
CompanyName: upper(delta.insurance.company),
|
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||||
ClaimNumber: sanitize(delta.insurance.claim),
|
DealerNumber: bodyshopConfig?.dealer_number,
|
||||||
AdjusterName: upper(delta.insurance.adjuster),
|
StoreNumber: bodyshopConfig?.store_number,
|
||||||
AdjusterPhone: sanitize(delta.insurance.phone)
|
BranchNumber: bodyshopConfig?.branch_number,
|
||||||
}
|
RequestId: `LOOKUP-PART-${Date.now()}`,
|
||||||
: null;
|
SearchCriteria: {
|
||||||
|
PartNumber: criteria.part_number,
|
||||||
|
Description: criteria.description,
|
||||||
|
Make: criteria.make,
|
||||||
|
Model: criteria.model,
|
||||||
|
Year: num(criteria.year),
|
||||||
|
Category: criteria.category,
|
||||||
|
MaxResults: criteria.max_results || 25
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const notes =
|
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
||||||
Array.isArray(delta?.notes) && delta.notes.length
|
// accept nested or flat input
|
||||||
? { Items: delta.notes.map((n) => sanitize(n)).filter(Boolean) }
|
const c = criteria || {};
|
||||||
: null;
|
const cust = c.customer || c.Customer || {};
|
||||||
|
const veh = c.vehicle || c.Vehicle || {};
|
||||||
|
const comp = c.company || c.Company || {};
|
||||||
|
|
||||||
|
// build optional blocks only if they have at least one value
|
||||||
|
const customerBlock = {
|
||||||
|
FirstName: cust.firstName || cust.FirstName || c.firstName,
|
||||||
|
LastName: cust.lastName || cust.LastName || c.lastName,
|
||||||
|
PhoneNumber: cust.phoneNumber || cust.PhoneNumber || c.phoneNumber || c.phone,
|
||||||
|
EmailAddress: cust.email || cust.EmailAddress || c.email,
|
||||||
|
CompanyName: cust.companyName || cust.CompanyName || c.companyName,
|
||||||
|
CustomerId: cust.customerId || cust.CustomerId || c.customerId
|
||||||
|
};
|
||||||
|
const vehicleBlock = {
|
||||||
|
VIN: veh.vin || veh.VIN || c.vin,
|
||||||
|
LicensePlate: veh.licensePlate || veh.LicensePlate || c.licensePlate,
|
||||||
|
Make: veh.make || veh.Make || c.make,
|
||||||
|
Model: veh.model || veh.Model || c.model,
|
||||||
|
Year: veh.year != null ? String(veh.year) : c.year != null ? String(c.year) : undefined,
|
||||||
|
VehicleId: veh.vehicleId || veh.VehicleId || c.vehicleId
|
||||||
|
};
|
||||||
|
const companyBlock = {
|
||||||
|
Name: comp.name || comp.Name || c.companyName,
|
||||||
|
Phone: comp.phone || comp.Phone || c.companyPhone
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dealer,
|
// Dealer / routing (aligns with your other mappers)
|
||||||
RequestId: delta?.RequestId || current?.RequestId || null,
|
STAR_NS: require("./rr-constants").RR_NS.STAR,
|
||||||
Environment: process.env.NODE_ENV || "development",
|
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,
|
||||||
|
|
||||||
RepairOrderId: current?.RepairOrderId || delta?.RepairOrderId || null,
|
RequestId: c.requestId || `COMBINED-${Date.now()}`,
|
||||||
RepairOrderNumber: delta?.RepairOrderNumber || current?.RepairOrderNumber || null,
|
Environment: process.env.NODE_ENV,
|
||||||
Status: delta?.Status || null,
|
|
||||||
ROType: delta?.ROType || null,
|
|
||||||
OpenDate: delta?.OpenDate || null,
|
|
||||||
PromisedDate: delta?.PromisedDate || null,
|
|
||||||
CloseDate: delta?.CloseDate || null,
|
|
||||||
ServiceAdvisorId: delta?.ServiceAdvisorId || null,
|
|
||||||
TechnicianId: delta?.TechnicianId || null,
|
|
||||||
LocationCode: delta?.LocationCode || null,
|
|
||||||
Department: delta?.Department || null,
|
|
||||||
PurchaseOrder: delta?.PurchaseOrder || null,
|
|
||||||
|
|
||||||
// Optional customer/vehicle patches
|
// Only include these blocks when they have content; Mustache {{#Block}} respects undefined
|
||||||
Customer: delta?.Customer || null,
|
Customer: hasAny(customerBlock) ? customerBlock : undefined,
|
||||||
Vehicle: delta?.Vehicle || null,
|
Vehicle: hasAny(vehicleBlock) ? vehicleBlock : undefined, // template wraps as <rr:ServiceVehicle>…</rr:ServiceVehicle>
|
||||||
|
Company: hasAny(companyBlock) ? companyBlock : undefined,
|
||||||
|
|
||||||
// Line changes
|
// Search behavior flags
|
||||||
AddedJobLines: added.length ? added : null,
|
SearchMode: c.searchMode || c.SearchMode, // EXACT | PARTIAL
|
||||||
UpdatedJobLines: updated.length ? updated : null,
|
ExactMatch: toBoolStr(c.exactMatch ?? c.ExactMatch),
|
||||||
RemovedJobLines: removed.length ? removed : null,
|
PartialMatch: toBoolStr(c.partialMatch ?? c.PartialMatch),
|
||||||
|
CaseInsensitive: toBoolStr(c.caseInsensitive ?? c.CaseInsensitive),
|
||||||
|
|
||||||
Totals: totals,
|
// Result shaping (default to true when unspecified)
|
||||||
Insurance: insurance,
|
ReturnCustomers: toBoolStr(c.returnCustomers ?? c.ReturnCustomers ?? true),
|
||||||
Notes: notes
|
ReturnVehicles: toBoolStr(c.returnVehicles ?? c.ReturnVehicles ?? true),
|
||||||
|
ReturnCompanies: toBoolStr(c.returnCompanies ?? c.ReturnCompanies ?? true),
|
||||||
|
|
||||||
|
// Paging / sorting
|
||||||
|
MaxResults: c.maxResults ?? c.MaxResults,
|
||||||
|
PageNumber: c.pageNumber ?? c.PageNumber,
|
||||||
|
SortBy: c.sortBy ?? c.SortBy, // e.g., NAME, VIN, PARTNUMBER
|
||||||
|
SortDirection: c.sortDirection ?? c.SortDirection // ASC | DESC
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------- Line Mapping ------------------------------- */
|
|
||||||
|
|
||||||
function mapJobLineToRRLine(line, sequenceFallback, opts = {}) {
|
|
||||||
// opts.includePayType => include PayType when present (CUST|INS|WARR|INT)
|
|
||||||
const qty = asNumberOrNull(line?.Quantity || line?.qty || line?.part_qty || 1);
|
|
||||||
const unit = asNumberOrNull(line?.UnitPrice || line?.price || line?.unitPrice);
|
|
||||||
const ext = asNumberOrNull(line?.ExtendedPrice || (qty && unit ? qty * unit : line?.extended));
|
|
||||||
|
|
||||||
return {
|
|
||||||
Sequence: asNumberOrNull(line?.Sequence || line?.seq) || asNumberOrNull(sequenceFallback),
|
|
||||||
OpCode: upper(line?.OpCode || line?.opCode || line?.opcode),
|
|
||||||
Description: sanitize(line?.Description || line?.description || line?.desc || line?.story),
|
|
||||||
LaborHours: asNumberOrNull(line?.LaborHours || line?.laborHours),
|
|
||||||
LaborRate: asNumberOrNull(line?.LaborRate || line?.laborRate),
|
|
||||||
PartNumber: upper(line?.PartNumber || line?.partNumber || line?.part_no),
|
|
||||||
PartDescription: sanitize(line?.PartDescription || line?.partDescription || line?.part_desc),
|
|
||||||
Quantity: qty,
|
|
||||||
UnitPrice: unit,
|
|
||||||
ExtendedPrice: ext,
|
|
||||||
TaxCode: upper(line?.TaxCode || line?.taxCode) || null,
|
|
||||||
PayType: opts.includePayType ? upper(line?.PayType || line?.payType) || null : undefined,
|
|
||||||
Reason: sanitize(line?.Reason || line?.reason) || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Customer
|
|
||||||
mapCustomerInsert,
|
mapCustomerInsert,
|
||||||
mapCustomerUpdate,
|
mapCustomerUpdate,
|
||||||
|
mapServiceVehicle,
|
||||||
// Vehicle
|
mapRepairOrderCreate,
|
||||||
mapVehicleInsertFromJob,
|
mapRepairOrderUpdate,
|
||||||
|
mapAdvisorLookup,
|
||||||
// Repair orders
|
mapPartsLookup,
|
||||||
mapRepairOrderAddFromJob,
|
mapCombinedSearch
|
||||||
mapRepairOrderChangeFromJob,
|
|
||||||
mapJobLineToRRLine,
|
|
||||||
|
|
||||||
// shared utils (handy in tests)
|
|
||||||
buildDealerVars,
|
|
||||||
_sanitize: sanitize,
|
|
||||||
_upper: upper,
|
|
||||||
_normalizePostal: normalizePostal
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,144 +1,99 @@
|
|||||||
/**
|
/**
|
||||||
* @file rr-repair-orders.js
|
* @file rr-repair-orders.js
|
||||||
* @description Reynolds & Reynolds (Rome) Repair Order Create & Update.
|
* @description Rome (Reynolds & Reynolds) Repair Order Integration.
|
||||||
* Implements the "Create Body Shop Management Repair Order" and
|
* Handles creation and updates of repair orders (BSMRepairOrderRq/Resp).
|
||||||
* "Update Body Shop Management Repair Order" specifications.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
const { MakeRRCall } = require("./rr-helpers");
|
||||||
const { assertRrOk } = require("./rr-error");
|
|
||||||
const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers");
|
const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers");
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
const { client } = require("../graphql-client/graphql-client");
|
const { RrApiError } = require("./rr-error");
|
||||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch rr_configuration for the current bodyshop directly from DB.
|
* Create a new repair order in Rome.
|
||||||
* Dealer-specific configuration is mandatory for RR operations.
|
* @param {Socket} socket - active socket connection
|
||||||
|
* @param {Object} job - Hasura job object (including vehicle, customer, joblines)
|
||||||
|
* @param {Object} bodyshopConfig - DMS config for current bodyshop
|
||||||
|
* @returns {Promise<Object>} normalized result
|
||||||
*/
|
*/
|
||||||
async function getDealerConfigFromDB(bodyshopId, logger) {
|
async function createRepairOrder(socket, job, bodyshopConfig) {
|
||||||
|
const action = "CreateRepairOrder";
|
||||||
|
const template = "CreateRepairOrder"; // maps to xml-templates/CreateRepairOrder.xml
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`);
|
||||||
const config = result?.bodyshops_by_pk?.rr_configuration || null;
|
|
||||||
|
|
||||||
if (!config) {
|
const data = mapRepairOrderCreate(job, bodyshopConfig);
|
||||||
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config);
|
const resultXml = await MakeRRCall({
|
||||||
return config;
|
action,
|
||||||
|
body: { template, data },
|
||||||
|
socket,
|
||||||
|
dealerConfig: bodyshopConfig,
|
||||||
|
jobid: job.id
|
||||||
|
});
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", `${action} completed successfully`, { jobid: job.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
dms: "Rome",
|
||||||
|
jobid: job.id,
|
||||||
|
action,
|
||||||
|
xml: resultXml
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, {
|
RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, {
|
||||||
bodyshopId,
|
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
throw error;
|
throw new RrApiError(`RR CreateRepairOrder failed: ${error.message}`, "CREATE_RO_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CREATE REPAIR ORDER
|
* Update an existing repair order in Rome.
|
||||||
* Based on "Rome Create Body Shop Management Repair Order Specification"
|
* @param {Socket} socket
|
||||||
*
|
* @param {Object} job
|
||||||
* @param {object} options
|
* @param {Object} bodyshopConfig
|
||||||
* @param {object} options.socket - socket or express request
|
* @returns {Promise<Object>}
|
||||||
* @param {object} options.redisHelpers
|
|
||||||
* @param {object} options.JobData - internal job object
|
|
||||||
* @param {object} [options.txEnvelope] - transaction metadata (advisor, timestamps, etc.)
|
|
||||||
*/
|
*/
|
||||||
async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
|
async function updateRepairOrder(socket, job, bodyshopConfig) {
|
||||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
const action = "UpdateRepairOrder";
|
||||||
const logger = socket?.logger || console;
|
const template = "UpdateRepairOrder";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RRLogger(socket, "info", "RR Create Repair Order started", {
|
RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`);
|
||||||
jobid: JobData?.id,
|
|
||||||
bodyshopId
|
|
||||||
});
|
|
||||||
|
|
||||||
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
const data = mapRepairOrderUpdate(job, bodyshopConfig);
|
||||||
|
|
||||||
// Build Mustache variables for server/rr/xml-templates/CreateRepairOrder.xml
|
const resultXml = await MakeRRCall({
|
||||||
const vars = mapRepairOrderCreate({ JobData, txEnvelope, dealerConfig });
|
action,
|
||||||
|
body: { template, data },
|
||||||
const data = await MakeRRCall({
|
|
||||||
action: RRActions.CreateRepairOrder, // resolves SOAPAction+URL
|
|
||||||
body: { template: "CreateRepairOrder", data: vars }, // render XML template
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
socket,
|
||||||
jobid: JobData.id
|
dealerConfig: bodyshopConfig,
|
||||||
|
jobid: job.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = assertRrOk(data, { apiName: "RR Create Repair Order" });
|
RRLogger(socket, "debug", `${action} completed successfully`, { jobid: job.id });
|
||||||
|
|
||||||
RRLogger(socket, "debug", "RR Create Repair Order success", {
|
return {
|
||||||
jobid: JobData?.id,
|
success: true,
|
||||||
dealer: dealerConfig?.dealer_code || dealerConfig?.dealerCode
|
dms: "Rome",
|
||||||
});
|
jobid: job.id,
|
||||||
|
action,
|
||||||
return response;
|
xml: resultXml
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `RR Create Repair Order failed: ${error.message}`, {
|
RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, {
|
||||||
jobid: JobData?.id
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
throw error;
|
throw new RrApiError(`RR UpdateRepairOrder failed: ${error.message}`, "UPDATE_RO_ERROR");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UPDATE REPAIR ORDER
|
|
||||||
* Based on "Rome Update Body Shop Management Repair Order Specification"
|
|
||||||
*
|
|
||||||
* @param {object} options
|
|
||||||
* @param {object} options.socket
|
|
||||||
* @param {object} options.redisHelpers
|
|
||||||
* @param {object} options.JobData
|
|
||||||
* @param {object} [options.txEnvelope]
|
|
||||||
*/
|
|
||||||
async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
|
|
||||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
|
||||||
const logger = socket?.logger || console;
|
|
||||||
|
|
||||||
try {
|
|
||||||
RRLogger(socket, "info", "RR Update Repair Order started", {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
bodyshopId,
|
|
||||||
rr_ro_id: JobData?.rr_ro_id
|
|
||||||
});
|
|
||||||
|
|
||||||
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
|
||||||
|
|
||||||
// Build Mustache variables for server/rr/xml-templates/UpdateRepairOrder.xml
|
|
||||||
const vars = mapRepairOrderUpdate({ JobData, txEnvelope, dealerConfig });
|
|
||||||
|
|
||||||
const data = await MakeRRCall({
|
|
||||||
action: RRActions.UpdateRepairOrder, // resolves SOAPAction+URL
|
|
||||||
body: { template: "UpdateRepairOrder", data: vars }, // render XML template
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = assertRrOk(data, { apiName: "RR Update Repair Order" });
|
|
||||||
|
|
||||||
RRLogger(socket, "debug", "RR Update Repair Order success", {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
rr_ro_id: JobData?.rr_ro_id
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(socket, "error", `RR Update Repair Order failed: ${error.message}`, {
|
|
||||||
jobid: JobData?.id,
|
|
||||||
rr_ro_id: JobData?.rr_ro_id
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CreateRepairOrder,
|
createRepairOrder,
|
||||||
UpdateRepairOrder,
|
updateRepairOrder
|
||||||
getDealerConfigFromDB
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,127 +1,191 @@
|
|||||||
// node server/rr/rr-test.js
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file rr-test.js
|
* RR smoke test / CLI (STAR-only)
|
||||||
* @description Diagnostic test script for Reynolds & Reynolds (R&R) integration.
|
|
||||||
* Run with: NODE_ENV=development node server/rr/rr-test.js
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
require("dotenv").config({
|
const fs = require("fs");
|
||||||
path: path.resolve(__dirname, "../../", `.env.${process.env.NODE_ENV || "development"}`)
|
const dotenv = require("dotenv");
|
||||||
});
|
const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers");
|
||||||
|
|
||||||
const fs = require("fs/promises");
|
|
||||||
const mustache = require("mustache");
|
|
||||||
const { getBaseRRConfig } = require("./rr-constants");
|
const { getBaseRRConfig } = require("./rr-constants");
|
||||||
const { RRActions, MakeRRCall } = require("./rr-helpers");
|
|
||||||
const RRLogger = require("./rr-logger");
|
|
||||||
|
|
||||||
// --- Mock socket + redis helpers for standalone test
|
// Load env file for local runs
|
||||||
const socket = {
|
const defaultEnvPath = path.resolve(__dirname, "../../.env.development");
|
||||||
bodyshopId: process.env.TEST_BODYSHOP_ID || null,
|
if (fs.existsSync(defaultEnvPath)) {
|
||||||
user: { email: "test@romeonline.io" },
|
const result = dotenv.config({ path: defaultEnvPath });
|
||||||
emit: (event, data) => console.log(`[SOCKET EVENT] ${event}`, data),
|
if (result?.parsed) {
|
||||||
logger: console
|
console.log(
|
||||||
};
|
`${defaultEnvPath}\n[dotenv@${require("dotenv/package.json").version}] injecting env (${Object.keys(result.parsed).length}) from ../../.env.development`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const redisHelpers = {
|
// Parse CLI args
|
||||||
setSessionData: async () => {},
|
const argv = process.argv.slice(2);
|
||||||
getSessionData: async () => {},
|
const args = { _: [] };
|
||||||
setSessionTransactionData: async () => {},
|
for (let i = 0; i < argv.length; i++) {
|
||||||
getSessionTransactionData: async () => {},
|
const a = argv[i];
|
||||||
clearSessionTransactionData: async () => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
if (a.startsWith("--")) {
|
||||||
try {
|
const eq = a.indexOf("=");
|
||||||
console.log("=== Reynolds & Reynolds Integration Test ===");
|
if (eq > -1) {
|
||||||
console.log("NODE_ENV:", process.env.NODE_ENV);
|
const k = a.slice(2, eq);
|
||||||
|
const v = a.slice(eq + 1);
|
||||||
const baseCfg = getBaseRRConfig();
|
args[k] = v;
|
||||||
console.log("Base R&R Config (from env):", {
|
} else {
|
||||||
baseUrl: baseCfg.baseUrl,
|
const k = a.slice(2);
|
||||||
hasUser: !!baseCfg.username || !!process.env.RR_API_USER || !!process.env.RR_USERNAME,
|
const next = argv[i + 1];
|
||||||
hasPass: !!baseCfg.password || !!process.env.RR_API_PASS || !!process.env.RR_PASSWORD,
|
if (next && !next.startsWith("-")) {
|
||||||
timeout: baseCfg.timeout
|
args[k] = next;
|
||||||
});
|
i++; // consume value
|
||||||
|
} else {
|
||||||
// ---- test variables for GetAdvisors
|
args[k] = true; // boolean flag
|
||||||
const templateVars = {
|
|
||||||
DealerCode: process.env.RR_DEALER_NAME || "ROME",
|
|
||||||
DealerName: "Rome Collision Test",
|
|
||||||
SearchCriteria: {
|
|
||||||
Department: "Body Shop",
|
|
||||||
Status: "ACTIVE"
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
} 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("-")) {
|
||||||
|
args[k] = next;
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
args[k] = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args._.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dealer/Store/Branch/PPSysId can come from rr_configuration or env; for test we override:
|
function toIntOr(defaultVal, maybe) {
|
||||||
const dealerConfigOverride = {
|
const n = parseInt(maybe, 10);
|
||||||
// baseUrl can also be overridden here if you want
|
return Number.isFinite(n) ? n : defaultVal;
|
||||||
ppsysid: process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID || "TEST-PPSYSID",
|
}
|
||||||
dealer_number: process.env.RR_DEALER_NUMBER || "12345",
|
|
||||||
store_number: process.env.RR_STORE_NUMBER || "01",
|
|
||||||
branch_number: process.env.RR_BRANCH_NUMBER || "001",
|
|
||||||
// creds (optional here; MakeRRCall will fallback to env if omitted)
|
|
||||||
username: process.env.RR_API_USER || process.env.RR_USERNAME || "Rome",
|
|
||||||
password: process.env.RR_API_PASS || process.env.RR_PASSWORD || "secret"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show the first ~600 chars of the envelope we will send (by rendering the template + header)
|
// ✅ fixed guard clause
|
||||||
// NOTE: This is just for printing; MakeRRCall will rebuild with proper header internally.
|
function pickActionName(raw) {
|
||||||
const templatePath = path.join(__dirname, "xml-templates", "GetAdvisors.xml");
|
if (!raw || typeof raw !== "string") return "ping";
|
||||||
const tpl = await fs.readFile(templatePath, "utf8");
|
const x = raw.toLowerCase();
|
||||||
const renderedBody = mustache.render(tpl, templateVars);
|
if (x === "combined" || x === "combinedsearch" || x === "comb") return "combined";
|
||||||
|
if (x === "advisors" || x === "advisor" || x === "getadvisors") return "advisors";
|
||||||
|
if (x === "parts" || x === "getparts" || x === "part") return "parts";
|
||||||
|
if (x === "ping") return "ping";
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
// Build a preview envelope using the same helper used by MakeRRCall
|
function buildBodyForAction(action, args, cfg) {
|
||||||
const { renderXmlTemplate } = require("./rr-helpers");
|
switch (action) {
|
||||||
const headerPreview = await renderXmlTemplate("_EnvelopeHeader", {
|
case "ping":
|
||||||
PPSysId: dealerConfigOverride.ppsysid,
|
case "advisors": {
|
||||||
DealerNumber: dealerConfigOverride.dealer_number,
|
const max = toIntOr(1, args.max);
|
||||||
StoreNumber: dealerConfigOverride.store_number,
|
const data = {
|
||||||
BranchNumber: dealerConfigOverride.branch_number,
|
DealerCode: cfg.dealerNumber,
|
||||||
Username: dealerConfigOverride.username,
|
DealerNumber: cfg.dealerNumber,
|
||||||
Password: dealerConfigOverride.password,
|
StoreNumber: cfg.storeNumber,
|
||||||
CorrelationId: "preview-correlation"
|
BranchNumber: cfg.branchNumber,
|
||||||
});
|
SearchCriteria: {
|
||||||
const previewEnvelope = `
|
AdvisorId: args.advisorId,
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
FirstName: args.first || args.firstname,
|
||||||
<soapenv:Header>
|
LastName: args.last || args.lastname,
|
||||||
${headerPreview}
|
Department: args.department,
|
||||||
</soapenv:Header>
|
Status: args.status || "ACTIVE",
|
||||||
<soapenv:Body>
|
IncludeInactive: args.includeInactive ? "true" : undefined,
|
||||||
${renderedBody}
|
MaxResults: max
|
||||||
</soapenv:Body>
|
}
|
||||||
</soapenv:Envelope>`.trim();
|
};
|
||||||
|
return { template: "GetAdvisors", data, appArea: {} };
|
||||||
console.log("\n--- Rendered SOAP Envelope (first 600 chars) ---\n");
|
|
||||||
console.log(previewEnvelope.slice(0, 600));
|
|
||||||
console.log("... [truncated]\n");
|
|
||||||
|
|
||||||
// If we don't have a base URL, skip the live call
|
|
||||||
if (!baseCfg.baseUrl) {
|
|
||||||
console.warn("\n⚠️ No RR baseUrl defined. Skipping live call.\n");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`--- Sending SOAP Request: ${RRActions.GetAdvisors.action} ---\n`);
|
case "combined": {
|
||||||
|
const max = toIntOr(10, args.max);
|
||||||
|
const data = {
|
||||||
|
DealerNumber: cfg.dealerNumber,
|
||||||
|
StoreNumber: cfg.storeNumber,
|
||||||
|
BranchNumber: cfg.branchNumber,
|
||||||
|
Customer: {
|
||||||
|
FirstName: args.first,
|
||||||
|
LastName: args.last,
|
||||||
|
PhoneNumber: args.phone,
|
||||||
|
EmailAddress: args.email
|
||||||
|
},
|
||||||
|
Vehicle: {
|
||||||
|
VIN: args.vin,
|
||||||
|
LicensePlate: args.plate
|
||||||
|
},
|
||||||
|
MaxResults: max
|
||||||
|
};
|
||||||
|
return { template: "CombinedSearch", data, appArea: {} };
|
||||||
|
}
|
||||||
|
|
||||||
const responseXml = await MakeRRCall({
|
case "parts": {
|
||||||
action: "GetAdvisors",
|
const max = toIntOr(5, args.max);
|
||||||
baseUrl: process.env.RR_API_BASE_URL,
|
const data = {
|
||||||
body: { template: "GetAdvisors", data: templateVars },
|
DealerNumber: cfg.dealerNumber,
|
||||||
dealerConfig: dealerConfigOverride,
|
StoreNumber: cfg.storeNumber,
|
||||||
redisHelpers,
|
BranchNumber: cfg.branchNumber,
|
||||||
socket,
|
SearchCriteria: {
|
||||||
jobid: "test-job",
|
PartNumber: args.part,
|
||||||
retries: 1
|
Description: args.desc,
|
||||||
});
|
Make: args.make,
|
||||||
|
Model: args.model,
|
||||||
|
Year: args.year,
|
||||||
|
MaxResults: max
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { template: "GetParts", data, appArea: {} };
|
||||||
|
}
|
||||||
|
|
||||||
RRLogger(socket, "info", "RR test successful", { bytes: Buffer.byteLength(responseXml, "utf8") });
|
default:
|
||||||
console.log("\n✅ Test completed successfully.\n");
|
throw new Error(`Unsupported action: ${action}`);
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Test failed:", error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const action = pickActionName(args.action || args.a || args._[0]);
|
||||||
|
const rrAction =
|
||||||
|
action === "ping"
|
||||||
|
? "GetAdvisors"
|
||||||
|
: action === "advisors"
|
||||||
|
? "GetAdvisors"
|
||||||
|
: action === "combined"
|
||||||
|
? "CombinedSearch"
|
||||||
|
: action === "parts"
|
||||||
|
? "GetParts"
|
||||||
|
: action;
|
||||||
|
|
||||||
|
const cfg = getBaseRRConfig();
|
||||||
|
const body = buildBodyForAction(action, args, cfg);
|
||||||
|
const templateName = body.template || rrAction;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xml = await renderXmlTemplate(templateName, body.data);
|
||||||
|
console.log("✅ Templates verified.");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ Template verification failed:", e.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.dry) {
|
||||||
|
const business = await renderXmlTemplate(templateName, body.data);
|
||||||
|
const envelope = await buildStarEnvelope(business, cfg, body.appArea);
|
||||||
|
console.log("\n--- FULL SOAP ENVELOPE ---\n");
|
||||||
|
console.log(envelope);
|
||||||
|
console.log("\n(dry run) 🚫 Skipping network call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`\n▶ Calling Rome action: ${rrAction}`);
|
||||||
|
const xml = await MakeRRCall({ action: rrAction, body, dealerConfig: cfg });
|
||||||
|
console.log("\n✅ RR call succeeded.\n");
|
||||||
|
console.log(xml);
|
||||||
|
} catch (err) {
|
||||||
|
console.dir(err);
|
||||||
|
console.error("[RR] rr-test failed", { message: err.message, stack: err.stack });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,97 +1,123 @@
|
|||||||
/**
|
/**
|
||||||
* RR WSDL / SOAP XML Transport Layer (thin wrapper)
|
* @file rr-wsdl.js
|
||||||
* -------------------------------------------------
|
* @description Lightweight service description + utilities for the Rome (R&R) SOAP actions.
|
||||||
* Delegates to rr-helpers.MakeRRCall (which handles:
|
* - Maps actions to SOAPAction headers (from rr-constants)
|
||||||
* - fetching dealer config from DB via resolveRRConfig
|
* - Maps actions to Mustache template filenames (xml-templates/*.xml)
|
||||||
* - rendering Mustache XML templates
|
* - Provides verification helpers to ensure templates exist
|
||||||
* - building SOAP envelope + headers
|
* - Provides normalized SOAP headers used by the transport
|
||||||
* - axios POST + retries
|
|
||||||
*
|
|
||||||
* Use this when you prefer the "action + variables" style and (optionally)
|
|
||||||
* want a parsed Body node back instead of raw XML.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { XMLParser } = require("fast-xml-parser");
|
const path = require("path");
|
||||||
const logger = require("../utils/logger");
|
const fs = require("fs/promises");
|
||||||
const { MakeRRCall, resolveRRConfig, renderXmlTemplate } = require("./rr-helpers");
|
const { RR_ACTIONS, RR_SOAP_HEADERS } = require("./rr-constants");
|
||||||
|
|
||||||
// Map friendly action names to template filenames (no envelope here; helpers add it)
|
// ---- Action <-> Template wiring ----
|
||||||
const RR_ACTION_MAP = {
|
// Keep action names consistent with rr-helpers / rr-lookup / rr-repair-orders / rr-customer
|
||||||
CustomerInsert: { file: "InsertCustomer.xml" },
|
const ACTION_TEMPLATES = Object.freeze({
|
||||||
CustomerUpdate: { file: "UpdateCustomer.xml" },
|
InsertCustomer: "InsertCustomer",
|
||||||
ServiceVehicleInsert: { file: "InsertServiceVehicle.xml" },
|
UpdateCustomer: "UpdateCustomer",
|
||||||
CombinedSearch: { file: "CombinedSearch.xml" },
|
InsertServiceVehicle: "InsertServiceVehicle",
|
||||||
GetParts: { file: "GetParts.xml" },
|
CreateRepairOrder: "CreateRepairOrder",
|
||||||
GetAdvisors: { file: "GetAdvisors.xml" },
|
UpdateRepairOrder: "UpdateRepairOrder",
|
||||||
CreateRepairOrder: { file: "CreateRepairOrder.xml" },
|
GetAdvisors: "GetAdvisors",
|
||||||
UpdateRepairOrder: { file: "UpdateRepairOrder.xml" }
|
GetParts: "GetParts",
|
||||||
};
|
CombinedSearch: "CombinedSearch"
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optionally render just the body XML for a given action (no SOAP envelope).
|
* Get the SOAPAction string for a known action.
|
||||||
* Mostly useful for diagnostics/tests.
|
* Throws if action is unknown.
|
||||||
*/
|
*/
|
||||||
async function buildRRXml(action, variables = {}) {
|
function getSoapAction(action) {
|
||||||
const entry = RR_ACTION_MAP[action];
|
const entry = RR_ACTIONS[action];
|
||||||
if (!entry) throw new Error(`Unknown RR action: ${action}`);
|
if (!entry) {
|
||||||
const templateName = entry.file.replace(/\.xml$/i, "");
|
const known = Object.keys(RR_ACTIONS).join(", ");
|
||||||
return renderXmlTemplate(templateName, variables);
|
throw new Error(`Unknown RR action "${action}". Known: ${known}`);
|
||||||
|
}
|
||||||
|
return entry.soapAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an RR SOAP request using helpers (action + variables).
|
* Get the template filename (without extension) for a known action.
|
||||||
* @param {object} opts
|
* e.g., "CreateRepairOrder" -> "CreateRepairOrder"
|
||||||
* @param {string} opts.action One of RR_ACTION_MAP keys (and RR_ACTIONS in rr-constants)
|
|
||||||
* @param {object} opts.variables Mustache variables for the body template
|
|
||||||
* @param {object} opts.socket Socket/req for context (bodyshopId + auth)
|
|
||||||
* @param {boolean} [opts.raw=false] If true, returns raw XML string
|
|
||||||
* @param {number} [opts.retries=1] Transient retry attempts (5xx/network)
|
|
||||||
* @returns {Promise<string|object>} Raw XML (raw=true) or parsed Body node
|
|
||||||
*/
|
*/
|
||||||
async function sendRRRequest({ action, variables = {}, socket, raw = false, retries = 1 }) {
|
function getTemplateForAction(action) {
|
||||||
const entry = RR_ACTION_MAP[action];
|
const tpl = ACTION_TEMPLATES[action];
|
||||||
if (!entry) throw new Error(`Unknown RR action: ${action}`);
|
if (!tpl) {
|
||||||
|
const known = Object.keys(ACTION_TEMPLATES).join(", ");
|
||||||
|
throw new Error(`No template mapping for RR action "${action}". Known: ${known}`);
|
||||||
|
}
|
||||||
|
return tpl;
|
||||||
|
}
|
||||||
|
|
||||||
const templateName = entry.file.replace(/\.xml$/i, "");
|
/**
|
||||||
const dealerConfig = await resolveRRConfig(socket);
|
* Build headers for a SOAP request, including SOAPAction.
|
||||||
|
* Consumers: rr-helpers (transport).
|
||||||
|
*/
|
||||||
|
function buildSoapHeadersForAction(action) {
|
||||||
|
return {
|
||||||
|
...RR_SOAP_HEADERS,
|
||||||
|
SOAPAction: getSoapAction(action)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Let MakeRRCall render + envelope + post
|
/**
|
||||||
const xml = await MakeRRCall({
|
* List all known actions with their SOAPAction + template.
|
||||||
|
* Useful for diagnostics (e.g., /rr/actions route).
|
||||||
|
*/
|
||||||
|
function listActions() {
|
||||||
|
return Object.keys(ACTION_TEMPLATES).map((action) => ({
|
||||||
action,
|
action,
|
||||||
body: { template: templateName, data: variables },
|
soapAction: getSoapAction(action),
|
||||||
socket,
|
template: getTemplateForAction(action)
|
||||||
dealerConfig,
|
}));
|
||||||
retries
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (raw) return xml;
|
/**
|
||||||
|
* Verify that every required template exists in xml-templates/.
|
||||||
|
* Returns an array of issues; empty array means all good.
|
||||||
|
*/
|
||||||
|
async function verifyTemplatesExist() {
|
||||||
|
const issues = [];
|
||||||
|
const baseDir = path.join(__dirname, "xml-templates");
|
||||||
|
|
||||||
try {
|
for (const [action, tpl] of Object.entries(ACTION_TEMPLATES)) {
|
||||||
const parser = new XMLParser({ ignoreAttributes: false });
|
const filePath = path.join(baseDir, `${tpl}.xml`);
|
||||||
const parsed = parser.parse(xml);
|
try {
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (!stat.isFile()) {
|
||||||
|
issues.push({ action, template: tpl, error: "Not a regular file" });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
issues.push({ action, template: tpl, error: `Missing file: ${filePath}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
// Try several common namespace variants for Envelope/Body
|
/**
|
||||||
const bodyNode =
|
* Quick assert that throws if any template is missing.
|
||||||
parsed?.Envelope?.Body ||
|
* You can call this once during boot and log the result.
|
||||||
parsed?.["soapenv:Envelope"]?.["soapenv:Body"] ||
|
*/
|
||||||
parsed?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
|
async function assertTemplates() {
|
||||||
parsed?.["S:Envelope"]?.["S:Body"] ||
|
const issues = await verifyTemplatesExist();
|
||||||
parsed;
|
if (issues.length) {
|
||||||
|
const msg =
|
||||||
return bodyNode;
|
"RR xml-templates verification failed:\n" +
|
||||||
} catch (err) {
|
issues.map((i) => ` - ${i.action} -> ${i.template}.xml :: ${i.error}`).join("\n");
|
||||||
logger.log("rr-wsdl-parse-error", "ERROR", "RR", null, {
|
throw new Error(msg);
|
||||||
action,
|
|
||||||
message: err.message,
|
|
||||||
stack: err.stack
|
|
||||||
});
|
|
||||||
// If parsing fails, return raw so caller can inspect
|
|
||||||
return xml;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendRRRequest,
|
// Maps / helpers
|
||||||
buildRRXml,
|
ACTION_TEMPLATES,
|
||||||
RR_ACTION_MAP
|
listActions,
|
||||||
|
getSoapAction,
|
||||||
|
getTemplateForAction,
|
||||||
|
buildSoapHeadersForAction,
|
||||||
|
|
||||||
|
// Verification
|
||||||
|
verifyTemplatesExist,
|
||||||
|
assertTemplates
|
||||||
};
|
};
|
||||||
|
|||||||
193
server/rr/rrRoutes.js
Normal file
193
server/rr/rrRoutes.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* @file rrRoutes.js
|
||||||
|
* @description Express routes for Reynolds & Reynolds (Rome) integration.
|
||||||
|
* Endpoints:
|
||||||
|
* - POST /rr/customer/insert
|
||||||
|
* - POST /rr/customer/update
|
||||||
|
* - POST /rr/repair-order/create
|
||||||
|
* - POST /rr/repair-order/update
|
||||||
|
* - POST /rr/lookup/advisors
|
||||||
|
* - POST /rr/lookup/parts
|
||||||
|
* - POST /rr/lookup/combined-search
|
||||||
|
* - POST /rr/export/job
|
||||||
|
* - GET /rr/actions
|
||||||
|
* - GET /rr/templates/verify
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
const { RrApiError } = require("./rr-error");
|
||||||
|
|
||||||
|
// Domain modules
|
||||||
|
const customerApi = require("./rr-customer"); // insertCustomer, updateCustomer
|
||||||
|
const roApi = require("./rr-repair-orders"); // createRepairOrder, updateRepairOrder
|
||||||
|
const lookupApi = require("./rr-lookup"); // getAdvisors, getParts, combinedSearch
|
||||||
|
const { exportJobToRome } = require("./rr-job-export"); // orchestrator
|
||||||
|
|
||||||
|
// Diagnostics
|
||||||
|
const { listActions, verifyTemplatesExist } = require("./rr-wsdl");
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- 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 result = await customerApi.insertCustomer(socket, customer, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /customer/insert failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 result = await customerApi.updateCustomer(socket, customer, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /customer/update failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Repair Orders --------------------
|
||||||
|
|
||||||
|
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 result = await roApi.createRepairOrder(socket, job, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /repair-order/create failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 result = await roApi.updateRepairOrder(socket, job, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /repair-order/update failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Lookups --------------------
|
||||||
|
|
||||||
|
router.post("/rr/lookup/advisors", async (req, res) => {
|
||||||
|
const socket = socketOf(req);
|
||||||
|
const { criteria = {} } = req.body || {};
|
||||||
|
const cfg = pickConfig(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await lookupApi.getAdvisors(socket, criteria, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /lookup/advisors failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/rr/lookup/parts", async (req, res) => {
|
||||||
|
const socket = socketOf(req);
|
||||||
|
const { criteria = {} } = req.body || {};
|
||||||
|
const cfg = pickConfig(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await lookupApi.getParts(socket, criteria, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /lookup/parts failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/rr/lookup/combined-search", async (req, res) => {
|
||||||
|
const socket = socketOf(req);
|
||||||
|
const { criteria = {} } = req.body || {};
|
||||||
|
const cfg = pickConfig(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await lookupApi.combinedSearch(socket, criteria, cfg);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /lookup/combined-search failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Orchestrated export --------------------
|
||||||
|
|
||||||
|
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 result = await exportJobToRome(socket, job, cfg, options);
|
||||||
|
return ok(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
RRLogger(socket, "error", "RR /export/job failed", { err: err.message });
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Diagnostics --------------------
|
||||||
|
|
||||||
|
router.get("/rr/actions", (_req, res) => {
|
||||||
|
try {
|
||||||
|
return ok(res, { actions: listActions() });
|
||||||
|
} catch (err) {
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/rr/templates/verify", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const issues = await verifyTemplatesExist();
|
||||||
|
return ok(res, { ok: issues.length === 0, issues });
|
||||||
|
} catch (err) {
|
||||||
|
return fail(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,73 +1,15 @@
|
|||||||
<rr:CombinedSearchRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeCustServVehCombReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<!-- NOTE: ApplicationArea is injected by buildStarEnvelope(); do not include it here. -->
|
||||||
{{#RequestId}}
|
<CustServVehCombReq>
|
||||||
<rr:RequestId>{{RequestId}}</rr:RequestId>
|
<QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}>
|
||||||
{{/RequestId}}
|
{{#Customer.PhoneNumber}}<Phone Num="{{Customer.PhoneNumber}}"/>{{/Customer.PhoneNumber}}
|
||||||
{{#Environment}}
|
|
||||||
<rr:Environment>{{Environment}}</rr:Environment>
|
|
||||||
{{/Environment}}
|
|
||||||
|
|
||||||
<rr:Dealer>
|
{{#Customer.FirstName}}<FirstName>{{Customer.FirstName}}</FirstName>{{/Customer.FirstName}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
{{#Customer.LastName}}<LastName>{{Customer.LastName}}</LastName>{{/Customer.LastName}}
|
||||||
{{#DealerName}}
|
{{#Customer.EmailAddress}}<EMail>{{Customer.EmailAddress}}</EMail>{{/Customer.EmailAddress}}
|
||||||
<rr:DealerName>{{DealerName}}</rr:DealerName>
|
|
||||||
{{/DealerName}}
|
|
||||||
{{#DealerNumber}}
|
|
||||||
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
|
|
||||||
{{/DealerNumber}}
|
|
||||||
{{#StoreNumber}}
|
|
||||||
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
|
|
||||||
{{/StoreNumber}}
|
|
||||||
{{#BranchNumber}}
|
|
||||||
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
|
|
||||||
{{/BranchNumber}}
|
|
||||||
</rr:Dealer>
|
|
||||||
|
|
||||||
<rr:SearchCriteria>
|
{{#Vehicle.VIN}}<VIN>{{Vehicle.VIN}}</VIN>{{/Vehicle.VIN}}
|
||||||
{{#Customer}}
|
{{#Vehicle.LicensePlate}}<LicensePlate>{{Vehicle.LicensePlate}}</LicensePlate>{{/Vehicle.LicensePlate}}
|
||||||
<rr:Customer>
|
</QueryData>
|
||||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
</CustServVehCombReq>
|
||||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
</rey_RomeCustServVehCombReq>
|
||||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
|
||||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
|
||||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
|
||||||
</rr:Customer>
|
|
||||||
{{/Customer}}
|
|
||||||
|
|
||||||
{{#Vehicle}}
|
|
||||||
<rr:ServiceVehicle>
|
|
||||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
|
||||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
|
||||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
|
||||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
|
||||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
|
||||||
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
|
||||||
</rr:ServiceVehicle>
|
|
||||||
{{/Vehicle}}
|
|
||||||
|
|
||||||
{{#Company}}
|
|
||||||
<rr:Company>
|
|
||||||
{{#Name}}<rr:Name>{{Name}}</rr:Name>{{/Name}}
|
|
||||||
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
|
|
||||||
</rr:Company>
|
|
||||||
{{/Company}}
|
|
||||||
|
|
||||||
<!-- Search behavior flags (all optional) -->
|
|
||||||
{{#SearchMode}}<rr:SearchMode>{{SearchMode}}</rr:SearchMode>{{/SearchMode}}
|
|
||||||
{{#ExactMatch}}<rr:ExactMatch>{{ExactMatch}}</rr:ExactMatch>{{/ExactMatch}}
|
|
||||||
{{#PartialMatch}}<rr:PartialMatch>{{PartialMatch}}</rr:PartialMatch>{{/PartialMatch}}
|
|
||||||
{{#CaseInsensitive}}<rr:CaseInsensitive>{{CaseInsensitive}}</rr:CaseInsensitive>{{/CaseInsensitive}}
|
|
||||||
|
|
||||||
<!-- Result shaping (all optional) -->
|
|
||||||
{{#ReturnCustomers}}<rr:ReturnCustomers>{{ReturnCustomers}}</rr:ReturnCustomers>{{/ReturnCustomers}}
|
|
||||||
{{#ReturnVehicles}}<rr:ReturnVehicles>{{ReturnVehicles}}</rr:ReturnVehicles>{{/ReturnVehicles}}
|
|
||||||
{{#ReturnCompanies}}<rr:ReturnCompanies>{{ReturnCompanies}}</rr:ReturnCompanies>{{/ReturnCompanies}}
|
|
||||||
|
|
||||||
<!-- Paging/sorting (all optional) -->
|
|
||||||
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
|
||||||
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
|
||||||
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
|
|
||||||
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
|
|
||||||
</rr:SearchCriteria>
|
|
||||||
</rr:CombinedSearchRq>
|
|
||||||
|
|||||||
@@ -1,158 +1,117 @@
|
|||||||
<rr:RepairOrderInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeCreateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<BSMRepairOrderReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<RepairOrder>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>
|
||||||
|
{{#DmsRepairOrderId}}<DmsRepairOrderId>{{DmsRepairOrderId}}</DmsRepairOrderId>{{/DmsRepairOrderId}}
|
||||||
<rr:Dealer>
|
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
|
||||||
</rr:Dealer>
|
{{#ProfitCenter}}<ProfitCenter>{{ProfitCenter}}</ProfitCenter>{{/ProfitCenter}}
|
||||||
|
{{#ROType}}<ROType>{{ROType}}</ROType>{{/ROType}}
|
||||||
<rr:RepairOrder>
|
{{#Status}}<Status>{{Status}}</Status>{{/Status}}
|
||||||
<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>
|
{{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
|
||||||
{{#DmsRepairOrderId}}<rr:DmsRepairOrderId>{{DmsRepairOrderId}}</rr:DmsRepairOrderId>{{/DmsRepairOrderId}}
|
{{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
|
||||||
|
<Customer>
|
||||||
<!-- Core dates -->
|
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||||
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||||
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
|
||||||
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
|
||||||
|
{{#Address}}
|
||||||
<!-- People & routing -->
|
<Address>
|
||||||
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
|
||||||
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
|
||||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
{{#City}}<City>{{City}}</City>{{/City}}
|
||||||
{{#ProfitCenter}}<rr:ProfitCenter>{{ProfitCenter}}</rr:ProfitCenter>{{/ProfitCenter}}
|
{{#State}}<State>{{State}}</State>{{/State}}
|
||||||
|
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
|
||||||
<!-- Type & status -->
|
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
|
||||||
{{#ROType}}<rr:ROType>{{ROType}}</rr:ROType>{{/ROType}}
|
</Address>
|
||||||
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}}
|
{{/Address}}
|
||||||
{{#IsBodyShop}}<rr:IsBodyShop>{{IsBodyShop}}</rr:IsBodyShop>{{/IsBodyShop}}
|
</Customer>
|
||||||
{{#DRPFlag}}<rr:DRPFlag>{{DRPFlag}}</rr:DRPFlag>{{/DRPFlag}}
|
<ServiceVehicle>
|
||||||
|
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
|
||||||
<!-- Customer -->
|
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||||
<rr:Customer>
|
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
|
||||||
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||||
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||||
|
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||||
<!-- Optional address if you have it -->
|
</ServiceVehicle>
|
||||||
{{#Address}}
|
{{#JobLines}}
|
||||||
<rr:Address>
|
<JobLine>
|
||||||
{{#Line1}}<rr:Line1>{{Line1}}</rr:Line1>{{/Line1}}
|
<Sequence>{{Sequence}}</Sequence>
|
||||||
{{#Line2}}<rr:Line2>{{Line2}}</rr:Line2>{{/Line2}}
|
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||||
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
{{#LineType}}<LineType>
|
||||||
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
{{LineType}}</LineType>{{/LineType}
|
||||||
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
{{#Category}}<Category>
|
||||||
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
{{Category}}</Category>{{/Category}}
|
||||||
</rr:Address>
|
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||||
{{/Address}}
|
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||||
</rr:Customer>
|
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||||
|
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||||
<!-- Vehicle -->
|
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||||
<rr:Vehicle>
|
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||||
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
|
||||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
{{#Taxes}}
|
||||||
</rr:Vehicle>
|
<Taxes>
|
||||||
|
{{#Items}}
|
||||||
<!-- Job lines -->
|
<Tax>
|
||||||
{{#JobLines}}
|
<Code>{{Code}}</Code>
|
||||||
<rr:JobLine>
|
<Amount>{{Amount}}</Amount>
|
||||||
<rr:Sequence>{{Sequence}}</rr:Sequence>
|
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
|
||||||
{{#ParentSequence}}<rr:ParentSequence>{{ParentSequence}}</rr:ParentSequence>{{/ParentSequence}}
|
</Tax>
|
||||||
|
{{/Items}}
|
||||||
{{#LineType}}<rr:LineType>
|
</Taxes>
|
||||||
{{LineType}}</rr:LineType>{{/LineType}} <!-- LABOR | PART | MISC | FEE | DISCOUNT -->
|
{{/Taxes}}
|
||||||
{{#Category}}<rr:Category>
|
</JobLine>
|
||||||
{{Category}}</rr:Category>{{/Category}} <!-- e.g., BODY, PAINT, GLASS -->
|
{{/JobLines}}
|
||||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
{{#Totals}}
|
||||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
<Totals>
|
||||||
|
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
|
||||||
<!-- Labor fields -->
|
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
|
||||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
|
||||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
|
||||||
|
{{#DiscountTotal}}<DiscountTotal>{{DiscountTotal}}</DiscountTotal>{{/DiscountTotal}}
|
||||||
<!-- Part fields -->
|
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
|
||||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
<GrandTotal>{{GrandTotal}}</GrandTotal>
|
||||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
</Totals>
|
||||||
|
{{/Totals}}
|
||||||
<!-- Amounts -->
|
{{#Payments}}
|
||||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
<Payments>
|
||||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
|
||||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
|
||||||
{{#DiscountAmount}}<rr:DiscountAmount>{{DiscountAmount}}</rr:DiscountAmount>{{/DiscountAmount}}
|
|
||||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
|
||||||
{{#GLAccount}}<rr:GLAccount>{{GLAccount}}</rr:GLAccount>{{/GLAccount}}
|
|
||||||
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
|
|
||||||
|
|
||||||
<!-- Tax details (optional) -->
|
|
||||||
{{#Taxes}}
|
|
||||||
<rr:Taxes>
|
|
||||||
{{#Items}}
|
{{#Items}}
|
||||||
<rr:Tax>
|
<Payment>
|
||||||
<rr:Code>{{Code}}</rr:Code>
|
<PayerType>{{PayerType}}</PayerType>
|
||||||
<rr:Amount>{{Amount}}</rr:Amount>
|
{{#PayerName}}<PayerName>{{PayerName}}</PayerName>{{/PayerName}}
|
||||||
{{#Rate}}<rr:Rate>{{Rate}}</rr:Rate>{{/Rate}}
|
<Amount>{{Amount}}</Amount>
|
||||||
</rr:Tax>
|
{{#Method}}<Method>{{Method}}</Method>{{/Method}}
|
||||||
|
{{#Reference}}<Reference>{{Reference}}</Reference>{{/Reference}}
|
||||||
|
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||||
|
</Payment>
|
||||||
{{/Items}}
|
{{/Items}}
|
||||||
</rr:Taxes>
|
</Payments>
|
||||||
{{/Taxes}}
|
{{/Payments}}
|
||||||
</rr:JobLine>
|
{{#Insurance}}
|
||||||
{{/JobLines}}
|
<Insurance>
|
||||||
|
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||||
<!-- Totals -->
|
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
|
||||||
{{#Totals}}
|
{{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
|
||||||
<rr:Totals>
|
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
|
||||||
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
</Insurance>
|
||||||
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
{{/Insurance}}
|
||||||
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
{{#Notes}}
|
||||||
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
<Notes>
|
||||||
{{#DiscountTotal}}<rr:DiscountTotal>{{DiscountTotal}}</rr:DiscountTotal>{{/DiscountTotal}}
|
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||||
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
</Notes>
|
||||||
<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>
|
{{/Notes}}
|
||||||
</rr:Totals>
|
</RepairOrder>
|
||||||
{{/Totals}}
|
</BSMRepairOrderReq>
|
||||||
|
</rey_RomeCreateBSMRepairOrderReq>
|
||||||
<!-- Payers/Payments (optional) -->
|
|
||||||
{{#Payments}}
|
|
||||||
<rr:Payments>
|
|
||||||
{{#Items}}
|
|
||||||
<rr:Payment>
|
|
||||||
<rr:PayerType>{{PayerType}}</rr:PayerType> <!-- CUSTOMER | INSURANCE | WARRANTY | FLEET -->
|
|
||||||
{{#PayerName}}<rr:PayerName>{{PayerName}}</rr:PayerName>{{/PayerName}}
|
|
||||||
<rr:Amount>{{Amount}}</rr:Amount>
|
|
||||||
{{#Method}}<rr:Method>{{Method}}</rr:Method>{{/Method}}
|
|
||||||
{{#Reference}}<rr:Reference>{{Reference}}</rr:Reference>{{/Reference}}
|
|
||||||
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
|
|
||||||
</rr:Payment>
|
|
||||||
{{/Items}}
|
|
||||||
</rr:Payments>
|
|
||||||
{{/Payments}}
|
|
||||||
|
|
||||||
<!-- Insurance block (optional) -->
|
|
||||||
{{#Insurance}}
|
|
||||||
<rr:Insurance>
|
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
|
||||||
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
|
|
||||||
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
|
|
||||||
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
|
|
||||||
</rr:Insurance>
|
|
||||||
{{/Insurance}}
|
|
||||||
|
|
||||||
<!-- Notes -->
|
|
||||||
{{#Notes}}
|
|
||||||
<rr:Notes>
|
|
||||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
|
||||||
</rr:Notes>
|
|
||||||
{{/Notes}}
|
|
||||||
</rr:RepairOrder>
|
|
||||||
</rr:RepairOrderInsertRq>
|
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
<rr:GetAdvisorsRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeGetAdvisorsReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<GetAdvisorsReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<QueryData{{#SearchCriteria.MaxResults}} MaxRecs="{{SearchCriteria.MaxResults}}"{{/SearchCriteria.MaxResults}}>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
{{#SearchCriteria.AdvisorId}}<AdvisorID>{{SearchCriteria.AdvisorId}}</AdvisorID>{{/SearchCriteria.AdvisorId}}
|
||||||
|
{{#SearchCriteria.FirstName}}<FirstName>{{SearchCriteria.FirstName}}</FirstName>{{/SearchCriteria.FirstName}}
|
||||||
<rr:Dealer>
|
{{#SearchCriteria.LastName}}<LastName>{{SearchCriteria.LastName}}</LastName>{{/SearchCriteria.LastName}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
{{#SearchCriteria.Department}}<Department>{{SearchCriteria.Department}}</Department>{{/SearchCriteria.Department}}
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#SearchCriteria.Status}}<Status>{{SearchCriteria.Status}}</Status>{{/SearchCriteria.Status}}
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
{{#SearchCriteria.IncludeInactive}}<IncludeInactive>{{SearchCriteria.IncludeInactive}}</IncludeInactive>{{/SearchCriteria.IncludeInactive}}
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{#SearchCriteria.PageNumber}}<PageNumber>{{SearchCriteria.PageNumber}}</PageNumber>{{/SearchCriteria.PageNumber}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
{{#SearchCriteria.SortBy}}<SortBy>{{SearchCriteria.SortBy}}</SortBy>{{/SearchCriteria.SortBy}}
|
||||||
</rr:Dealer>
|
{{#SearchCriteria.SortDirection}}<SortDirection>{{SearchCriteria.SortDirection}}</SortDirection>{{/SearchCriteria.SortDirection}}
|
||||||
|
</QueryData>
|
||||||
{{#SearchCriteria}}
|
</GetAdvisorsReq>
|
||||||
<rr:SearchCriteria>
|
</rey_RomeGetAdvisorsReq>
|
||||||
{{#AdvisorId}}<rr:AdvisorId>{{AdvisorId}}</rr:AdvisorId>{{/AdvisorId}}
|
|
||||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
|
||||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
|
||||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
|
||||||
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}} <!-- ACTIVE | INACTIVE -->
|
|
||||||
{{#SearchMode}}<rr:SearchMode>
|
|
||||||
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
|
|
||||||
{{#Email}}<rr:Email>{{Email}}</rr:Email>{{/Email}}
|
|
||||||
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
|
|
||||||
{{#IncludeInactive}}<rr:IncludeInactive>{{IncludeInactive}}</rr:IncludeInactive>{{/IncludeInactive}}
|
|
||||||
|
|
||||||
<!-- Optional paging/sorting -->
|
|
||||||
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
|
||||||
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
|
||||||
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
|
|
||||||
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
|
|
||||||
</rr:SearchCriteria>
|
|
||||||
{{/SearchCriteria}}
|
|
||||||
</rr:GetAdvisorsRq>
|
|
||||||
|
|||||||
@@ -1,50 +1,25 @@
|
|||||||
<rr:GetPartRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeGetPartsReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<GetPartReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}{{#PageNumber}} Page="{{PageNumber}}"{{/PageNumber}}>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||||
|
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||||
<rr:Dealer>
|
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
{{#Vendor}}<Vendor>{{Vendor}}</Vendor>{{/Vendor}}
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
{{#Brand}}<Brand>{{Brand}}</Brand>{{/Brand}}
|
||||||
</rr:Dealer>
|
{{#IsOEM}}<IsOEM>{{IsOEM}}</IsOEM>{{/IsOEM}}
|
||||||
|
{{#IsAftermarket}}<IsAftermarket>{{IsAftermarket}}</IsAftermarket>{{/IsAftermarket}}
|
||||||
<rr:SearchCriteria>
|
{{#InStock}}<InStock>{{InStock}}</InStock>{{/InStock}}
|
||||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
{{#Warehouse}}<Warehouse>{{Warehouse}}</Warehouse>{{/Warehouse}}
|
||||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
{{#Location}}<Location>{{Location}}</Location>{{/Location}}
|
||||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
{{#MinPrice}}<MinPrice>{{MinPrice}}</MinPrice>{{/MinPrice}}
|
||||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
{{#MaxPrice}}<MaxPrice>{{MaxPrice}}</MaxPrice>{{/MaxPrice}}
|
||||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
|
||||||
{{#Vendor}}<rr:Vendor>{{Vendor}}</rr:Vendor>{{/Vendor}}
|
{{#SearchMode}}<SearchMode>{{SearchMode}}</SearchMode>{{/SearchMode}}
|
||||||
{{#Category}}<rr:Category>{{Category}}</rr:Category>{{/Category}}
|
{{#SortBy}}<SortBy>{{SortBy}}</SortBy>{{/SortBy}}
|
||||||
|
{{#SortDirection}}<SortDirection>{{SortDirection}}</SortDirection>{{/SortDirection}}
|
||||||
<!-- Optional classification flags -->
|
</QueryData>
|
||||||
{{#Brand}}<rr:Brand>{{Brand}}</rr:Brand>{{/Brand}}
|
</GetPartReq>
|
||||||
{{#IsOEM}}<rr:IsOEM>{{IsOEM}}</rr:IsOEM>{{/IsOEM}} <!-- true | false -->
|
</rey_RomeGetPartsReq>
|
||||||
{{#IsAftermarket}}<rr:IsAftermarket>{{IsAftermarket}}</rr:IsAftermarket>{{/IsAftermarket}}
|
|
||||||
|
|
||||||
<!-- Availability / inventory -->
|
|
||||||
{{#InStock}}<rr:InStock>{{InStock}}</rr:InStock>{{/InStock}} <!-- true | false -->
|
|
||||||
{{#Warehouse}}<rr:Warehouse>{{Warehouse}}</rr:Warehouse>{{/Warehouse}}
|
|
||||||
{{#Location}}<rr:Location>{{Location}}</rr:Location>{{/Location}}
|
|
||||||
|
|
||||||
<!-- Pricing filters -->
|
|
||||||
{{#MinPrice}}<rr:MinPrice>{{MinPrice}}</rr:MinPrice>{{/MinPrice}}
|
|
||||||
{{#MaxPrice}}<rr:MaxPrice>{{MaxPrice}}</rr:MaxPrice>{{/MaxPrice}}
|
|
||||||
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
|
||||||
|
|
||||||
<!-- Search behavior -->
|
|
||||||
{{#SearchMode}}<rr:SearchMode>
|
|
||||||
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
|
|
||||||
|
|
||||||
<!-- Paging / sorting -->
|
|
||||||
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
|
||||||
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
|
||||||
{{#SortBy}}<rr:SortBy>
|
|
||||||
{{SortBy}}</rr:SortBy>{{/SortBy}} <!-- e.g., PARTNUMBER, DESCRIPTION, PRICE -->
|
|
||||||
{{#SortDirection}}<rr:SortDirection>
|
|
||||||
{{SortDirection}}</rr:SortDirection>{{/SortDirection}} <!-- ASC | DESC -->
|
|
||||||
</rr:SearchCriteria>
|
|
||||||
</rr:GetPartRq>
|
|
||||||
|
|||||||
@@ -1,102 +1,63 @@
|
|||||||
<rr:CustomerInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeCustomerInsertReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<CustomerInsertReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<Customer>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
{{#CustomerNumber}}<CustomerNumber>{{CustomerNumber}}</CustomerNumber>{{/CustomerNumber}}
|
||||||
|
{{#CustomerType}}<CustomerType>
|
||||||
<rr:Dealer>
|
{{CustomerType}}</CustomerType>{{/CustomerType}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
<CustomerName>{{CustomerName}}</CustomerName>
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
{{#Language}}<Language>{{Language}}</Language>{{/Language}}
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{#GroupName}}<GroupName>{{GroupName}}</GroupName>{{/GroupName}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
{{#TaxExempt}}<TaxExempt>{{TaxExempt}}</TaxExempt>{{/TaxExempt}}
|
||||||
</rr:Dealer>
|
{{#DiscountLevel}}<DiscountLevel>{{DiscountLevel}}</DiscountLevel>{{/DiscountLevel}}
|
||||||
|
{{#Active}}<Active>{{Active}}</Active>{{/Active}}
|
||||||
<rr:Customer>
|
{{#Addresses}}
|
||||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
<Address>
|
||||||
{{#CustomerType}}<rr:CustomerType>
|
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
|
||||||
{{CustomerType}}</rr:CustomerType>{{/CustomerType}} <!-- RETAIL | FLEET | INTERNAL -->
|
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
|
||||||
|
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
{{#City}}<City>{{City}}</City>{{/City}}
|
||||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
{{#State}}<State>{{State}}</State>{{/State}}
|
||||||
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
|
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
|
||||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
|
||||||
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
|
</Address>
|
||||||
|
{{/Addresses}}
|
||||||
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
|
{{#Phones}}
|
||||||
|
<Phone>
|
||||||
<!-- Optional customer classification -->
|
<Type>{{Type}}</Type>
|
||||||
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
|
<Number>{{Number}}</Number>
|
||||||
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
|
{{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
|
||||||
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
|
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
|
||||||
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
|
</Phone>
|
||||||
|
{{/Phones}}
|
||||||
<!-- Addresses -->
|
{{#Emails}}
|
||||||
{{#Addresses}}
|
<Email>
|
||||||
<rr:Address>
|
<Type>{{Type}}</Type>
|
||||||
{{#AddressType}}<rr:AddressType>
|
<Address>{{Address}}</Address>
|
||||||
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
|
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
|
||||||
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
|
</Email>
|
||||||
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
|
{{/Emails}}
|
||||||
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
{{#Insurance}}
|
||||||
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
<Insurance>
|
||||||
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||||
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
|
||||||
</rr:Address>
|
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
|
||||||
{{/Addresses}}
|
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
|
||||||
|
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
|
||||||
<!-- Phones -->
|
</Insurance>
|
||||||
{{#Phones}}
|
{{/Insurance}}
|
||||||
<rr:Phone>
|
{{#LinkedAccounts}}
|
||||||
<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>
|
<LinkedAccount>
|
||||||
{{#PhoneType}}<rr:PhoneType>
|
<Type>{{Type}}</Type>
|
||||||
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
<AccountNumber>{{AccountNumber}}</AccountNumber>
|
||||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
{{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
|
||||||
</rr:Phone>
|
</LinkedAccount>
|
||||||
{{/Phones}}
|
{{/LinkedAccounts}}
|
||||||
|
{{#Notes}}
|
||||||
<!-- Emails -->
|
<Notes>
|
||||||
{{#Emails}}
|
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||||
<rr:Email>
|
</Notes>
|
||||||
<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>
|
{{/Notes}}
|
||||||
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
</Customer>
|
||||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
</CustomerInsertReq>
|
||||||
</rr:Email>
|
</rey_RomeCustomerInsertReq>
|
||||||
{{/Emails}}
|
|
||||||
|
|
||||||
<!-- Driver's License -->
|
|
||||||
{{#DriverLicense}}
|
|
||||||
<rr:DriverLicense>
|
|
||||||
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
|
|
||||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
|
||||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
|
||||||
</rr:DriverLicense>
|
|
||||||
{{/DriverLicense}}
|
|
||||||
|
|
||||||
<!-- Insurance -->
|
|
||||||
{{#Insurance}}
|
|
||||||
<rr:Insurance>
|
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
|
||||||
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
|
||||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
|
||||||
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
|
||||||
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
|
||||||
</rr:Insurance>
|
|
||||||
{{/Insurance}}
|
|
||||||
|
|
||||||
<!-- Optional linked accounts -->
|
|
||||||
{{#LinkedAccounts}}
|
|
||||||
<rr:LinkedAccount>
|
|
||||||
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
|
|
||||||
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
|
|
||||||
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
|
|
||||||
</rr:LinkedAccount>
|
|
||||||
{{/LinkedAccounts}}
|
|
||||||
|
|
||||||
<!-- Notes -->
|
|
||||||
{{#Notes}}
|
|
||||||
<rr:Notes>
|
|
||||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
|
||||||
</rr:Notes>
|
|
||||||
{{/Notes}}
|
|
||||||
</rr:Customer>
|
|
||||||
</rr:CustomerInsertRq>
|
|
||||||
|
|||||||
@@ -1,83 +1,57 @@
|
|||||||
<rr:ServiceVehicleAddRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeServVehicleInsertReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<ServVehicleInsertReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<ServiceVehicle>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||||
|
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||||
<rr:Dealer>
|
{{#UnitNumber}}<UnitNumber>{{UnitNumber}}</UnitNumber>{{/UnitNumber}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
{{#StockNumber}}<StockNumber>{{StockNumber}}</StockNumber>{{/StockNumber}}
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
{{#Trim}}<Trim>{{Trim}}</Trim>{{/Trim}}
|
||||||
</rr:Dealer>
|
{{#BodyStyle}}<BodyStyle>{{BodyStyle}}</BodyStyle>{{/BodyStyle}}
|
||||||
|
{{#Transmission}}<Transmission>{{Transmission}}</Transmission>{{/Transmission}}
|
||||||
<rr:ServiceVehicle>
|
{{#Engine}}<Engine>{{Engine}}</Engine>{{/Engine}}
|
||||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
{{#FuelType}}<FuelType>{{FuelType}}</FuelType>{{/FuelType}}
|
||||||
|
{{#DriveType}}<DriveType>{{DriveType}}</DriveType>{{/DriveType}}
|
||||||
<!-- Identity -->
|
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
|
||||||
{{#UnitNumber}}<rr:UnitNumber>{{UnitNumber}}</rr:UnitNumber>{{/UnitNumber}}
|
{{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
|
||||||
{{#StockNumber}}<rr:StockNumber>{{StockNumber}}</rr:StockNumber>{{/StockNumber}}
|
{{#RegistrationExpiry}}<RegistrationExpiry>{{RegistrationExpiry}}</RegistrationExpiry>{{/RegistrationExpiry}}
|
||||||
|
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||||
<!-- Descriptive -->
|
{{#OdometerUnits}}<OdometerUnits>
|
||||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
{{OdometerUnits}}</OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
|
||||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
{{#InServiceDate}}<InServiceDate>{{InServiceDate}}</InServiceDate>{{/InServiceDate}}
|
||||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
{{#Ownership}}
|
||||||
{{#Trim}}<rr:Trim>{{Trim}}</rr:Trim>{{/Trim}}
|
<Ownership>
|
||||||
{{#BodyStyle}}<rr:BodyStyle>{{BodyStyle}}</rr:BodyStyle>{{/BodyStyle}}
|
{{#OwnerId}}<OwnerId>{{OwnerId}}</OwnerId>{{/OwnerId}}
|
||||||
{{#Transmission}}<rr:Transmission>{{Transmission}}</rr:Transmission>{{/Transmission}}
|
{{#OwnerName}}<OwnerName>{{OwnerName}}</OwnerName>{{/OwnerName}}
|
||||||
{{#Engine}}<rr:Engine>{{Engine}}</rr:Engine>{{/Engine}}
|
{{#OwnershipType}}<OwnershipType>
|
||||||
{{#FuelType}}<rr:FuelType>{{FuelType}}</rr:FuelType>{{/FuelType}}
|
{{OwnershipType}}</OwnershipType>{{/OwnershipType}}
|
||||||
{{#DriveType}}<rr:DriveType>{{DriveType}}</rr:DriveType>{{/DriveType}}
|
</Ownership>
|
||||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
{{/Ownership}}
|
||||||
|
{{#Insurance}}
|
||||||
<!-- Registration -->
|
<Insurance>
|
||||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
|
||||||
{{#RegistrationExpiry}}<rr:RegistrationExpiry>{{RegistrationExpiry}}</rr:RegistrationExpiry>{{/RegistrationExpiry}}
|
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
|
||||||
|
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
|
||||||
<!-- Odometer -->
|
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
|
||||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
</Insurance>
|
||||||
{{#OdometerUnits}}<rr:OdometerUnits>
|
{{/Insurance}}
|
||||||
{{OdometerUnits}}</rr:OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
|
{{#Warranty}}
|
||||||
{{#InServiceDate}}<rr:InServiceDate>{{InServiceDate}}</rr:InServiceDate>{{/InServiceDate}}
|
<Warranty>
|
||||||
|
{{#WarrantyCompany}}<WarrantyCompany>{{WarrantyCompany}}</WarrantyCompany>{{/WarrantyCompany}}
|
||||||
<!-- Ownership -->
|
{{#WarrantyNumber}}<WarrantyNumber>{{WarrantyNumber}}</WarrantyNumber>{{/WarrantyNumber}}
|
||||||
{{#Ownership}}
|
{{#WarrantyType}}<WarrantyType>{{WarrantyType}}</WarrantyType>{{/WarrantyType}}
|
||||||
<rr:Ownership>
|
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
|
||||||
{{#OwnerId}}<rr:OwnerId>{{OwnerId}}</rr:OwnerId>{{/OwnerId}}
|
</Warranty>
|
||||||
{{#OwnerName}}<rr:OwnerName>{{OwnerName}}</rr:OwnerName>{{/OwnerName}}
|
{{/Warranty}}
|
||||||
{{#OwnershipType}}<rr:OwnershipType>
|
{{#VehicleNotes}}
|
||||||
{{OwnershipType}}</rr:OwnershipType>{{/OwnershipType}} <!-- OWNER | LEASED | FLEET -->
|
<Notes>
|
||||||
</rr:Ownership>
|
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||||
{{/Ownership}}
|
</Notes>
|
||||||
|
{{/VehicleNotes}}
|
||||||
<!-- Insurance -->
|
</ServiceVehicle>
|
||||||
{{#Insurance}}
|
</ServVehicleInsertReq>
|
||||||
<rr:Insurance>
|
</rey_RomeServVehicleInsertReq>
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
|
||||||
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
|
||||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
|
||||||
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
|
||||||
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
|
||||||
</rr:Insurance>
|
|
||||||
{{/Insurance}}
|
|
||||||
|
|
||||||
<!-- Warranty -->
|
|
||||||
{{#Warranty}}
|
|
||||||
<rr:Warranty>
|
|
||||||
{{#WarrantyCompany}}<rr:WarrantyCompany>{{WarrantyCompany}}</rr:WarrantyCompany>{{/WarrantyCompany}}
|
|
||||||
{{#WarrantyNumber}}<rr:WarrantyNumber>{{WarrantyNumber}}</rr:WarrantyNumber>{{/WarrantyNumber}}
|
|
||||||
{{#WarrantyType}}<rr:WarrantyType>{{WarrantyType}}</rr:WarrantyType>{{/WarrantyType}}
|
|
||||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
|
||||||
</rr:Warranty>
|
|
||||||
{{/Warranty}}
|
|
||||||
|
|
||||||
<!-- Notes -->
|
|
||||||
{{#VehicleNotes}}
|
|
||||||
<rr:Notes>
|
|
||||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
|
||||||
</rr:Notes>
|
|
||||||
{{/VehicleNotes}}
|
|
||||||
</rr:ServiceVehicle>
|
|
||||||
</rr:ServiceVehicleAddRq>
|
|
||||||
|
|||||||
@@ -1,107 +1,83 @@
|
|||||||
<rr:CustomerUpdateRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeCustomerUpdateReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<CustomerUpdateReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<Customer>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
<CustomerId>{{CustomerId}}</CustomerId>
|
||||||
|
{{#CustomerType}}<CustomerType>
|
||||||
|
{{CustomerType}}</CustomerType>{{/CustomerType}}
|
||||||
|
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||||
|
{{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
|
||||||
|
{{#PreferredName}}<PreferredName>{{PreferredName}}</PreferredName>{{/PreferredName}}
|
||||||
|
{{#Language}}<Language>{{Language}}</Language>{{/Language}}
|
||||||
|
{{#GroupName}}<GroupName>{{GroupName}}</GroupName>{{/GroupName}}
|
||||||
|
{{#TaxExempt}}<TaxExempt>{{TaxExempt}}</TaxExempt>{{/TaxExempt}}
|
||||||
|
{{#DiscountLevel}}<DiscountLevel>{{DiscountLevel}}</DiscountLevel>{{/DiscountLevel}}
|
||||||
|
{{#Active}}<Active>{{Active}}</Active>{{/Active}}
|
||||||
|
{{#Addresses}}
|
||||||
|
<Address>
|
||||||
|
{{#AddressId}}<AddressId>{{AddressId}}</AddressId>{{/AddressId}}
|
||||||
|
{{#Type}}<Type>
|
||||||
|
{{Type}}</Type>{{/Type}}
|
||||||
|
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
|
||||||
|
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
|
||||||
|
{{#City}}<City>{{City}}</City>{{/City}}
|
||||||
|
{{#State}}<State>{{State}}</State>{{/State}}
|
||||||
|
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
|
||||||
|
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
|
||||||
|
{{#IsPrimary}}<IsPrimary>{{IsPrimary}}</IsPrimary>{{/IsPrimary}}
|
||||||
|
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
|
||||||
|
</Address>
|
||||||
|
{{/Addresses}}
|
||||||
|
{{#Phones}}
|
||||||
|
<Phone>
|
||||||
|
{{#PhoneId}}<PhoneId>{{PhoneId}}</PhoneId>{{/PhoneId}}
|
||||||
|
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
|
||||||
|
{{#Number}}<Number>{{Number}}</Number>{{/Number}}
|
||||||
|
{{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
|
||||||
|
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
|
||||||
|
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
|
||||||
|
</Phone>
|
||||||
|
{{/Phones}}
|
||||||
|
{{#Emails}}
|
||||||
|
<Email>
|
||||||
|
{{#EmailId}}<EmailId>{{EmailId}}</EmailId>{{/EmailId}}
|
||||||
|
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
|
||||||
|
{{#Address}}<Address>{{Address}}</Address>{{/Address}}
|
||||||
|
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
|
||||||
|
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
|
||||||
|
</Email>
|
||||||
|
{{/Emails}}
|
||||||
|
{{#DriverLicense}}
|
||||||
|
<DriverLicense>
|
||||||
|
{{#LicenseNumber}}<LicenseNumber>{{LicenseNumber}}</LicenseNumber>{{/LicenseNumber}}
|
||||||
|
{{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
|
||||||
|
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
|
||||||
|
</DriverLicense>
|
||||||
|
{{/DriverLicense}}
|
||||||
|
{{#Insurance}}
|
||||||
|
<Insurance>
|
||||||
|
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||||
|
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
|
||||||
|
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
|
||||||
|
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
|
||||||
|
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
|
||||||
|
</Insurance>
|
||||||
|
{{/Insurance}}
|
||||||
|
{{#LinkedAccounts}}
|
||||||
|
<LinkedAccount>
|
||||||
|
{{#AccountId}}<AccountId>{{AccountId}}</AccountId>{{/AccountId}}
|
||||||
|
<Type>{{Type}}
|
||||||
|
</Type>
|
||||||
|
{{#AccountNumber}}<AccountNumber>{{AccountNumber}}</AccountNumber>{{/AccountNumber}}
|
||||||
|
{{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
|
||||||
|
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
|
||||||
|
</LinkedAccount>
|
||||||
|
{{/LinkedAccounts}}
|
||||||
|
|
||||||
<rr:Dealer>
|
{{#Notes}}
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
<Notes>
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
</Notes>
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{/Notes}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
</Customer>
|
||||||
</rr:Dealer>
|
</CustomerUpdateReq>
|
||||||
|
</rey_RomeCustomerUpdateReq>
|
||||||
<rr:Customer>
|
|
||||||
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
|
||||||
{{#CustomerType}}<rr:CustomerType>{{CustomerType}}</rr:CustomerType>{{/CustomerType}}
|
|
||||||
|
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
|
||||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
|
||||||
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
|
|
||||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
|
||||||
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
|
|
||||||
|
|
||||||
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
|
|
||||||
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
|
|
||||||
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
|
|
||||||
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
|
|
||||||
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
|
|
||||||
|
|
||||||
<!-- Addresses -->
|
|
||||||
{{#Addresses}}
|
|
||||||
<rr:Address>
|
|
||||||
{{#AddressId}}<rr:AddressId>{{AddressId}}</rr:AddressId>{{/AddressId}}
|
|
||||||
{{#AddressType}}<rr:AddressType>
|
|
||||||
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
|
|
||||||
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
|
|
||||||
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
|
|
||||||
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
|
||||||
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
|
||||||
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
|
||||||
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
|
||||||
{{#IsPrimary}}<rr:IsPrimary>{{IsPrimary}}</rr:IsPrimary>{{/IsPrimary}}
|
|
||||||
</rr:Address>
|
|
||||||
{{/Addresses}}
|
|
||||||
|
|
||||||
<!-- Phones -->
|
|
||||||
{{#Phones}}
|
|
||||||
<rr:Phone>
|
|
||||||
{{#PhoneId}}<rr:PhoneId>{{PhoneId}}</rr:PhoneId>{{/PhoneId}}
|
|
||||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
|
||||||
{{#PhoneType}}<rr:PhoneType>
|
|
||||||
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
|
||||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
|
||||||
{{#IsDeleted}}<rr:IsDeleted>
|
|
||||||
{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}} <!-- Mark for deletion -->
|
|
||||||
</rr:Phone>
|
|
||||||
{{/Phones}}
|
|
||||||
|
|
||||||
<!-- Emails -->
|
|
||||||
{{#Emails}}
|
|
||||||
<rr:Email>
|
|
||||||
{{#EmailId}}<rr:EmailId>{{EmailId}}</rr:EmailId>{{/EmailId}}
|
|
||||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
|
||||||
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
|
||||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
|
||||||
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
|
|
||||||
</rr:Email>
|
|
||||||
{{/Emails}}
|
|
||||||
|
|
||||||
<!-- Driver's License -->
|
|
||||||
{{#DriverLicense}}
|
|
||||||
<rr:DriverLicense>
|
|
||||||
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
|
|
||||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
|
||||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
|
||||||
</rr:DriverLicense>
|
|
||||||
{{/DriverLicense}}
|
|
||||||
|
|
||||||
<!-- Insurance -->
|
|
||||||
{{#Insurance}}
|
|
||||||
<rr:Insurance>
|
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
|
||||||
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
|
||||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
|
||||||
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
|
||||||
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
|
||||||
</rr:Insurance>
|
|
||||||
{{/Insurance}}
|
|
||||||
|
|
||||||
<!-- Linked Accounts -->
|
|
||||||
{{#LinkedAccounts}}
|
|
||||||
<rr:LinkedAccount>
|
|
||||||
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
|
|
||||||
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
|
|
||||||
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
|
|
||||||
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
|
|
||||||
</rr:LinkedAccount>
|
|
||||||
{{/LinkedAccounts}}
|
|
||||||
|
|
||||||
<!-- Notes -->
|
|
||||||
{{#Notes}}
|
|
||||||
<rr:Notes>
|
|
||||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
|
||||||
</rr:Notes>
|
|
||||||
{{/Notes}}
|
|
||||||
</rr:Customer>
|
|
||||||
</rr:CustomerUpdateRq>
|
|
||||||
|
|||||||
@@ -1,135 +1,139 @@
|
|||||||
<rr:RepairOrderChgRq xmlns:rr="http://reynoldsandrey.com/">
|
<rey_RomeUpdateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||||
<!-- Optional request metadata -->
|
<BSMRepairOrderChgReq>
|
||||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
<RepairOrder>
|
||||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
{{#RepairOrderId}}<RepairOrderId>{{RepairOrderId}}</RepairOrderId>{{/RepairOrderId}}
|
||||||
|
{{#RepairOrderNumber}}<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>{{/RepairOrderNumber}}
|
||||||
<rr:Dealer>
|
{{#Status}}<Status>
|
||||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
{{Status}}</Status>{{/Status}}
|
||||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
{{#ROType}}<ROType>
|
||||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
{{ROType}}</ROType>{{/ROType}}
|
||||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
|
||||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
|
||||||
</rr:Dealer>
|
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
|
||||||
|
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||||
<rr:RepairOrder>
|
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
|
||||||
<!-- Identity -->
|
{{#LocationCode}}<LocationCode>{{LocationCode}}</LocationCode>{{/LocationCode}}
|
||||||
{{#RepairOrderId}}<rr:RepairOrderId>{{RepairOrderId}}</rr:RepairOrderId>{{/RepairOrderId}}
|
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
|
||||||
{{#RepairOrderNumber}}<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>{{/RepairOrderNumber}}
|
{{#PurchaseOrder}}<PurchaseOrder>{{PurchaseOrder}}</PurchaseOrder>{{/PurchaseOrder}}
|
||||||
|
{{#Customer}}
|
||||||
<!-- Header fields that may be patched -->
|
<Customer>
|
||||||
{{#Status}}<rr:Status>
|
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||||
{{Status}}</rr:Status>{{/Status}} <!-- e.g., OPEN|IN_PROGRESS|CLOSED -->
|
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||||
{{#ROType}}<rr:ROType>
|
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
|
||||||
{{ROType}}</rr:ROType>{{/ROType}} <!-- e.g., INSURANCE|CUSTOMER_PAY -->
|
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
|
||||||
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
</Customer>
|
||||||
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
{{/Customer}}
|
||||||
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
{{#Vehicle}}
|
||||||
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
<ServiceVehicle>
|
||||||
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
|
||||||
{{#LocationCode}}<rr:LocationCode>{{LocationCode}}</rr:LocationCode>{{/LocationCode}}
|
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
|
||||||
{{#PurchaseOrder}}<rr:PurchaseOrder>{{PurchaseOrder}}</rr:PurchaseOrder>{{/PurchaseOrder}}
|
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||||
|
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||||
<!-- Optional customer patch -->
|
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||||
{{#Customer}}
|
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||||
<rr:Customer>
|
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
</ServiceVehicle>
|
||||||
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
{{/Vehicle}}
|
||||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
{{#AddedJobLines}}
|
||||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
<AddedJobLines>
|
||||||
</rr:Customer>
|
{{#Items}}
|
||||||
{{/Customer}}
|
<JobLine>
|
||||||
|
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||||
<!-- Optional vehicle patch -->
|
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||||
{{#Vehicle}}
|
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||||
<rr:Vehicle>
|
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
{{#LineType}}<LineType>
|
||||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
{{LineType}}</LineType>{{/LineType}}
|
||||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
{{#Category}}<Category>
|
||||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
{{Category}}</Category>{{/Category}}
|
||||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||||
</rr:Vehicle>
|
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||||
{{/Vehicle}}
|
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||||
|
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||||
<!-- Line changes: use one of AddedJobLines / UpdatedJobLines / RemovedJobLines -->
|
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||||
{{#AddedJobLines}}
|
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
|
||||||
<rr:AddedJobLine>
|
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
{{#Taxes}}
|
||||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
<Taxes>
|
||||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
{{#Items}}
|
||||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
<Tax>
|
||||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
<Code>{{Code}}</Code>
|
||||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
<Amount>{{Amount}}</Amount>
|
||||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
|
||||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
</Tax>
|
||||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
{{/Items}}
|
||||||
{{#PayType}}<rr:PayType>
|
</Taxes>
|
||||||
{{PayType}}</rr:PayType>{{/PayType}} <!-- CUST|INS|WARR|INT -->
|
{{/Taxes}}
|
||||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
{{#PayType}}<PayType>
|
||||||
</rr:AddedJobLine>
|
{{PayType}}</PayType>{{/PayType}}
|
||||||
{{/AddedJobLines}}
|
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||||
|
</JobLine>
|
||||||
{{#UpdatedJobLines}}
|
{{/Items}}
|
||||||
<rr:UpdatedJobLine>
|
</AddedJobLines>
|
||||||
<!-- Identify the existing line either by Sequence or LineId -->
|
{{/AddedJobLines}}
|
||||||
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
{{#UpdatedJobLines}}
|
||||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
<UpdatedJobLines>
|
||||||
{{#ChangeType}}<rr:ChangeType>
|
{{#Items}}
|
||||||
{{ChangeType}}</rr:ChangeType>{{/ChangeType}} <!-- PRICE|QTY|DESC|OPCODE|PAYTYPE -->
|
<JobLine>
|
||||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
{{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
|
||||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
{{#ChangeType}}<ChangeType>
|
||||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
{{ChangeType}}</ChangeType>{{/ChangeType}}
|
||||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||||
{{#PayType}}<rr:PayType>{{PayType}}</rr:PayType>{{/PayType}}
|
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||||
</rr:UpdatedJobLine>
|
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||||
{{/UpdatedJobLines}}
|
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||||
|
{{#PayType}}<PayType>{{PayType}}</PayType>{{/PayType}}
|
||||||
{{#RemovedJobLines}}
|
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||||
<rr:RemovedJobLine>
|
</JobLine>
|
||||||
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
{{/Items}}
|
||||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
</UpdatedJobLines>
|
||||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
{{/UpdatedJobLines}}
|
||||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
{{#RemovedJobLines}}
|
||||||
</rr:RemovedJobLine>
|
<RemovedJobLines>
|
||||||
{{/RemovedJobLines}}
|
{{#Items}}
|
||||||
|
<JobLine>
|
||||||
<!-- Totals (optional patch if RR expects header totals on change) -->
|
{{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
|
||||||
{{#Totals}}
|
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||||
<rr:Totals>
|
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||||
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||||
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
</JobLine>
|
||||||
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
{{/Items}}
|
||||||
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
</RemovedJobLines>
|
||||||
{{#GrandTotal}}<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>{{/GrandTotal}}
|
{{/RemovedJobLines}}
|
||||||
</rr:Totals>
|
{{#Totals}}
|
||||||
{{/Totals}}
|
<Totals>
|
||||||
|
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
|
||||||
<!-- Insurance (optional update) -->
|
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
|
||||||
{{#Insurance}}
|
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
|
||||||
<rr:Insurance>
|
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
|
||||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
{{#GrandTotal}}<GrandTotal>{{GrandTotal}}</GrandTotal>{{/GrandTotal}}
|
||||||
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
|
</Totals>
|
||||||
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
|
{{/Totals}}
|
||||||
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
|
{{#Insurance}}
|
||||||
</rr:Insurance>
|
<Insurance>
|
||||||
{{/Insurance}}
|
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||||
|
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
|
||||||
<!-- Notes (append or replace depending on RR semantics) -->
|
{{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
|
||||||
{{#Notes}}
|
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
|
||||||
<rr:Notes>
|
</Insurance>
|
||||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
{{/Insurance}}
|
||||||
</rr:Notes>
|
{{#Notes}}
|
||||||
{{/Notes}}
|
<Notes>
|
||||||
</rr:RepairOrder>
|
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||||
</rr:RepairOrderChgRq>
|
</Notes>
|
||||||
|
{{/Notes}}
|
||||||
|
</RepairOrder>
|
||||||
|
</BSMRepairOrderChgReq>
|
||||||
|
</rey_RomeUpdateBSMRepairOrderReq>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<!-- _EnvelopeFooter.xml -->
|
|
||||||
<rr:Footer xmlns:rr="http://reynoldsandrey.com/">
|
|
||||||
<!-- Optional system trace or session info -->
|
|
||||||
{{#SessionId}}
|
|
||||||
<rr:SessionId>{{SessionId}}</rr:SessionId>
|
|
||||||
{{/SessionId}}
|
|
||||||
|
|
||||||
{{#Checksum}}
|
|
||||||
<rr:Checksum>{{Checksum}}</rr:Checksum>
|
|
||||||
{{/Checksum}}
|
|
||||||
|
|
||||||
{{#Timestamp}}
|
|
||||||
<rr:Timestamp>{{Timestamp}}</rr:Timestamp>
|
|
||||||
{{/Timestamp}}
|
|
||||||
|
|
||||||
<!-- Placeholder for any future required footer elements -->
|
|
||||||
</rr:Footer>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!-- _EnvelopeHeader.xml -->
|
|
||||||
<rr:Authentication xmlns:rr="http://reynoldsandrey.com/">
|
|
||||||
<!-- Required system identifier -->
|
|
||||||
{{#PPSysId}}
|
|
||||||
<rr:PPSysId>{{PPSysId}}</rr:PPSysId>
|
|
||||||
{{/PPSysId}}
|
|
||||||
|
|
||||||
<!-- Dealer / Store / Branch numbers (optional but strongly recommended) -->
|
|
||||||
{{#DealerNumber}}
|
|
||||||
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
|
|
||||||
{{/DealerNumber}}
|
|
||||||
|
|
||||||
{{#StoreNumber}}
|
|
||||||
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
|
|
||||||
{{/StoreNumber}}
|
|
||||||
|
|
||||||
{{#BranchNumber}}
|
|
||||||
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
|
|
||||||
{{/BranchNumber}}
|
|
||||||
|
|
||||||
<!-- Basic user credentials (always required) -->
|
|
||||||
<rr:Username>{{Username}}</rr:Username>
|
|
||||||
<rr:Password>{{Password}}</rr:Password>
|
|
||||||
|
|
||||||
<!-- Optional custom correlation token -->
|
|
||||||
{{#CorrelationId}}
|
|
||||||
<rr:CorrelationId>{{CorrelationId}}</rr:CorrelationId>
|
|
||||||
{{/CorrelationId}}
|
|
||||||
</rr:Authentication>
|
|
||||||
@@ -3,7 +3,24 @@ const FortellisLogger = require("../fortellis/fortellis-logger");
|
|||||||
const RRLogger = require("../rr/rr-logger");
|
const RRLogger = require("../rr/rr-logger");
|
||||||
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
const { RRJobExport, RRSelectedCustomer } = require("../rr/rr-job-export");
|
const { exportJobToRome } = require("../rr/rr-job-export");
|
||||||
|
const lookupApi = require("../rr/rr-lookup");
|
||||||
|
const RRCalculateAllocations = require("../rr/rr-calculate-allocations");
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -340,75 +357,35 @@ const redisSocketEvents = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const registerRREvents = (socket) => {
|
const registerRREvents = (socket) => {
|
||||||
socket.on("rr-export-job", async ({ jobid, txEnvelope }) => {
|
// Orchestrated Export (Customer → Vehicle → Repair Order)
|
||||||
|
socket.on("rr-export-job", async (payload = {}) => {
|
||||||
try {
|
try {
|
||||||
await RRJobExport({
|
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, config, options }
|
||||||
socket,
|
// Prefer direct job/config, otherwise try txEnvelope.{job,config}
|
||||||
redisHelpers: {
|
const job = payload.job || payload.txEnvelope?.job;
|
||||||
setSessionData,
|
const options = payload.options || payload.txEnvelope?.options || {};
|
||||||
getSessionData,
|
const cfg = resolveRRConfigFrom(payload);
|
||||||
addUserSocketMapping,
|
|
||||||
removeUserSocketMapping,
|
if (!job) {
|
||||||
refreshUserSocketTTL,
|
RRLogger(socket, "error", "RR export missing job payload");
|
||||||
getUserSocketMappingByBodyshop,
|
return;
|
||||||
setSessionTransactionData,
|
}
|
||||||
getSessionTransactionData,
|
|
||||||
clearSessionTransactionData
|
const result = await exportJobToRome(socket, job, cfg, options);
|
||||||
},
|
// Broadcast to bodyshop room for UI to pick up
|
||||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
const room = getBodyshopRoom(socket.bodyshopId);
|
||||||
jobid,
|
io.to(room).emit("rr-export-job:result", { jobid: job.id, result });
|
||||||
txEnvelope
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
RRLogger(socket, "error", `Error during RR export: ${error.message}`);
|
RRLogger(socket, "error", `Error during RR export: ${error.message}`);
|
||||||
logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack });
|
logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId }) => {
|
// Combined search
|
||||||
|
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
||||||
try {
|
try {
|
||||||
await RRSelectedCustomer({
|
const cfg = resolveRRConfigFrom({}); // if you want per-call overrides, pass them in the payload and merge here
|
||||||
socket,
|
const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
|
||||||
redisHelpers: {
|
|
||||||
setSessionData,
|
|
||||||
getSessionData,
|
|
||||||
addUserSocketMapping,
|
|
||||||
removeUserSocketMapping,
|
|
||||||
refreshUserSocketTTL,
|
|
||||||
getUserSocketMappingByBodyshop,
|
|
||||||
setSessionTransactionData,
|
|
||||||
getSessionTransactionData,
|
|
||||||
clearSessionTransactionData
|
|
||||||
},
|
|
||||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
|
||||||
jobid,
|
|
||||||
selectedCustomerId
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`);
|
|
||||||
logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("rr-calculate-allocations", async (jobid, callback) => {
|
|
||||||
try {
|
|
||||||
const allocations = await CdkCalculateAllocations(socket, jobid);
|
|
||||||
callback(allocations);
|
|
||||||
} catch (error) {
|
|
||||||
RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`);
|
|
||||||
logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("rr-lookup-combined", async ({ jobid, params }, cb) => {
|
|
||||||
try {
|
|
||||||
const { RrCombinedSearch } = require("../rr/rr-lookup");
|
|
||||||
const data = await RrCombinedSearch({
|
|
||||||
socket,
|
|
||||||
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
|
|
||||||
jobid,
|
|
||||||
params
|
|
||||||
});
|
|
||||||
cb?.(data);
|
cb?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RRLogger(socket, "error", `RR combined lookup error: ${e.message}`);
|
RRLogger(socket, "error", `RR combined lookup error: ${e.message}`);
|
||||||
@@ -416,15 +393,11 @@ const redisSocketEvents = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("rr-get-advisors", async ({ jobid, params }, cb) => {
|
// Get Advisors
|
||||||
|
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
|
||||||
try {
|
try {
|
||||||
const { RrGetAdvisors } = require("../rr/rr-lookup");
|
const cfg = resolveRRConfigFrom({});
|
||||||
const data = await RrGetAdvisors({
|
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
|
||||||
socket,
|
|
||||||
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
|
|
||||||
jobid,
|
|
||||||
params
|
|
||||||
});
|
|
||||||
cb?.(data);
|
cb?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RRLogger(socket, "error", `RR get advisors error: ${e.message}`);
|
RRLogger(socket, "error", `RR get advisors error: ${e.message}`);
|
||||||
@@ -432,23 +405,46 @@ const redisSocketEvents = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("rr-get-parts", async ({ jobid, params }, cb) => {
|
// Get Parts
|
||||||
|
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
|
||||||
try {
|
try {
|
||||||
const { RrGetParts } = require("../rr/rr-lookup");
|
const cfg = resolveRRConfigFrom({});
|
||||||
const data = await RrGetParts({
|
const data = await lookupApi.getParts(socket, params || {}, cfg);
|
||||||
socket,
|
|
||||||
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
|
|
||||||
jobid,
|
|
||||||
params
|
|
||||||
});
|
|
||||||
cb?.(data);
|
cb?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RRLogger(socket, "error", `RR get parts error: ${e.message}`);
|
RRLogger(socket, "error", `RR get parts error: ${e.message}`);
|
||||||
cb?.(null);
|
cb?.(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
// (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
|
||||||
|
});
|
||||||
|
// If later you add support, call your implementation here.
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`);
|
||||||
|
logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate allocations (unchanged — CDK utility)
|
||||||
|
socket.on("rr-calculate-allocations", async (jobid, callback) => {
|
||||||
|
try {
|
||||||
|
const allocations = await RRCalculateAllocations(socket, jobid);
|
||||||
|
callback(allocations);
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`);
|
||||||
|
logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
// Call Handlers
|
// Call Handlers
|
||||||
registerRoomAndBroadcastEvents(socket);
|
registerRoomAndBroadcastEvents(socket);
|
||||||
registerUpdateEvents(socket);
|
registerUpdateEvents(socket);
|
||||||
|
|||||||
Reference in New Issue
Block a user