feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -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
|
||||
* @description Central constants and configuration for Reynolds & Reynolds (R&R) integration.
|
||||
* 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.
|
||||
* STAR-only constants for Reynolds & Reynolds (Rome/RCI)
|
||||
* Used by rr-helpers.js to build and send SOAP requests.
|
||||
*/
|
||||
|
||||
const RR_TIMEOUT_MS = 30000; // 30-second SOAP call timeout
|
||||
const RR_NAMESPACE_URI = "http://reynoldsandrey.com/";
|
||||
const RR_DEFAULT_MAX_RESULTS = 25;
|
||||
|
||||
/**
|
||||
* Maps internal operation names to Reynolds & Reynolds SOAP actions.
|
||||
* soapAction is sent as the SOAPAction header; URL selection happens in rr-helpers.
|
||||
*/
|
||||
const RR_ACTIONS = {
|
||||
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
|
||||
exports.RR_NS = Object.freeze({
|
||||
SOAP_ENV: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
SOAP_ENC: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
XSD: "http://www.w3.org/2001/XMLSchema",
|
||||
XSI: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
WSSE: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
|
||||
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",
|
||||
STAR_BUSINESS: "http://www.starstandards.org/STAR"
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
RR_TIMEOUT_MS,
|
||||
RR_NAMESPACE_URI,
|
||||
RR_DEFAULT_MAX_RESULTS,
|
||||
RR_ACTIONS,
|
||||
RR_SOAP_HEADERS,
|
||||
buildSoapEnvelope,
|
||||
getBaseRRConfig
|
||||
const RR_STAR_SOAP_ACTION = "http://www.starstandards.org/webservices/2005/10/transport/ProcessMessage";
|
||||
exports.RR_SOAP_ACTION = RR_STAR_SOAP_ACTION;
|
||||
|
||||
const RR_SOAP_HEADERS = {
|
||||
"Content-Type": "text/xml; charset=utf-8",
|
||||
SOAPAction: RR_STAR_SOAP_ACTION
|
||||
};
|
||||
|
||||
// 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
|
||||
* @description Reynolds & Reynolds (Rome) Customer Insert/Update integration.
|
||||
* Builds request payloads using rr-mappers and executes via rr-helpers.
|
||||
* All dealer-specific data (DealerNumber, LocationId, etc.) is read from the DB (bodyshop.rr_configuration).
|
||||
* @description Rome (Reynolds & Reynolds) Customer Insert / Update integration.
|
||||
* Maps internal customer objects to Rome XML schemas and executes RCI calls.
|
||||
*/
|
||||
|
||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
||||
const { assertRrOk } = require("./rr-error");
|
||||
const { MakeRRCall } = require("./rr-helpers");
|
||||
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { client } = require("../graphql-client/graphql-client");
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
|
||||
/**
|
||||
* Fetch rr_configuration for the current bodyshop directly from DB.
|
||||
* This ensures we always have the latest Dealer/Location mapping.
|
||||
* Insert a new customer into Rome.
|
||||
* @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 {
|
||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||
const config = result?.bodyshops_by_pk?.rr_configuration || null;
|
||||
RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
|
||||
}
|
||||
const data = mapCustomerInsert(customer, bodyshopConfig);
|
||||
|
||||
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config);
|
||||
return config;
|
||||
const resultXml = await MakeRRCall({
|
||||
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) {
|
||||
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, {
|
||||
bodyshopId,
|
||||
RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
throw new RrApiError(`RR InsertCustomer failed: ${error.message}`, "INSERT_CUSTOMER_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CUSTOMER INSERT (Rome Customer Insert Specification 1.2)
|
||||
* Creates a new customer record in the DMS.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {object} options.socket - socket.io connection or express req
|
||||
* @param {object} options.redisHelpers
|
||||
* @param {object} options.JobData - normalized job record
|
||||
* Update an existing customer in Rome.
|
||||
* @param {Socket} socket
|
||||
* @param {Object} customer
|
||||
* @param {Object} bodyshopConfig
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function RrCustomerInsert({ socket, redisHelpers, JobData }) {
|
||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||
const logger = socket?.logger || console;
|
||||
async function updateCustomer(socket, customer, bodyshopConfig) {
|
||||
const action = "UpdateCustomer";
|
||||
const template = "UpdateCustomer";
|
||||
|
||||
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 vars = mapCustomerInsert(JobData, dealerConfig);
|
||||
|
||||
const data = await MakeRRCall({
|
||||
action: RRActions.CreateCustomer, // resolves to SOAPAction + URL
|
||||
body: { template: "InsertCustomer", data: vars }, // render server/rr/xml-templates/InsertCustomer.xml
|
||||
redisHelpers,
|
||||
const resultXml = await MakeRRCall({
|
||||
action,
|
||||
body: { template, data },
|
||||
socket,
|
||||
jobid: JobData.id
|
||||
dealerConfig: bodyshopConfig,
|
||||
jobid: customer.id
|
||||
});
|
||||
|
||||
const response = assertRrOk(data, { apiName: "RR Create Customer" });
|
||||
RRLogger(socket, "debug", "RR Customer Insert success", {
|
||||
jobid: JobData?.id,
|
||||
dealer: dealerConfig?.dealerCode || dealerConfig?.dealer_code
|
||||
});
|
||||
RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
|
||||
|
||||
return response;
|
||||
return {
|
||||
success: true,
|
||||
dms: "Rome",
|
||||
action,
|
||||
customerId: customer.id,
|
||||
xml: resultXml
|
||||
};
|
||||
} catch (error) {
|
||||
RRLogger(socket, "error", `RR Customer Insert failed: ${error.message}`, { jobid: JobData?.id });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
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;
|
||||
throw new RrApiError(`RR UpdateCustomer failed: ${error.message}`, "UPDATE_CUSTOMER_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RrCustomerInsert,
|
||||
RrCustomerUpdate,
|
||||
getDealerConfigFromDB
|
||||
insertCustomer,
|
||||
updateCustomer
|
||||
};
|
||||
|
||||
@@ -1,103 +1,48 @@
|
||||
/**
|
||||
* @file rr-error.js
|
||||
* @description Centralized error class and assertion logic for Reynolds & Reynolds API calls.
|
||||
* Provides consistent handling across all RR modules (customer, repair order, lookups, etc.)
|
||||
* @description Custom error types for the Reynolds & Reynolds (Rome) integration.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom Error type for RR API responses
|
||||
* Base RR API Error class — always structured with a message and a code.
|
||||
*/
|
||||
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);
|
||||
this.name = "RrApiError";
|
||||
this.reqId = reqId || null;
|
||||
this.url = url || null;
|
||||
this.apiName = apiName || null;
|
||||
this.errorData = errorData || null;
|
||||
this.status = status || null;
|
||||
this.statusText = statusText || null;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
details: this.details
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a Reynolds & Reynolds response is successful.
|
||||
*
|
||||
* 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.
|
||||
* Helper to normalize thrown errors into a consistent RrApiError instance.
|
||||
*/
|
||||
function assertRrOk(data, { apiName = "RR API Call", allowEmpty = false } = {}) {
|
||||
if (!data && !allowEmpty) {
|
||||
throw new RrApiError(`${apiName} returned no data`, { apiName });
|
||||
}
|
||||
|
||||
// Normalize envelope
|
||||
const response =
|
||||
data?.Envelope?.Body?.Response ||
|
||||
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
|
||||
);
|
||||
function toRrError(err, defaultCode = "RR_ERROR") {
|
||||
if (!err) return new RrApiError("Unknown RR error", defaultCode);
|
||||
if (err instanceof RrApiError) return err;
|
||||
if (typeof err === "string") return new RrApiError(err, defaultCode);
|
||||
const msg = err.message || "Unspecified RR error";
|
||||
const code = err.code || defaultCode;
|
||||
const details = err.details || {};
|
||||
return new RrApiError(msg, code, details);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RrApiError,
|
||||
assertRrOk,
|
||||
extractRrResponseData
|
||||
toRrError
|
||||
};
|
||||
|
||||
@@ -1,257 +1,319 @@
|
||||
/**
|
||||
* @file rr-helpers.js
|
||||
* @description Core helper functions for Reynolds & Reynolds integration.
|
||||
* Handles XML rendering, SOAP communication, and configuration merging.
|
||||
* STAR-only SOAP transport + template rendering for Reynolds & Reynolds (Rome/RCI).
|
||||
* - Renders Mustache STAR business templates (rey_*Req rooted with STAR ns)
|
||||
* - 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 path = require("path");
|
||||
const mustache = require("mustache");
|
||||
const axios = require("axios");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants");
|
||||
const mustache = require("mustache");
|
||||
const { XMLParser } = require("fast-xml-parser");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { client } = require("../graphql-client/graphql-client");
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* Configuration
|
||||
* ----------------------------------------------------------------------------------------------*/
|
||||
const { RR_ACTIONS, RR_SOAP_HEADERS, RR_STAR_SOAP_ACTION, RR_NS, getBaseRRConfig } = require("./rr-constants");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
const xmlFormatter = require("xml-formatter");
|
||||
|
||||
/**
|
||||
* Loads the rr_configuration JSON for a given bodyshop directly from the database.
|
||||
* Dealer-level settings only. Platform/secret defaults come from getBaseRRConfig().
|
||||
* @param {string} bodyshopId
|
||||
* @returns {Promise<object>} rr_configuration
|
||||
* Remove XML decl, collapse inter-tag whitespace, strip empty lines,
|
||||
* then pretty-print. Safe for XML because we only touch whitespace
|
||||
* BETWEEN tags, not inside text nodes.
|
||||
/**
|
||||
* Collapse Mustache-induced whitespace and pretty print.
|
||||
* - strips XML decl (inner)
|
||||
* - removes lines that are only whitespace
|
||||
* - collapses inter-tag whitespace
|
||||
* - formats with consistent indentation
|
||||
*/
|
||||
async function getDealerConfig(bodyshopId) {
|
||||
try {
|
||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||
const cfg = result?.bodyshops_by_pk?.rr_configuration || {};
|
||||
return cfg;
|
||||
} catch (err) {
|
||||
console.error(`[RR] Failed to load rr_configuration for bodyshop ${bodyshopId}:`, err.message);
|
||||
return {};
|
||||
}
|
||||
function prettyPrintXml(xml) {
|
||||
let s = xml;
|
||||
|
||||
// strip any inner XML declaration
|
||||
s = s.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
|
||||
// remove lines that are only whitespace
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to retrieve combined configuration (env + dealer) for calls.
|
||||
* 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
|
||||
* @returns {Promise<object>} configuration
|
||||
*/
|
||||
async function resolveRRConfig(socket) {
|
||||
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||
const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
return { ...getBaseRRConfig(), ...dealerCfg };
|
||||
// ---------- Public action map (compat with rr-test.js) ----------
|
||||
const RRActions = Object.fromEntries(Object.entries(RR_ACTIONS).map(([k]) => [k, { action: k }]));
|
||||
|
||||
// ---------- Template cache ----------
|
||||
const templateCache = new Map();
|
||||
|
||||
async function loadTemplate(templateName) {
|
||||
if (templateCache.has(templateName)) return templateCache.get(templateName);
|
||||
const filePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||
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) {
|
||||
const templatePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||
const xmlTemplate = await fs.readFile(templatePath, "utf8");
|
||||
return mustache.render(xmlTemplate, data);
|
||||
const tpl = await loadTemplate(templateName);
|
||||
// Render and strip any XML declaration to keep a single root element for the BOD
|
||||
const rendered = mustache.render(tpl, data || {});
|
||||
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SOAP envelope with a rendered header + body.
|
||||
* Header comes from xml-templates/_EnvelopeHeader.xml.
|
||||
* @param {string} renderedBodyXml
|
||||
* @param {object} headerVars - values for header mustache
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) {
|
||||
const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars);
|
||||
// ---------- Config resolution (STAR only) ----------
|
||||
async function resolveRRConfig(_socket, bodyshopConfig) {
|
||||
const envCfg = getBaseRRConfig();
|
||||
|
||||
return `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||
if (bodyshopConfig && typeof bodyshopConfig === "object") {
|
||||
return {
|
||||
...envCfg,
|
||||
baseUrl: bodyshopConfig.baseUrl || envCfg.baseUrl,
|
||||
username: bodyshopConfig.username || envCfg.username,
|
||||
password: bodyshopConfig.password || envCfg.password,
|
||||
ppsysId: bodyshopConfig.ppsysId || envCfg.ppsysId,
|
||||
dealerNumber: bodyshopConfig.dealer_number || envCfg.dealerNumber,
|
||||
storeNumber: bodyshopConfig.store_number || envCfg.storeNumber,
|
||||
branchNumber: bodyshopConfig.branch_number || envCfg.branchNumber,
|
||||
wssePasswordType: bodyshopConfig.wssePasswordType || envCfg.wssePasswordType || "Text",
|
||||
timeout: envCfg.timeout
|
||||
};
|
||||
}
|
||||
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>
|
||||
${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: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:Envelope>
|
||||
`.trim();
|
||||
</soapenv:Envelope>`;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
*/
|
||||
// ---------- Main transport (STAR only) ----------
|
||||
async function MakeRRCall({
|
||||
action,
|
||||
body,
|
||||
socket,
|
||||
// redisHelpers,
|
||||
jobid,
|
||||
dealerConfig,
|
||||
retries = 1
|
||||
dealerConfig, // optional per-shop overrides
|
||||
retries = 1,
|
||||
jobid
|
||||
}) {
|
||||
const correlationId = uuidv4();
|
||||
|
||||
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 || {});
|
||||
if (!action || !RR_ACTIONS[action]) {
|
||||
throw new Error(`Invalid RR action: ${action}`);
|
||||
}
|
||||
|
||||
// Build header vars (from env + rr_configuration)
|
||||
const headerVars = {
|
||||
PPSysId: effectiveConfig.ppsysid || process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID,
|
||||
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
|
||||
};
|
||||
const cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
|
||||
const baseUrl = cfg.baseUrl;
|
||||
if (!baseUrl) throw new Error("Missing RR base URL");
|
||||
|
||||
// Build full SOAP envelope with proper header
|
||||
const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars);
|
||||
// Render STAR business body
|
||||
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,
|
||||
url,
|
||||
correlationId
|
||||
mode: "STAR"
|
||||
});
|
||||
|
||||
const headers = {
|
||||
...RR_SOAP_HEADERS,
|
||||
SOAPAction: soapAction,
|
||||
"Content-Type": "text/xml; charset=utf-8",
|
||||
"X-Request-Id": correlationId
|
||||
};
|
||||
try {
|
||||
const { data: responseXml } = await axios.post(baseUrl, formattedEnvelope, {
|
||||
headers,
|
||||
timeout: cfg.timeout
|
||||
// Some RCI tenants require Basic in addition to WSSE
|
||||
// auth: { username: cfg.username, password: cfg.password }
|
||||
});
|
||||
|
||||
let attempt = 0;
|
||||
while (attempt <= retries) {
|
||||
attempt += 1;
|
||||
try {
|
||||
const response = await axios.post(url, soapEnvelope, {
|
||||
headers,
|
||||
timeout: effectiveConfig.timeout || 30000,
|
||||
responseType: "text",
|
||||
validateStatus: () => true
|
||||
const parsed = parseRRResponse(responseXml);
|
||||
|
||||
if (!parsed.success) {
|
||||
RRLogger(socket, "error", `RR ${action} failed`, {
|
||||
code: parsed.code,
|
||||
message: parsed.message
|
||||
});
|
||||
|
||||
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;
|
||||
throw new RrApiError(parsed.message || `RR ${action} failed`, parsed.code || "RR_ERROR");
|
||||
}
|
||||
|
||||
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 = {
|
||||
MakeRRCall,
|
||||
getDealerConfig,
|
||||
renderXmlTemplate,
|
||||
resolveRRConfig,
|
||||
parseRRResponse,
|
||||
buildStarEnvelope,
|
||||
RRActions
|
||||
};
|
||||
|
||||
@@ -1,133 +1,158 @@
|
||||
/**
|
||||
* @file rr-job-export.js
|
||||
* @description Orchestrates the full Reynolds & Reynolds DMS export flow.
|
||||
* Creates/updates customers, vehicles, and repair orders according to Rome specs.
|
||||
* @description End-to-end export of a Hasura "job" to Reynolds & Reynolds (Rome).
|
||||
* 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 { 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.
|
||||
* Follows the "Rome Insert Service Vehicle Interface Specification" via SOAP/XML.
|
||||
* Decide if we should CREATE or UPDATE an entity in Rome based on external IDs
|
||||
*/
|
||||
async function RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig }) {
|
||||
try {
|
||||
RRLogger(socket, "info", "RR Insert Service Vehicle started", { jobid: JobData?.id });
|
||||
function decideAction({ customer, vehicle, job }) {
|
||||
const hasCustId = !!(customer?.external_id || customer?.rr_customer_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
|
||||
const variables = mapServiceVehicleInsert(JobData, dealerConfig);
|
||||
|
||||
const xml = await MakeRRCall({
|
||||
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;
|
||||
}
|
||||
return {
|
||||
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
|
||||
repairOrderAction: hasRoId ? "update" : "create"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Full DMS export sequence for Reynolds & Reynolds.
|
||||
*
|
||||
* 1. Ensure customer exists (insert or update)
|
||||
* 2. Ensure vehicle exists/linked
|
||||
* 3. Create or update repair order
|
||||
* Normalize a stage result to a consistent structure.
|
||||
*/
|
||||
async function ExportJobToRR({ socket, redisHelpers, JobData }) {
|
||||
const jobid = JobData?.id;
|
||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||
function stageOk(name, extra = {}) {
|
||||
return { stage: name, success: true, ...extra };
|
||||
}
|
||||
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 {
|
||||
// Pull dealer-level overrides once (DB), env/platform secrets come from rr-helpers internally.
|
||||
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
|
||||
//
|
||||
// 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
|
||||
});
|
||||
if (actions.customerAction === "insert") {
|
||||
const res = await customerApi.insertCustomer(socket, customer, bodyshopConfig);
|
||||
stages.push(stageOk("customer.insert"));
|
||||
summary.customer_xml = res.xml;
|
||||
} 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) {
|
||||
RRLogger(socket, "error", `RR job export failed: ${error.message}`, { jobid });
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
};
|
||||
stages.push(stageFail(`customer.${actions.customerAction}`, error));
|
||||
RRLogger(socket, "error", `RR customer ${actions.customerAction} failed`, {
|
||||
jobid: job?.id,
|
||||
error: error.message
|
||||
});
|
||||
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 = {
|
||||
ExportJobToRR,
|
||||
RrServiceVehicleInsert
|
||||
exportJobToRome
|
||||
};
|
||||
|
||||
@@ -1,50 +1,55 @@
|
||||
/**
|
||||
* @file rr-logger.js
|
||||
* @description Centralized logger for Reynolds & Reynolds (RR) integrations.
|
||||
* Emits logs to CloudWatch via logger util, and back to client sockets for live visibility.
|
||||
* @description Structured logger for Reynolds & Reynolds (Rome) integration.
|
||||
* 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.
|
||||
*
|
||||
* @param {object} socket - The socket or Express request (both supported).
|
||||
* @param {"debug"|"info"|"warn"|"error"} level - Log level.
|
||||
* @param {string} message - Human-readable log message.
|
||||
* @param {object} [txnDetails] - Optional additional details (payloads, responses, etc.)
|
||||
* @typedef {Object} LogContext
|
||||
* @property {string} [jobid]
|
||||
* @property {string} [action]
|
||||
* @property {string} [stage]
|
||||
* @property {string} [endpoint]
|
||||
* @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 =
|
||||
socket?.user?.email || socket?.request?.user?.email || socket?.handshake?.auth?.email || "unknown@user";
|
||||
/**
|
||||
* Emit a structured log event to console, Socket.IO, or upstream logger.
|
||||
* @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;
|
||||
|
||||
// Main logging entry (to CloudWatch / file)
|
||||
logger.log("rr-log-event", levelUpper, userEmail, jobid, {
|
||||
wsmessage: message,
|
||||
txnDetails
|
||||
});
|
||||
|
||||
// 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);
|
||||
// Console log (stdout/stderr)
|
||||
const serialized = `[RR] ${logEvent.timestamp} [${level.toUpperCase()}] ${message}`;
|
||||
if (level === "error" || level === "warn") {
|
||||
console.error(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
|
||||
} else {
|
||||
console.log(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -1,143 +1,136 @@
|
||||
/**
|
||||
* @file rr-lookup.js
|
||||
* @description Reynolds & Reynolds lookup operations
|
||||
* (Combined Search, Get Advisors, Get Parts) via SOAP/XML templates.
|
||||
* @description Rome (Reynolds & Reynolds) lookup operations — Advisors, Parts, and CombinedSearch
|
||||
*/
|
||||
|
||||
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers");
|
||||
const { assertRrOkXml, extractRrResponseData } = require("./rr-error");
|
||||
const { mapCombinedSearchVars, mapGetAdvisorsVars, mapGetPartsVars } = require("./rr-mappers");
|
||||
const { MakeRRCall, parseRRResponse } = require("./rr-helpers");
|
||||
const { mapAdvisorLookup, mapPartsLookup, mapCombinedSearch } = require("./rr-mappers");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
|
||||
/**
|
||||
* Combined Search
|
||||
* 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"]]
|
||||
* Get a list of service advisors from Rome.
|
||||
*/
|
||||
async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) {
|
||||
async function getAdvisors(socket, criteria = {}, bodyshopConfig) {
|
||||
const action = "GetAdvisors";
|
||||
const template = "GetAdvisors";
|
||||
|
||||
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 dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
|
||||
// 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,
|
||||
const resultXml = await MakeRRCall({
|
||||
action,
|
||||
body: { template, data },
|
||||
socket,
|
||||
jobid
|
||||
dealerConfig: bodyshopConfig
|
||||
});
|
||||
|
||||
// Validate + normalize
|
||||
const ok = assertRrOkXml(xml, { apiName: "RR Combined Search", allowEmpty: true });
|
||||
const normalized = extractRrResponseData(ok, { action: "CombinedSearch" });
|
||||
const parsed = parseRRResponse(resultXml);
|
||||
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
|
||||
|
||||
RRLogger(socket, "debug", "RR Combined Search complete", {
|
||||
jobid,
|
||||
count: Array.isArray(normalized) ? normalized.length : 0
|
||||
});
|
||||
return normalized;
|
||||
const advisors = parsed.parsed?.Advisors?.Advisor || parsed.parsed?.AdvisorList?.Advisor || [];
|
||||
const advisorList = Array.isArray(advisors) ? advisors : [advisors];
|
||||
|
||||
RRLogger(socket, "debug", `${action} lookup returned ${advisorList.length} advisors`);
|
||||
return { success: true, dms: "Rome", action, advisors: advisorList };
|
||||
} catch (error) {
|
||||
RRLogger(socket, "error", `RR Combined Search failed: ${error.message}`, { jobid });
|
||||
throw error;
|
||||
RRLogger(socket, "error", `Error in ${action} lookup`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_ADVISORS_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Advisors
|
||||
* 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]
|
||||
* Get parts information from Rome.
|
||||
*/
|
||||
async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) {
|
||||
async function getParts(socket, criteria = {}, bodyshopConfig) {
|
||||
const action = "GetParts";
|
||||
const template = "GetParts";
|
||||
|
||||
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 dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
|
||||
// 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,
|
||||
const resultXml = await MakeRRCall({
|
||||
action,
|
||||
body: { template, data },
|
||||
socket,
|
||||
jobid
|
||||
dealerConfig: bodyshopConfig
|
||||
});
|
||||
|
||||
const ok = assertRrOkXml(xml, { apiName: "RR Get Advisors", allowEmpty: true });
|
||||
const normalized = extractRrResponseData(ok, { action: "GetAdvisors" });
|
||||
const parsed = parseRRResponse(resultXml);
|
||||
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
|
||||
|
||||
RRLogger(socket, "debug", "RR Get Advisors complete", {
|
||||
jobid,
|
||||
count: Array.isArray(normalized) ? normalized.length : 0
|
||||
});
|
||||
return normalized;
|
||||
const parts = parsed.parsed?.Parts?.Part || parsed.parsed?.PartList?.Part || [];
|
||||
const partList = Array.isArray(parts) ? parts : [parts];
|
||||
|
||||
RRLogger(socket, "debug", `${action} lookup returned ${partList.length} parts`);
|
||||
return { success: true, dms: "Rome", action, parts: partList };
|
||||
} catch (error) {
|
||||
RRLogger(socket, "error", `RR Get Advisors failed: ${error.message}`, { jobid });
|
||||
throw error;
|
||||
RRLogger(socket, "error", `Error in ${action} lookup`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_PARTS_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Parts
|
||||
* Maps to "Get Part Specification" (Rome)
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {object} options.socket
|
||||
* @param {object} options.redisHelpers
|
||||
* @param {string} options.jobid
|
||||
* @param {Array<[string, string]>} [options.params]
|
||||
* Perform a combined customer / vehicle / company search.
|
||||
* Equivalent to Rome CombinedSearchRq / Resp.
|
||||
* @param {Socket} socket
|
||||
* @param {Object} criteria - { VIN, LicensePlate, CustomerName, Phone, Email }
|
||||
* @param {Object} bodyshopConfig
|
||||
* @returns {Promise<Object>} { customers, vehicles, companies }
|
||||
*/
|
||||
async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) {
|
||||
async function combinedSearch(socket, criteria = {}, bodyshopConfig) {
|
||||
const action = "CombinedSearch";
|
||||
const template = "CombinedSearch";
|
||||
|
||||
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 dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||
|
||||
// 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,
|
||||
const resultXml = await MakeRRCall({
|
||||
action,
|
||||
body: { template, data },
|
||||
socket,
|
||||
jobid
|
||||
dealerConfig: bodyshopConfig
|
||||
});
|
||||
|
||||
const ok = assertRrOkXml(xml, { apiName: "RR Get Parts", allowEmpty: true });
|
||||
const normalized = extractRrResponseData(ok, { action: "GetParts" });
|
||||
const parsed = parseRRResponse(resultXml);
|
||||
if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
|
||||
|
||||
RRLogger(socket, "debug", "RR Get Parts complete", {
|
||||
jobid,
|
||||
count: Array.isArray(normalized) ? normalized.length : 0
|
||||
});
|
||||
return normalized;
|
||||
const customers = parsed.parsed?.Customers?.Customer || [];
|
||||
const vehicles = parsed.parsed?.Vehicles?.ServiceVehicle || [];
|
||||
const companies = parsed.parsed?.Companies?.Company || [];
|
||||
|
||||
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) {
|
||||
RRLogger(socket, "error", `RR Get Parts failed: ${error.message}`, { jobid });
|
||||
throw error;
|
||||
RRLogger(socket, "error", `Error in ${action}`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new RrApiError(`RR ${action} failed: ${error.message}`, "COMBINED_SEARCH_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RrCombinedSearch,
|
||||
RrGetAdvisors,
|
||||
RrGetParts
|
||||
getAdvisors,
|
||||
getParts,
|
||||
combinedSearch
|
||||
};
|
||||
|
||||
@@ -1,424 +1,412 @@
|
||||
// server/rr/rr-mappers.js
|
||||
// -----------------------------------------------------------------------------
|
||||
// Centralized mapping for Reynolds & Reynolds (RR) XML templates.
|
||||
// These functions take our domain objects (JobData, txEnvelope, current/patch)
|
||||
// and produce the Mustache variable objects expected by the RR XML templates in
|
||||
// /server/rr/xml-templates.
|
||||
/**
|
||||
* @file rr-mappers.js
|
||||
* @description Maps internal ImEX (Hasura) entities into Rome (Reynolds & Reynolds) XML structures.
|
||||
* Each function returns a plain JS object that matches Mustache templates in 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
|
||||
// exact RR field semantics (type restrictions, enums, required/optional) based
|
||||
// on the Rome RR PDFs you shared.
|
||||
// ===================== CUSTOMER =====================
|
||||
//
|
||||
// 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)
|
||||
const REPLACE_SPECIAL = /[^a-zA-Z0-9 .,\n#\-()/]+/g;
|
||||
return {
|
||||
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) {
|
||||
if (v === null || v === undefined) return null;
|
||||
return String(v).replace(REPLACE_SPECIAL, "").trim();
|
||||
}
|
||||
CustomerId: customer.external_id || undefined,
|
||||
CustomerType: customer.type || "RETAIL",
|
||||
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) {
|
||||
const s = sanitize(v);
|
||||
return s ? s.toUpperCase() : null;
|
||||
}
|
||||
CustomerGroup: customer.group_name,
|
||||
TaxExempt: customer.tax_exempt ? "true" : "false",
|
||||
DiscountLevel: num(customer.discount_level),
|
||||
PreferredLanguage: customer.language || "EN",
|
||||
|
||||
function asNumberOrNull(v) {
|
||||
if (v === null || v === undefined || v === "") return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
Addresses: (customer.addresses || []).map((a) => ({
|
||||
AddressType: a.type || "BILLING",
|
||||
AddressLine1: a.line1,
|
||||
AddressLine2: a.line2,
|
||||
City: a.city,
|
||||
State: a.state,
|
||||
PostalCode: a.postal_code,
|
||||
Country: a.country || "US"
|
||||
})),
|
||||
|
||||
function normalizePostal(raw) {
|
||||
if (!raw) return null;
|
||||
const s = String(raw).toUpperCase().replace(/\s+/g, "");
|
||||
// If Canadian format (A1A1A1), keep as-is. Otherwise return raw sanitized.
|
||||
return s.length === 6 ? `${s.slice(0, 3)} ${s.slice(3)}` : sanitize(raw);
|
||||
Phones: (customer.phones || []).map((p) => ({
|
||||
PhoneNumber: p.number,
|
||||
PhoneType: p.type || "MOBILE",
|
||||
Preferred: p.preferred ? "true" : "false"
|
||||
})),
|
||||
|
||||
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.
|
||||
* We prefer dealer-level rr_configuration first; fallback to env.
|
||||
* Map internal customer record to Rome CustomerUpdateRq.
|
||||
*/
|
||||
function buildDealerVars(dealerCfg = {}) {
|
||||
function mapCustomerUpdate(customer, bodyshopConfig) {
|
||||
if (!customer) return {};
|
||||
return {
|
||||
DealerCode: dealerCfg.dealerCode || process.env.RR_DEALER_CODE || null,
|
||||
DealerName: dealerCfg.dealerName || process.env.RR_DEALER_NAME || null,
|
||||
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
|
||||
...mapCustomerInsert(customer, bodyshopConfig),
|
||||
RequestId: `CUST-UPDATE-${customer.id}`
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------- Phones/Emails ------------------------------- */
|
||||
//
|
||||
// ===================== VEHICLE =====================
|
||||
//
|
||||
|
||||
function mapPhones({ ph1, ph2, mobile }) {
|
||||
// TODO (spec): adjust PhoneType enumerations if RR requires strict codes.
|
||||
const out = [];
|
||||
if (ph1) out.push({ PhoneNumber: sanitize(ph1), PhoneType: "HOME" });
|
||||
if (ph2) out.push({ PhoneNumber: sanitize(ph2), PhoneType: "WORK" });
|
||||
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() !== "");
|
||||
/**
|
||||
* Map vehicle to Rome ServiceVehicleAddRq.
|
||||
*/
|
||||
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
|
||||
if (!vehicle) return {};
|
||||
|
||||
return {
|
||||
...dealer,
|
||||
// Envelope metadata (optional)
|
||||
RequestId: job?.id || null,
|
||||
Environment: process.env.NODE_ENV || "development",
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
RequestId: `VEH-${vehicle.id}`,
|
||||
|
||||
// Customer node (see InsertCustomer.xml)
|
||||
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",
|
||||
CustomerId: ownerCustomer?.external_id,
|
||||
|
||||
Addresses: mapPostalAddressFromJob(job),
|
||||
Phones: mapPhones({ ph1: job.ownr_ph1, ph2: job.ownr_ph2, mobile: job.ownr_mobile }),
|
||||
Emails: mapEmails({ email: job.ownr_ea }),
|
||||
VIN: vehicle.vin,
|
||||
UnitNumber: vehicle.unit_number,
|
||||
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)
|
||||
DriverLicense: null, // { LicenseNumber, LicenseState, ExpirationDate }
|
||||
Insurance: null, // { CompanyName, PolicyNumber, ExpirationDate }
|
||||
Notes: null // { Note }
|
||||
Insurance: vehicle.insurance
|
||||
? {
|
||||
CompanyName: vehicle.insurance.company,
|
||||
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);
|
||||
// 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;
|
||||
//
|
||||
// ===================== REPAIR ORDER =====================
|
||||
//
|
||||
|
||||
// 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 = {
|
||||
CompanyName: isCompany ? upper(merged?.CompanyName || merged?.customerName?.companyName) : null,
|
||||
FirstName: !isCompany ? upper(merged?.FirstName || merged?.customerName?.firstName) : null,
|
||||
LastName: !isCompany ? upper(merged?.LastName || merged?.customerName?.lastName) : null
|
||||
};
|
||||
const cust = job.customer || {};
|
||||
const veh = job.vehicle || {};
|
||||
|
||||
// Addresses
|
||||
const addr =
|
||||
merged?.Addresses ||
|
||||
merged?.postalAddress ||
|
||||
(merged?.addressLine1 || merged?.addressLine2 || merged?.city
|
||||
? [
|
||||
{
|
||||
AddressLine1: sanitize(merged?.addressLine1),
|
||||
AddressLine2: sanitize(merged?.addressLine2),
|
||||
City: upper(merged?.city),
|
||||
State: upper(merged?.state || merged?.province),
|
||||
PostalCode: normalizePostal(merged?.postalCode),
|
||||
Country: upper(merged?.country) || "USA"
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
RequestId: `RO-${job.id}`,
|
||||
Environment: process.env.NODE_ENV,
|
||||
|
||||
RepairOrderNumber: job.ro_number,
|
||||
DmsRepairOrderId: job.external_id,
|
||||
|
||||
OpenDate: formatDate(job.open_date),
|
||||
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)
|
||||
}))
|
||||
}
|
||||
]
|
||||
: 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"
|
||||
: undefined
|
||||
})),
|
||||
|
||||
// Optional
|
||||
DriverLicense: merged?.DriverLicense || null,
|
||||
Insurance: merged?.Insurance || null,
|
||||
Notes: merged?.Notes || null
|
||||
};
|
||||
}
|
||||
Totals: {
|
||||
Currency: job.currency || "CAD",
|
||||
LaborTotal: num(job.totals?.labor),
|
||||
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 --------------------------------- */
|
||||
|
||||
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
|
||||
Payments: job.payments?.length
|
||||
? {
|
||||
LaborTotal: asNumberOrNull(txEnvelope.totals.labor),
|
||||
PartsTotal: asNumberOrNull(txEnvelope.totals.parts),
|
||||
MiscTotal: asNumberOrNull(txEnvelope.totals.misc),
|
||||
TaxTotal: asNumberOrNull(txEnvelope.totals.tax),
|
||||
GrandTotal: asNumberOrNull(txEnvelope.totals.total)
|
||||
Items: job.payments.map((p) => ({
|
||||
PayerType: p.payer_type,
|
||||
PayerName: p.payer_name,
|
||||
Amount: num(p.amount),
|
||||
Method: p.method,
|
||||
Reference: p.reference,
|
||||
ControlNumber: p.control_number
|
||||
}))
|
||||
}
|
||||
: null,
|
||||
: undefined,
|
||||
|
||||
Insurance: txEnvelope?.insurance
|
||||
Insurance: job.insurance
|
||||
? {
|
||||
CompanyName: upper(txEnvelope.insurance.company),
|
||||
ClaimNumber: sanitize(txEnvelope.insurance.claim),
|
||||
AdjusterName: upper(txEnvelope.insurance.adjuster),
|
||||
AdjusterPhone: sanitize(txEnvelope.insurance.phone)
|
||||
CompanyName: job.insurance.company,
|
||||
ClaimNumber: job.insurance.claim_number,
|
||||
AdjusterName: job.insurance.adjuster_name,
|
||||
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)
|
||||
// delta: patch object describing header fields and line changes
|
||||
const dealer = buildDealerVars(dealerCfg);
|
||||
/**
|
||||
* Map for repair order updates.
|
||||
*/
|
||||
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 })
|
||||
);
|
||||
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
|
||||
}));
|
||||
//
|
||||
// ===================== LOOKUPS =====================
|
||||
//
|
||||
|
||||
const totals = delta?.totals
|
||||
? {
|
||||
LaborTotal: asNumberOrNull(delta.totals.labor),
|
||||
PartsTotal: asNumberOrNull(delta.totals.parts),
|
||||
MiscTotal: asNumberOrNull(delta.totals.misc),
|
||||
TaxTotal: asNumberOrNull(delta.totals.tax),
|
||||
GrandTotal: asNumberOrNull(delta.totals.total)
|
||||
}
|
||||
: null;
|
||||
function mapAdvisorLookup(criteria, bodyshopConfig) {
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
|
||||
SearchCriteria: {
|
||||
Department: criteria.department || "Body Shop",
|
||||
Status: criteria.status || "ACTIVE"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const insurance = delta?.insurance
|
||||
? {
|
||||
CompanyName: upper(delta.insurance.company),
|
||||
ClaimNumber: sanitize(delta.insurance.claim),
|
||||
AdjusterName: upper(delta.insurance.adjuster),
|
||||
AdjusterPhone: sanitize(delta.insurance.phone)
|
||||
}
|
||||
: null;
|
||||
function mapPartsLookup(criteria, bodyshopConfig) {
|
||||
return {
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
RequestId: `LOOKUP-PART-${Date.now()}`,
|
||||
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 =
|
||||
Array.isArray(delta?.notes) && delta.notes.length
|
||||
? { Items: delta.notes.map((n) => sanitize(n)).filter(Boolean) }
|
||||
: null;
|
||||
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
|
||||
// accept nested or flat input
|
||||
const c = criteria || {};
|
||||
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 {
|
||||
...dealer,
|
||||
RequestId: delta?.RequestId || current?.RequestId || null,
|
||||
Environment: process.env.NODE_ENV || "development",
|
||||
// Dealer / routing (aligns with your other mappers)
|
||||
STAR_NS: require("./rr-constants").RR_NS.STAR,
|
||||
MaxRecs: criteria.maxResults || criteria.MaxResults || 50,
|
||||
DealerCode: bodyshopConfig?.dealer_code || "ROME",
|
||||
DealerName: bodyshopConfig?.dealer_name,
|
||||
DealerNumber: bodyshopConfig?.dealer_number,
|
||||
StoreNumber: bodyshopConfig?.store_number,
|
||||
BranchNumber: bodyshopConfig?.branch_number,
|
||||
|
||||
RepairOrderId: current?.RepairOrderId || delta?.RepairOrderId || null,
|
||||
RepairOrderNumber: delta?.RepairOrderNumber || current?.RepairOrderNumber || null,
|
||||
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,
|
||||
RequestId: c.requestId || `COMBINED-${Date.now()}`,
|
||||
Environment: process.env.NODE_ENV,
|
||||
|
||||
// Optional customer/vehicle patches
|
||||
Customer: delta?.Customer || null,
|
||||
Vehicle: delta?.Vehicle || null,
|
||||
// Only include these blocks when they have content; Mustache {{#Block}} respects undefined
|
||||
Customer: hasAny(customerBlock) ? customerBlock : undefined,
|
||||
Vehicle: hasAny(vehicleBlock) ? vehicleBlock : undefined, // template wraps as <rr:ServiceVehicle>…</rr:ServiceVehicle>
|
||||
Company: hasAny(companyBlock) ? companyBlock : undefined,
|
||||
|
||||
// Line changes
|
||||
AddedJobLines: added.length ? added : null,
|
||||
UpdatedJobLines: updated.length ? updated : null,
|
||||
RemovedJobLines: removed.length ? removed : null,
|
||||
// Search behavior flags
|
||||
SearchMode: c.searchMode || c.SearchMode, // EXACT | PARTIAL
|
||||
ExactMatch: toBoolStr(c.exactMatch ?? c.ExactMatch),
|
||||
PartialMatch: toBoolStr(c.partialMatch ?? c.PartialMatch),
|
||||
CaseInsensitive: toBoolStr(c.caseInsensitive ?? c.CaseInsensitive),
|
||||
|
||||
Totals: totals,
|
||||
Insurance: insurance,
|
||||
Notes: notes
|
||||
// Result shaping (default to true when unspecified)
|
||||
ReturnCustomers: toBoolStr(c.returnCustomers ?? c.ReturnCustomers ?? true),
|
||||
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 = {
|
||||
// Customer
|
||||
mapCustomerInsert,
|
||||
mapCustomerUpdate,
|
||||
|
||||
// Vehicle
|
||||
mapVehicleInsertFromJob,
|
||||
|
||||
// Repair orders
|
||||
mapRepairOrderAddFromJob,
|
||||
mapRepairOrderChangeFromJob,
|
||||
mapJobLineToRRLine,
|
||||
|
||||
// shared utils (handy in tests)
|
||||
buildDealerVars,
|
||||
_sanitize: sanitize,
|
||||
_upper: upper,
|
||||
_normalizePostal: normalizePostal
|
||||
mapServiceVehicle,
|
||||
mapRepairOrderCreate,
|
||||
mapRepairOrderUpdate,
|
||||
mapAdvisorLookup,
|
||||
mapPartsLookup,
|
||||
mapCombinedSearch
|
||||
};
|
||||
|
||||
@@ -1,144 +1,99 @@
|
||||
/**
|
||||
* @file rr-repair-orders.js
|
||||
* @description Reynolds & Reynolds (Rome) Repair Order Create & Update.
|
||||
* Implements the "Create Body Shop Management Repair Order" and
|
||||
* "Update Body Shop Management Repair Order" specifications.
|
||||
* @description Rome (Reynolds & Reynolds) Repair Order Integration.
|
||||
* Handles creation and updates of repair orders (BSMRepairOrderRq/Resp).
|
||||
*/
|
||||
|
||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
||||
const { assertRrOk } = require("./rr-error");
|
||||
const { MakeRRCall } = require("./rr-helpers");
|
||||
const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers");
|
||||
const RRLogger = require("./rr-logger");
|
||||
const { client } = require("../graphql-client/graphql-client");
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
const { RrApiError } = require("./rr-error");
|
||||
|
||||
/**
|
||||
* Fetch rr_configuration for the current bodyshop directly from DB.
|
||||
* Dealer-specific configuration is mandatory for RR operations.
|
||||
* Create a new repair order in Rome.
|
||||
* @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 {
|
||||
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||
const config = result?.bodyshops_by_pk?.rr_configuration || null;
|
||||
RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`);
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
|
||||
}
|
||||
const data = mapRepairOrderCreate(job, bodyshopConfig);
|
||||
|
||||
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config);
|
||||
return config;
|
||||
const resultXml = await MakeRRCall({
|
||||
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) {
|
||||
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, {
|
||||
bodyshopId,
|
||||
RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
throw new RrApiError(`RR CreateRepairOrder failed: ${error.message}`, "CREATE_RO_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE REPAIR ORDER
|
||||
* Based on "Rome Create Body Shop Management Repair Order Specification"
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {object} options.socket - socket or express request
|
||||
* @param {object} options.redisHelpers
|
||||
* @param {object} options.JobData - internal job object
|
||||
* @param {object} [options.txEnvelope] - transaction metadata (advisor, timestamps, etc.)
|
||||
* Update an existing repair order in Rome.
|
||||
* @param {Socket} socket
|
||||
* @param {Object} job
|
||||
* @param {Object} bodyshopConfig
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
|
||||
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||
const logger = socket?.logger || console;
|
||||
async function updateRepairOrder(socket, job, bodyshopConfig) {
|
||||
const action = "UpdateRepairOrder";
|
||||
const template = "UpdateRepairOrder";
|
||||
|
||||
try {
|
||||
RRLogger(socket, "info", "RR Create Repair Order started", {
|
||||
jobid: JobData?.id,
|
||||
bodyshopId
|
||||
});
|
||||
RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`);
|
||||
|
||||
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
||||
const data = mapRepairOrderUpdate(job, bodyshopConfig);
|
||||
|
||||
// Build Mustache variables for server/rr/xml-templates/CreateRepairOrder.xml
|
||||
const vars = mapRepairOrderCreate({ JobData, txEnvelope, dealerConfig });
|
||||
|
||||
const data = await MakeRRCall({
|
||||
action: RRActions.CreateRepairOrder, // resolves SOAPAction+URL
|
||||
body: { template: "CreateRepairOrder", data: vars }, // render XML template
|
||||
redisHelpers,
|
||||
const resultXml = await MakeRRCall({
|
||||
action,
|
||||
body: { template, data },
|
||||
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", {
|
||||
jobid: JobData?.id,
|
||||
dealer: dealerConfig?.dealer_code || dealerConfig?.dealerCode
|
||||
});
|
||||
|
||||
return response;
|
||||
return {
|
||||
success: true,
|
||||
dms: "Rome",
|
||||
jobid: job.id,
|
||||
action,
|
||||
xml: resultXml
|
||||
};
|
||||
} catch (error) {
|
||||
RRLogger(socket, "error", `RR Create Repair Order failed: ${error.message}`, {
|
||||
jobid: JobData?.id
|
||||
RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw 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;
|
||||
throw new RrApiError(`RR UpdateRepairOrder failed: ${error.message}`, "UPDATE_RO_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CreateRepairOrder,
|
||||
UpdateRepairOrder,
|
||||
getDealerConfigFromDB
|
||||
createRepairOrder,
|
||||
updateRepairOrder
|
||||
};
|
||||
|
||||
@@ -1,127 +1,191 @@
|
||||
// node server/rr/rr-test.js
|
||||
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file rr-test.js
|
||||
* @description Diagnostic test script for Reynolds & Reynolds (R&R) integration.
|
||||
* Run with: NODE_ENV=development node server/rr/rr-test.js
|
||||
* RR smoke test / CLI (STAR-only)
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(__dirname, "../../", `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
|
||||
const fs = require("fs/promises");
|
||||
const mustache = require("mustache");
|
||||
const fs = require("fs");
|
||||
const dotenv = require("dotenv");
|
||||
const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers");
|
||||
const { getBaseRRConfig } = require("./rr-constants");
|
||||
const { RRActions, MakeRRCall } = require("./rr-helpers");
|
||||
const RRLogger = require("./rr-logger");
|
||||
|
||||
// --- Mock socket + redis helpers for standalone test
|
||||
const socket = {
|
||||
bodyshopId: process.env.TEST_BODYSHOP_ID || null,
|
||||
user: { email: "test@romeonline.io" },
|
||||
emit: (event, data) => console.log(`[SOCKET EVENT] ${event}`, data),
|
||||
logger: console
|
||||
};
|
||||
// Load env file for local runs
|
||||
const defaultEnvPath = path.resolve(__dirname, "../../.env.development");
|
||||
if (fs.existsSync(defaultEnvPath)) {
|
||||
const result = dotenv.config({ path: defaultEnvPath });
|
||||
if (result?.parsed) {
|
||||
console.log(
|
||||
`${defaultEnvPath}\n[dotenv@${require("dotenv/package.json").version}] injecting env (${Object.keys(result.parsed).length}) from ../../.env.development`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const redisHelpers = {
|
||||
setSessionData: async () => {},
|
||||
getSessionData: async () => {},
|
||||
setSessionTransactionData: async () => {},
|
||||
getSessionTransactionData: async () => {},
|
||||
clearSessionTransactionData: async () => {}
|
||||
};
|
||||
// Parse CLI args
|
||||
const argv = process.argv.slice(2);
|
||||
const args = { _: [] };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log("=== Reynolds & Reynolds Integration Test ===");
|
||||
console.log("NODE_ENV:", process.env.NODE_ENV);
|
||||
|
||||
const baseCfg = getBaseRRConfig();
|
||||
console.log("Base R&R Config (from env):", {
|
||||
baseUrl: baseCfg.baseUrl,
|
||||
hasUser: !!baseCfg.username || !!process.env.RR_API_USER || !!process.env.RR_USERNAME,
|
||||
hasPass: !!baseCfg.password || !!process.env.RR_API_PASS || !!process.env.RR_PASSWORD,
|
||||
timeout: baseCfg.timeout
|
||||
});
|
||||
|
||||
// ---- test variables for GetAdvisors
|
||||
const templateVars = {
|
||||
DealerCode: process.env.RR_DEALER_NAME || "ROME",
|
||||
DealerName: "Rome Collision Test",
|
||||
SearchCriteria: {
|
||||
Department: "Body Shop",
|
||||
Status: "ACTIVE"
|
||||
if (a.startsWith("--")) {
|
||||
const eq = a.indexOf("=");
|
||||
if (eq > -1) {
|
||||
const k = a.slice(2, eq);
|
||||
const v = a.slice(eq + 1);
|
||||
args[k] = v;
|
||||
} else {
|
||||
const k = a.slice(2);
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith("-")) {
|
||||
args[k] = next;
|
||||
i++; // consume value
|
||||
} else {
|
||||
args[k] = true; // boolean flag
|
||||
}
|
||||
};
|
||||
}
|
||||
} 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:
|
||||
const dealerConfigOverride = {
|
||||
// baseUrl can also be overridden here if you want
|
||||
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"
|
||||
};
|
||||
function toIntOr(defaultVal, maybe) {
|
||||
const n = parseInt(maybe, 10);
|
||||
return Number.isFinite(n) ? n : defaultVal;
|
||||
}
|
||||
|
||||
// Show the first ~600 chars of the envelope we will send (by rendering the template + header)
|
||||
// NOTE: This is just for printing; MakeRRCall will rebuild with proper header internally.
|
||||
const templatePath = path.join(__dirname, "xml-templates", "GetAdvisors.xml");
|
||||
const tpl = await fs.readFile(templatePath, "utf8");
|
||||
const renderedBody = mustache.render(tpl, templateVars);
|
||||
// ✅ fixed guard clause
|
||||
function pickActionName(raw) {
|
||||
if (!raw || typeof raw !== "string") return "ping";
|
||||
const x = raw.toLowerCase();
|
||||
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
|
||||
const { renderXmlTemplate } = require("./rr-helpers");
|
||||
const headerPreview = await renderXmlTemplate("_EnvelopeHeader", {
|
||||
PPSysId: dealerConfigOverride.ppsysid,
|
||||
DealerNumber: dealerConfigOverride.dealer_number,
|
||||
StoreNumber: dealerConfigOverride.store_number,
|
||||
BranchNumber: dealerConfigOverride.branch_number,
|
||||
Username: dealerConfigOverride.username,
|
||||
Password: dealerConfigOverride.password,
|
||||
CorrelationId: "preview-correlation"
|
||||
});
|
||||
const previewEnvelope = `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||
<soapenv:Header>
|
||||
${headerPreview}
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
${renderedBody}
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>`.trim();
|
||||
|
||||
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;
|
||||
function buildBodyForAction(action, args, cfg) {
|
||||
switch (action) {
|
||||
case "ping":
|
||||
case "advisors": {
|
||||
const max = toIntOr(1, args.max);
|
||||
const data = {
|
||||
DealerCode: cfg.dealerNumber,
|
||||
DealerNumber: cfg.dealerNumber,
|
||||
StoreNumber: cfg.storeNumber,
|
||||
BranchNumber: cfg.branchNumber,
|
||||
SearchCriteria: {
|
||||
AdvisorId: args.advisorId,
|
||||
FirstName: args.first || args.firstname,
|
||||
LastName: args.last || args.lastname,
|
||||
Department: args.department,
|
||||
Status: args.status || "ACTIVE",
|
||||
IncludeInactive: args.includeInactive ? "true" : undefined,
|
||||
MaxResults: max
|
||||
}
|
||||
};
|
||||
return { template: "GetAdvisors", data, appArea: {} };
|
||||
}
|
||||
|
||||
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({
|
||||
action: "GetAdvisors",
|
||||
baseUrl: process.env.RR_API_BASE_URL,
|
||||
body: { template: "GetAdvisors", data: templateVars },
|
||||
dealerConfig: dealerConfigOverride,
|
||||
redisHelpers,
|
||||
socket,
|
||||
jobid: "test-job",
|
||||
retries: 1
|
||||
});
|
||||
case "parts": {
|
||||
const max = toIntOr(5, args.max);
|
||||
const data = {
|
||||
DealerNumber: cfg.dealerNumber,
|
||||
StoreNumber: cfg.storeNumber,
|
||||
BranchNumber: cfg.branchNumber,
|
||||
SearchCriteria: {
|
||||
PartNumber: args.part,
|
||||
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") });
|
||||
console.log("\n✅ Test completed successfully.\n");
|
||||
} catch (error) {
|
||||
console.error("\n❌ Test failed:", error.message);
|
||||
console.error(error.stack);
|
||||
default:
|
||||
throw new Error(`Unsupported action: ${action}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
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)
|
||||
* -------------------------------------------------
|
||||
* Delegates to rr-helpers.MakeRRCall (which handles:
|
||||
* - fetching dealer config from DB via resolveRRConfig
|
||||
* - rendering Mustache XML templates
|
||||
* - building SOAP envelope + headers
|
||||
* - axios POST + retries
|
||||
*
|
||||
* Use this when you prefer the "action + variables" style and (optionally)
|
||||
* want a parsed Body node back instead of raw XML.
|
||||
* @file rr-wsdl.js
|
||||
* @description Lightweight service description + utilities for the Rome (R&R) SOAP actions.
|
||||
* - Maps actions to SOAPAction headers (from rr-constants)
|
||||
* - Maps actions to Mustache template filenames (xml-templates/*.xml)
|
||||
* - Provides verification helpers to ensure templates exist
|
||||
* - Provides normalized SOAP headers used by the transport
|
||||
*/
|
||||
|
||||
const { XMLParser } = require("fast-xml-parser");
|
||||
const logger = require("../utils/logger");
|
||||
const { MakeRRCall, resolveRRConfig, renderXmlTemplate } = require("./rr-helpers");
|
||||
const path = require("path");
|
||||
const fs = require("fs/promises");
|
||||
const { RR_ACTIONS, RR_SOAP_HEADERS } = require("./rr-constants");
|
||||
|
||||
// Map friendly action names to template filenames (no envelope here; helpers add it)
|
||||
const RR_ACTION_MAP = {
|
||||
CustomerInsert: { file: "InsertCustomer.xml" },
|
||||
CustomerUpdate: { file: "UpdateCustomer.xml" },
|
||||
ServiceVehicleInsert: { file: "InsertServiceVehicle.xml" },
|
||||
CombinedSearch: { file: "CombinedSearch.xml" },
|
||||
GetParts: { file: "GetParts.xml" },
|
||||
GetAdvisors: { file: "GetAdvisors.xml" },
|
||||
CreateRepairOrder: { file: "CreateRepairOrder.xml" },
|
||||
UpdateRepairOrder: { file: "UpdateRepairOrder.xml" }
|
||||
};
|
||||
// ---- Action <-> Template wiring ----
|
||||
// Keep action names consistent with rr-helpers / rr-lookup / rr-repair-orders / rr-customer
|
||||
const ACTION_TEMPLATES = Object.freeze({
|
||||
InsertCustomer: "InsertCustomer",
|
||||
UpdateCustomer: "UpdateCustomer",
|
||||
InsertServiceVehicle: "InsertServiceVehicle",
|
||||
CreateRepairOrder: "CreateRepairOrder",
|
||||
UpdateRepairOrder: "UpdateRepairOrder",
|
||||
GetAdvisors: "GetAdvisors",
|
||||
GetParts: "GetParts",
|
||||
CombinedSearch: "CombinedSearch"
|
||||
});
|
||||
|
||||
/**
|
||||
* Optionally render just the body XML for a given action (no SOAP envelope).
|
||||
* Mostly useful for diagnostics/tests.
|
||||
* Get the SOAPAction string for a known action.
|
||||
* Throws if action is unknown.
|
||||
*/
|
||||
async function buildRRXml(action, variables = {}) {
|
||||
const entry = RR_ACTION_MAP[action];
|
||||
if (!entry) throw new Error(`Unknown RR action: ${action}`);
|
||||
const templateName = entry.file.replace(/\.xml$/i, "");
|
||||
return renderXmlTemplate(templateName, variables);
|
||||
function getSoapAction(action) {
|
||||
const entry = RR_ACTIONS[action];
|
||||
if (!entry) {
|
||||
const known = Object.keys(RR_ACTIONS).join(", ");
|
||||
throw new Error(`Unknown RR action "${action}". Known: ${known}`);
|
||||
}
|
||||
return entry.soapAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an RR SOAP request using helpers (action + variables).
|
||||
* @param {object} opts
|
||||
* @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
|
||||
* Get the template filename (without extension) for a known action.
|
||||
* e.g., "CreateRepairOrder" -> "CreateRepairOrder"
|
||||
*/
|
||||
async function sendRRRequest({ action, variables = {}, socket, raw = false, retries = 1 }) {
|
||||
const entry = RR_ACTION_MAP[action];
|
||||
if (!entry) throw new Error(`Unknown RR action: ${action}`);
|
||||
function getTemplateForAction(action) {
|
||||
const tpl = ACTION_TEMPLATES[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,
|
||||
body: { template: templateName, data: variables },
|
||||
socket,
|
||||
dealerConfig,
|
||||
retries
|
||||
});
|
||||
soapAction: getSoapAction(action),
|
||||
template: getTemplateForAction(action)
|
||||
}));
|
||||
}
|
||||
|
||||
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 {
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed = parser.parse(xml);
|
||||
for (const [action, tpl] of Object.entries(ACTION_TEMPLATES)) {
|
||||
const filePath = path.join(baseDir, `${tpl}.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 =
|
||||
parsed?.Envelope?.Body ||
|
||||
parsed?.["soapenv:Envelope"]?.["soapenv:Body"] ||
|
||||
parsed?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
|
||||
parsed?.["S:Envelope"]?.["S:Body"] ||
|
||||
parsed;
|
||||
|
||||
return bodyNode;
|
||||
} catch (err) {
|
||||
logger.log("rr-wsdl-parse-error", "ERROR", "RR", null, {
|
||||
action,
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
});
|
||||
// If parsing fails, return raw so caller can inspect
|
||||
return xml;
|
||||
/**
|
||||
* Quick assert that throws if any template is missing.
|
||||
* You can call this once during boot and log the result.
|
||||
*/
|
||||
async function assertTemplates() {
|
||||
const issues = await verifyTemplatesExist();
|
||||
if (issues.length) {
|
||||
const msg =
|
||||
"RR xml-templates verification failed:\n" +
|
||||
issues.map((i) => ` - ${i.action} -> ${i.template}.xml :: ${i.error}`).join("\n");
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendRRRequest,
|
||||
buildRRXml,
|
||||
RR_ACTION_MAP
|
||||
// Maps / helpers
|
||||
ACTION_TEMPLATES,
|
||||
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/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}
|
||||
<rr:RequestId>{{RequestId}}</rr:RequestId>
|
||||
{{/RequestId}}
|
||||
{{#Environment}}
|
||||
<rr:Environment>{{Environment}}</rr:Environment>
|
||||
{{/Environment}}
|
||||
<rey_RomeCustServVehCombReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
|
||||
<!-- NOTE: ApplicationArea is injected by buildStarEnvelope(); do not include it here. -->
|
||||
<CustServVehCombReq>
|
||||
<QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}>
|
||||
{{#Customer.PhoneNumber}}<Phone Num="{{Customer.PhoneNumber}}"/>{{/Customer.PhoneNumber}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}
|
||||
<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>
|
||||
{{#Customer.FirstName}}<FirstName>{{Customer.FirstName}}</FirstName>{{/Customer.FirstName}}
|
||||
{{#Customer.LastName}}<LastName>{{Customer.LastName}}</LastName>{{/Customer.LastName}}
|
||||
{{#Customer.EmailAddress}}<EMail>{{Customer.EmailAddress}}</EMail>{{/Customer.EmailAddress}}
|
||||
|
||||
<rr:SearchCriteria>
|
||||
{{#Customer}}
|
||||
<rr:Customer>
|
||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||
{{#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>
|
||||
{{#Vehicle.VIN}}<VIN>{{Vehicle.VIN}}</VIN>{{/Vehicle.VIN}}
|
||||
{{#Vehicle.LicensePlate}}<LicensePlate>{{Vehicle.LicensePlate}}</LicensePlate>{{/Vehicle.LicensePlate}}
|
||||
</QueryData>
|
||||
</CustServVehCombReq>
|
||||
</rey_RomeCustServVehCombReq>
|
||||
|
||||
@@ -1,158 +1,117 @@
|
||||
<rr:RepairOrderInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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:RepairOrder>
|
||||
<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>
|
||||
{{#DmsRepairOrderId}}<rr:DmsRepairOrderId>{{DmsRepairOrderId}}</rr:DmsRepairOrderId>{{/DmsRepairOrderId}}
|
||||
|
||||
<!-- Core dates -->
|
||||
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
||||
|
||||
<!-- People & routing -->
|
||||
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||
{{#ProfitCenter}}<rr:ProfitCenter>{{ProfitCenter}}</rr:ProfitCenter>{{/ProfitCenter}}
|
||||
|
||||
<!-- Type & status -->
|
||||
{{#ROType}}<rr:ROType>{{ROType}}</rr:ROType>{{/ROType}}
|
||||
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}}
|
||||
{{#IsBodyShop}}<rr:IsBodyShop>{{IsBodyShop}}</rr:IsBodyShop>{{/IsBodyShop}}
|
||||
{{#DRPFlag}}<rr:DRPFlag>{{DRPFlag}}</rr:DRPFlag>{{/DRPFlag}}
|
||||
|
||||
<!-- Customer -->
|
||||
<rr:Customer>
|
||||
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
||||
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||
|
||||
<!-- Optional address if you have it -->
|
||||
{{#Address}}
|
||||
<rr:Address>
|
||||
{{#Line1}}<rr:Line1>{{Line1}}</rr:Line1>{{/Line1}}
|
||||
{{#Line2}}<rr:Line2>{{Line2}}</rr:Line2>{{/Line2}}
|
||||
{{#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}}
|
||||
</rr:Address>
|
||||
{{/Address}}
|
||||
</rr:Customer>
|
||||
|
||||
<!-- Vehicle -->
|
||||
<rr:Vehicle>
|
||||
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||
</rr:Vehicle>
|
||||
|
||||
<!-- Job lines -->
|
||||
{{#JobLines}}
|
||||
<rr:JobLine>
|
||||
<rr:Sequence>{{Sequence}}</rr:Sequence>
|
||||
{{#ParentSequence}}<rr:ParentSequence>{{ParentSequence}}</rr:ParentSequence>{{/ParentSequence}}
|
||||
|
||||
{{#LineType}}<rr:LineType>
|
||||
{{LineType}}</rr:LineType>{{/LineType}} <!-- LABOR | PART | MISC | FEE | DISCOUNT -->
|
||||
{{#Category}}<rr:Category>
|
||||
{{Category}}</rr:Category>{{/Category}} <!-- e.g., BODY, PAINT, GLASS -->
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
|
||||
<!-- Labor fields -->
|
||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||
|
||||
<!-- Part fields -->
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||
|
||||
<!-- Amounts -->
|
||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||
{{#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>
|
||||
<rey_RomeCreateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<BSMRepairOrderReq>
|
||||
<RepairOrder>
|
||||
<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>
|
||||
{{#DmsRepairOrderId}}<DmsRepairOrderId>{{DmsRepairOrderId}}</DmsRepairOrderId>{{/DmsRepairOrderId}}
|
||||
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
|
||||
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
|
||||
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
|
||||
{{#ProfitCenter}}<ProfitCenter>{{ProfitCenter}}</ProfitCenter>{{/ProfitCenter}}
|
||||
{{#ROType}}<ROType>{{ROType}}</ROType>{{/ROType}}
|
||||
{{#Status}}<Status>{{Status}}</Status>{{/Status}}
|
||||
{{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
|
||||
{{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
|
||||
<Customer>
|
||||
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
|
||||
{{#Address}}
|
||||
<Address>
|
||||
{{#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}}
|
||||
</Address>
|
||||
{{/Address}}
|
||||
</Customer>
|
||||
<ServiceVehicle>
|
||||
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
|
||||
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
|
||||
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||
</ServiceVehicle>
|
||||
{{#JobLines}}
|
||||
<JobLine>
|
||||
<Sequence>{{Sequence}}</Sequence>
|
||||
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||
{{#LineType}}<LineType>
|
||||
{{LineType}}</LineType>{{/LineType}
|
||||
{{#Category}}<Category>
|
||||
{{Category}}</Category>{{/Category}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
{{#Taxes}}
|
||||
<Taxes>
|
||||
{{#Items}}
|
||||
<Tax>
|
||||
<Code>{{Code}}</Code>
|
||||
<Amount>{{Amount}}</Amount>
|
||||
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
|
||||
</Tax>
|
||||
{{/Items}}
|
||||
</Taxes>
|
||||
{{/Taxes}}
|
||||
</JobLine>
|
||||
{{/JobLines}}
|
||||
{{#Totals}}
|
||||
<Totals>
|
||||
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
|
||||
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
|
||||
{{#DiscountTotal}}<DiscountTotal>{{DiscountTotal}}</DiscountTotal>{{/DiscountTotal}}
|
||||
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
|
||||
<GrandTotal>{{GrandTotal}}</GrandTotal>
|
||||
</Totals>
|
||||
{{/Totals}}
|
||||
{{#Payments}}
|
||||
<Payments>
|
||||
{{#Items}}
|
||||
<rr:Tax>
|
||||
<rr:Code>{{Code}}</rr:Code>
|
||||
<rr:Amount>{{Amount}}</rr:Amount>
|
||||
{{#Rate}}<rr:Rate>{{Rate}}</rr:Rate>{{/Rate}}
|
||||
</rr:Tax>
|
||||
<Payment>
|
||||
<PayerType>{{PayerType}}</PayerType>
|
||||
{{#PayerName}}<PayerName>{{PayerName}}</PayerName>{{/PayerName}}
|
||||
<Amount>{{Amount}}</Amount>
|
||||
{{#Method}}<Method>{{Method}}</Method>{{/Method}}
|
||||
{{#Reference}}<Reference>{{Reference}}</Reference>{{/Reference}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
</Payment>
|
||||
{{/Items}}
|
||||
</rr:Taxes>
|
||||
{{/Taxes}}
|
||||
</rr:JobLine>
|
||||
{{/JobLines}}
|
||||
|
||||
<!-- Totals -->
|
||||
{{#Totals}}
|
||||
<rr:Totals>
|
||||
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
||||
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
||||
{{#DiscountTotal}}<rr:DiscountTotal>{{DiscountTotal}}</rr:DiscountTotal>{{/DiscountTotal}}
|
||||
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
||||
<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>
|
||||
</rr:Totals>
|
||||
{{/Totals}}
|
||||
|
||||
<!-- 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>
|
||||
</Payments>
|
||||
{{/Payments}}
|
||||
{{#Insurance}}
|
||||
<Insurance>
|
||||
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
|
||||
{{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
|
||||
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
|
||||
</Insurance>
|
||||
{{/Insurance}}
|
||||
{{#Notes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
</Notes>
|
||||
{{/Notes}}
|
||||
</RepairOrder>
|
||||
</BSMRepairOrderReq>
|
||||
</rey_RomeCreateBSMRepairOrderReq>
|
||||
|
||||
@@ -1,34 +1,15 @@
|
||||
<rr:GetAdvisorsRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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>
|
||||
|
||||
{{#SearchCriteria}}
|
||||
<rr:SearchCriteria>
|
||||
{{#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>
|
||||
<rey_RomeGetAdvisorsReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
|
||||
<GetAdvisorsReq>
|
||||
<QueryData{{#SearchCriteria.MaxResults}} MaxRecs="{{SearchCriteria.MaxResults}}"{{/SearchCriteria.MaxResults}}>
|
||||
{{#SearchCriteria.AdvisorId}}<AdvisorID>{{SearchCriteria.AdvisorId}}</AdvisorID>{{/SearchCriteria.AdvisorId}}
|
||||
{{#SearchCriteria.FirstName}}<FirstName>{{SearchCriteria.FirstName}}</FirstName>{{/SearchCriteria.FirstName}}
|
||||
{{#SearchCriteria.LastName}}<LastName>{{SearchCriteria.LastName}}</LastName>{{/SearchCriteria.LastName}}
|
||||
{{#SearchCriteria.Department}}<Department>{{SearchCriteria.Department}}</Department>{{/SearchCriteria.Department}}
|
||||
{{#SearchCriteria.Status}}<Status>{{SearchCriteria.Status}}</Status>{{/SearchCriteria.Status}}
|
||||
{{#SearchCriteria.IncludeInactive}}<IncludeInactive>{{SearchCriteria.IncludeInactive}}</IncludeInactive>{{/SearchCriteria.IncludeInactive}}
|
||||
{{#SearchCriteria.PageNumber}}<PageNumber>{{SearchCriteria.PageNumber}}</PageNumber>{{/SearchCriteria.PageNumber}}
|
||||
{{#SearchCriteria.SortBy}}<SortBy>{{SearchCriteria.SortBy}}</SortBy>{{/SearchCriteria.SortBy}}
|
||||
{{#SearchCriteria.SortDirection}}<SortDirection>{{SearchCriteria.SortDirection}}</SortDirection>{{/SearchCriteria.SortDirection}}
|
||||
</QueryData>
|
||||
</GetAdvisorsReq>
|
||||
</rey_RomeGetAdvisorsReq>
|
||||
|
||||
@@ -1,50 +1,25 @@
|
||||
<rr:GetPartRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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>
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Vendor}}<rr:Vendor>{{Vendor}}</rr:Vendor>{{/Vendor}}
|
||||
{{#Category}}<rr:Category>{{Category}}</rr:Category>{{/Category}}
|
||||
|
||||
<!-- Optional classification flags -->
|
||||
{{#Brand}}<rr:Brand>{{Brand}}</rr:Brand>{{/Brand}}
|
||||
{{#IsOEM}}<rr:IsOEM>{{IsOEM}}</rr:IsOEM>{{/IsOEM}} <!-- true | false -->
|
||||
{{#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>
|
||||
<rey_RomeGetPartsReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<GetPartReq>
|
||||
<QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}{{#PageNumber}} Page="{{PageNumber}}"{{/PageNumber}}>
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||
{{#Vendor}}<Vendor>{{Vendor}}</Vendor>{{/Vendor}}
|
||||
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
|
||||
{{#Brand}}<Brand>{{Brand}}</Brand>{{/Brand}}
|
||||
{{#IsOEM}}<IsOEM>{{IsOEM}}</IsOEM>{{/IsOEM}}
|
||||
{{#IsAftermarket}}<IsAftermarket>{{IsAftermarket}}</IsAftermarket>{{/IsAftermarket}}
|
||||
{{#InStock}}<InStock>{{InStock}}</InStock>{{/InStock}}
|
||||
{{#Warehouse}}<Warehouse>{{Warehouse}}</Warehouse>{{/Warehouse}}
|
||||
{{#Location}}<Location>{{Location}}</Location>{{/Location}}
|
||||
{{#MinPrice}}<MinPrice>{{MinPrice}}</MinPrice>{{/MinPrice}}
|
||||
{{#MaxPrice}}<MaxPrice>{{MaxPrice}}</MaxPrice>{{/MaxPrice}}
|
||||
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
|
||||
{{#SearchMode}}<SearchMode>{{SearchMode}}</SearchMode>{{/SearchMode}}
|
||||
{{#SortBy}}<SortBy>{{SortBy}}</SortBy>{{/SortBy}}
|
||||
{{#SortDirection}}<SortDirection>{{SortDirection}}</SortDirection>{{/SortDirection}}
|
||||
</QueryData>
|
||||
</GetPartReq>
|
||||
</rey_RomeGetPartsReq>
|
||||
|
||||
@@ -1,102 +1,63 @@
|
||||
<rr:CustomerInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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:Customer>
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
{{#CustomerType}}<rr:CustomerType>
|
||||
{{CustomerType}}</rr:CustomerType>{{/CustomerType}} <!-- RETAIL | FLEET | INTERNAL -->
|
||||
|
||||
{{#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}}
|
||||
|
||||
<!-- Optional customer classification -->
|
||||
{{#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>
|
||||
{{#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}}
|
||||
</rr:Address>
|
||||
{{/Addresses}}
|
||||
|
||||
<!-- Phones -->
|
||||
{{#Phones}}
|
||||
<rr:Phone>
|
||||
<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>
|
||||
{{#PhoneType}}<rr:PhoneType>
|
||||
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||
</rr:Phone>
|
||||
{{/Phones}}
|
||||
|
||||
<!-- Emails -->
|
||||
{{#Emails}}
|
||||
<rr:Email>
|
||||
<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>
|
||||
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||
</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}}
|
||||
|
||||
<!-- 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>
|
||||
<rey_RomeCustomerInsertReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<CustomerInsertReq>
|
||||
<Customer>
|
||||
{{#CustomerNumber}}<CustomerNumber>{{CustomerNumber}}</CustomerNumber>{{/CustomerNumber}}
|
||||
{{#CustomerType}}<CustomerType>
|
||||
{{CustomerType}}</CustomerType>{{/CustomerType}}
|
||||
<CustomerName>{{CustomerName}}</CustomerName>
|
||||
{{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
|
||||
{{#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>
|
||||
{{#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}}
|
||||
</Address>
|
||||
{{/Addresses}}
|
||||
{{#Phones}}
|
||||
<Phone>
|
||||
<Type>{{Type}}</Type>
|
||||
<Number>{{Number}}</Number>
|
||||
{{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
|
||||
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
|
||||
</Phone>
|
||||
{{/Phones}}
|
||||
{{#Emails}}
|
||||
<Email>
|
||||
<Type>{{Type}}</Type>
|
||||
<Address>{{Address}}</Address>
|
||||
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
|
||||
</Email>
|
||||
{{/Emails}}
|
||||
{{#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>
|
||||
<Type>{{Type}}</Type>
|
||||
<AccountNumber>{{AccountNumber}}</AccountNumber>
|
||||
{{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
|
||||
</LinkedAccount>
|
||||
{{/LinkedAccounts}}
|
||||
{{#Notes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
</Notes>
|
||||
{{/Notes}}
|
||||
</Customer>
|
||||
</CustomerInsertReq>
|
||||
</rey_RomeCustomerInsertReq>
|
||||
|
||||
@@ -1,83 +1,57 @@
|
||||
<rr:ServiceVehicleAddRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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:ServiceVehicle>
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
|
||||
<!-- Identity -->
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#UnitNumber}}<rr:UnitNumber>{{UnitNumber}}</rr:UnitNumber>{{/UnitNumber}}
|
||||
{{#StockNumber}}<rr:StockNumber>{{StockNumber}}</rr:StockNumber>{{/StockNumber}}
|
||||
|
||||
<!-- Descriptive -->
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Trim}}<rr:Trim>{{Trim}}</rr:Trim>{{/Trim}}
|
||||
{{#BodyStyle}}<rr:BodyStyle>{{BodyStyle}}</rr:BodyStyle>{{/BodyStyle}}
|
||||
{{#Transmission}}<rr:Transmission>{{Transmission}}</rr:Transmission>{{/Transmission}}
|
||||
{{#Engine}}<rr:Engine>{{Engine}}</rr:Engine>{{/Engine}}
|
||||
{{#FuelType}}<rr:FuelType>{{FuelType}}</rr:FuelType>{{/FuelType}}
|
||||
{{#DriveType}}<rr:DriveType>{{DriveType}}</rr:DriveType>{{/DriveType}}
|
||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||
|
||||
<!-- Registration -->
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||
{{#RegistrationExpiry}}<rr:RegistrationExpiry>{{RegistrationExpiry}}</rr:RegistrationExpiry>{{/RegistrationExpiry}}
|
||||
|
||||
<!-- Odometer -->
|
||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||
{{#OdometerUnits}}<rr:OdometerUnits>
|
||||
{{OdometerUnits}}</rr:OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
|
||||
{{#InServiceDate}}<rr:InServiceDate>{{InServiceDate}}</rr:InServiceDate>{{/InServiceDate}}
|
||||
|
||||
<!-- Ownership -->
|
||||
{{#Ownership}}
|
||||
<rr:Ownership>
|
||||
{{#OwnerId}}<rr:OwnerId>{{OwnerId}}</rr:OwnerId>{{/OwnerId}}
|
||||
{{#OwnerName}}<rr:OwnerName>{{OwnerName}}</rr:OwnerName>{{/OwnerName}}
|
||||
{{#OwnershipType}}<rr:OwnershipType>
|
||||
{{OwnershipType}}</rr:OwnershipType>{{/OwnershipType}} <!-- OWNER | LEASED | FLEET -->
|
||||
</rr:Ownership>
|
||||
{{/Ownership}}
|
||||
|
||||
<!-- 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}}
|
||||
|
||||
<!-- 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>
|
||||
<rey_RomeServVehicleInsertReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<ServVehicleInsertReq>
|
||||
<ServiceVehicle>
|
||||
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||
{{#UnitNumber}}<UnitNumber>{{UnitNumber}}</UnitNumber>{{/UnitNumber}}
|
||||
{{#StockNumber}}<StockNumber>{{StockNumber}}</StockNumber>{{/StockNumber}}
|
||||
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||
{{#Trim}}<Trim>{{Trim}}</Trim>{{/Trim}}
|
||||
{{#BodyStyle}}<BodyStyle>{{BodyStyle}}</BodyStyle>{{/BodyStyle}}
|
||||
{{#Transmission}}<Transmission>{{Transmission}}</Transmission>{{/Transmission}}
|
||||
{{#Engine}}<Engine>{{Engine}}</Engine>{{/Engine}}
|
||||
{{#FuelType}}<FuelType>{{FuelType}}</FuelType>{{/FuelType}}
|
||||
{{#DriveType}}<DriveType>{{DriveType}}</DriveType>{{/DriveType}}
|
||||
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
|
||||
{{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
|
||||
{{#RegistrationExpiry}}<RegistrationExpiry>{{RegistrationExpiry}}</RegistrationExpiry>{{/RegistrationExpiry}}
|
||||
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||
{{#OdometerUnits}}<OdometerUnits>
|
||||
{{OdometerUnits}}</OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
|
||||
{{#InServiceDate}}<InServiceDate>{{InServiceDate}}</InServiceDate>{{/InServiceDate}}
|
||||
{{#Ownership}}
|
||||
<Ownership>
|
||||
{{#OwnerId}}<OwnerId>{{OwnerId}}</OwnerId>{{/OwnerId}}
|
||||
{{#OwnerName}}<OwnerName>{{OwnerName}}</OwnerName>{{/OwnerName}}
|
||||
{{#OwnershipType}}<OwnershipType>
|
||||
{{OwnershipType}}</OwnershipType>{{/OwnershipType}}
|
||||
</Ownership>
|
||||
{{/Ownership}}
|
||||
{{#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}}
|
||||
{{#Warranty}}
|
||||
<Warranty>
|
||||
{{#WarrantyCompany}}<WarrantyCompany>{{WarrantyCompany}}</WarrantyCompany>{{/WarrantyCompany}}
|
||||
{{#WarrantyNumber}}<WarrantyNumber>{{WarrantyNumber}}</WarrantyNumber>{{/WarrantyNumber}}
|
||||
{{#WarrantyType}}<WarrantyType>{{WarrantyType}}</WarrantyType>{{/WarrantyType}}
|
||||
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
|
||||
</Warranty>
|
||||
{{/Warranty}}
|
||||
{{#VehicleNotes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
</Notes>
|
||||
{{/VehicleNotes}}
|
||||
</ServiceVehicle>
|
||||
</ServVehicleInsertReq>
|
||||
</rey_RomeServVehicleInsertReq>
|
||||
|
||||
@@ -1,107 +1,83 @@
|
||||
<rr:CustomerUpdateRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
<rey_RomeCustomerUpdateReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<CustomerUpdateReq>
|
||||
<Customer>
|
||||
<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>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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: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>
|
||||
{{#Notes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
</Notes>
|
||||
{{/Notes}}
|
||||
</Customer>
|
||||
</CustomerUpdateReq>
|
||||
</rey_RomeCustomerUpdateReq>
|
||||
|
||||
@@ -1,135 +1,139 @@
|
||||
<rr:RepairOrderChgRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<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:RepairOrder>
|
||||
<!-- Identity -->
|
||||
{{#RepairOrderId}}<rr:RepairOrderId>{{RepairOrderId}}</rr:RepairOrderId>{{/RepairOrderId}}
|
||||
{{#RepairOrderNumber}}<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>{{/RepairOrderNumber}}
|
||||
|
||||
<!-- Header fields that may be patched -->
|
||||
{{#Status}}<rr:Status>
|
||||
{{Status}}</rr:Status>{{/Status}} <!-- e.g., OPEN|IN_PROGRESS|CLOSED -->
|
||||
{{#ROType}}<rr:ROType>
|
||||
{{ROType}}</rr:ROType>{{/ROType}} <!-- e.g., INSURANCE|CUSTOMER_PAY -->
|
||||
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
||||
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
||||
{{#LocationCode}}<rr:LocationCode>{{LocationCode}}</rr:LocationCode>{{/LocationCode}}
|
||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||
{{#PurchaseOrder}}<rr:PurchaseOrder>{{PurchaseOrder}}</rr:PurchaseOrder>{{/PurchaseOrder}}
|
||||
|
||||
<!-- Optional customer patch -->
|
||||
{{#Customer}}
|
||||
<rr:Customer>
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||
</rr:Customer>
|
||||
{{/Customer}}
|
||||
|
||||
<!-- Optional vehicle patch -->
|
||||
{{#Vehicle}}
|
||||
<rr:Vehicle>
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||
</rr:Vehicle>
|
||||
{{/Vehicle}}
|
||||
|
||||
<!-- Line changes: use one of AddedJobLines / UpdatedJobLines / RemovedJobLines -->
|
||||
{{#AddedJobLines}}
|
||||
<rr:AddedJobLine>
|
||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||
{{#PayType}}<rr:PayType>
|
||||
{{PayType}}</rr:PayType>{{/PayType}} <!-- CUST|INS|WARR|INT -->
|
||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||
</rr:AddedJobLine>
|
||||
{{/AddedJobLines}}
|
||||
|
||||
{{#UpdatedJobLines}}
|
||||
<rr:UpdatedJobLine>
|
||||
<!-- Identify the existing line either by Sequence or LineId -->
|
||||
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||
{{#ChangeType}}<rr:ChangeType>
|
||||
{{ChangeType}}</rr:ChangeType>{{/ChangeType}} <!-- PRICE|QTY|DESC|OPCODE|PAYTYPE -->
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||
{{#PayType}}<rr:PayType>{{PayType}}</rr:PayType>{{/PayType}}
|
||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||
</rr:UpdatedJobLine>
|
||||
{{/UpdatedJobLines}}
|
||||
|
||||
{{#RemovedJobLines}}
|
||||
<rr:RemovedJobLine>
|
||||
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||
</rr:RemovedJobLine>
|
||||
{{/RemovedJobLines}}
|
||||
|
||||
<!-- Totals (optional patch if RR expects header totals on change) -->
|
||||
{{#Totals}}
|
||||
<rr:Totals>
|
||||
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
||||
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
||||
{{#GrandTotal}}<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>{{/GrandTotal}}
|
||||
</rr:Totals>
|
||||
{{/Totals}}
|
||||
|
||||
<!-- Insurance (optional update) -->
|
||||
{{#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 (append or replace depending on RR semantics) -->
|
||||
{{#Notes}}
|
||||
<rr:Notes>
|
||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||
</rr:Notes>
|
||||
{{/Notes}}
|
||||
</rr:RepairOrder>
|
||||
</rr:RepairOrderChgRq>
|
||||
<rey_RomeUpdateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<BSMRepairOrderChgReq>
|
||||
<RepairOrder>
|
||||
{{#RepairOrderId}}<RepairOrderId>{{RepairOrderId}}</RepairOrderId>{{/RepairOrderId}}
|
||||
{{#RepairOrderNumber}}<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>{{/RepairOrderNumber}}
|
||||
{{#Status}}<Status>
|
||||
{{Status}}</Status>{{/Status}}
|
||||
{{#ROType}}<ROType>
|
||||
{{ROType}}</ROType>{{/ROType}}
|
||||
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
|
||||
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
|
||||
{{#LocationCode}}<LocationCode>{{LocationCode}}</LocationCode>{{/LocationCode}}
|
||||
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
|
||||
{{#PurchaseOrder}}<PurchaseOrder>{{PurchaseOrder}}</PurchaseOrder>{{/PurchaseOrder}}
|
||||
{{#Customer}}
|
||||
<Customer>
|
||||
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
|
||||
</Customer>
|
||||
{{/Customer}}
|
||||
{{#Vehicle}}
|
||||
<ServiceVehicle>
|
||||
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
|
||||
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
|
||||
{{#Year}}<Year>{{Year}}</Year>{{/Year}}
|
||||
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
|
||||
{{#Model}}<Model>{{Model}}</Model>{{/Model}}
|
||||
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||
</ServiceVehicle>
|
||||
{{/Vehicle}}
|
||||
{{#AddedJobLines}}
|
||||
<AddedJobLines>
|
||||
{{#Items}}
|
||||
<JobLine>
|
||||
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LineType}}<LineType>
|
||||
{{LineType}}</LineType>{{/LineType}}
|
||||
{{#Category}}<Category>
|
||||
{{Category}}</Category>{{/Category}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
{{#Taxes}}
|
||||
<Taxes>
|
||||
{{#Items}}
|
||||
<Tax>
|
||||
<Code>{{Code}}</Code>
|
||||
<Amount>{{Amount}}</Amount>
|
||||
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
|
||||
</Tax>
|
||||
{{/Items}}
|
||||
</Taxes>
|
||||
{{/Taxes}}
|
||||
{{#PayType}}<PayType>
|
||||
{{PayType}}</PayType>{{/PayType}}
|
||||
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||
</JobLine>
|
||||
{{/Items}}
|
||||
</AddedJobLines>
|
||||
{{/AddedJobLines}}
|
||||
{{#UpdatedJobLines}}
|
||||
<UpdatedJobLines>
|
||||
{{#Items}}
|
||||
<JobLine>
|
||||
{{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
|
||||
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||
{{#ChangeType}}<ChangeType>
|
||||
{{ChangeType}}</ChangeType>{{/ChangeType}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#PayType}}<PayType>{{PayType}}</PayType>{{/PayType}}
|
||||
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||
</JobLine>
|
||||
{{/Items}}
|
||||
</UpdatedJobLines>
|
||||
{{/UpdatedJobLines}}
|
||||
{{#RemovedJobLines}}
|
||||
<RemovedJobLines>
|
||||
{{#Items}}
|
||||
<JobLine>
|
||||
{{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
|
||||
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||
</JobLine>
|
||||
{{/Items}}
|
||||
</RemovedJobLines>
|
||||
{{/RemovedJobLines}}
|
||||
{{#Totals}}
|
||||
<Totals>
|
||||
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
|
||||
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
|
||||
{{#GrandTotal}}<GrandTotal>{{GrandTotal}}</GrandTotal>{{/GrandTotal}}
|
||||
</Totals>
|
||||
{{/Totals}}
|
||||
{{#Insurance}}
|
||||
<Insurance>
|
||||
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
|
||||
{{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
|
||||
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
|
||||
</Insurance>
|
||||
{{/Insurance}}
|
||||
{{#Notes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
</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 { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
||||
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 = ({
|
||||
io,
|
||||
@@ -340,75 +357,35 @@ const redisSocketEvents = ({
|
||||
};
|
||||
|
||||
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 {
|
||||
await RRJobExport({
|
||||
socket,
|
||||
redisHelpers: {
|
||||
setSessionData,
|
||||
getSessionData,
|
||||
addUserSocketMapping,
|
||||
removeUserSocketMapping,
|
||||
refreshUserSocketTTL,
|
||||
getUserSocketMappingByBodyshop,
|
||||
setSessionTransactionData,
|
||||
getSessionTransactionData,
|
||||
clearSessionTransactionData
|
||||
},
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||||
jobid,
|
||||
txEnvelope
|
||||
});
|
||||
// Back-compat: old callers: { jobid, txEnvelope }; new: { job, config, options }
|
||||
// Prefer direct job/config, otherwise try txEnvelope.{job,config}
|
||||
const job = payload.job || payload.txEnvelope?.job;
|
||||
const options = payload.options || payload.txEnvelope?.options || {};
|
||||
const cfg = resolveRRConfigFrom(payload);
|
||||
|
||||
if (!job) {
|
||||
RRLogger(socket, "error", "RR export missing job payload");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await exportJobToRome(socket, job, cfg, options);
|
||||
// Broadcast to bodyshop room for UI to pick up
|
||||
const room = getBodyshopRoom(socket.bodyshopId);
|
||||
io.to(room).emit("rr-export-job:result", { jobid: job.id, result });
|
||||
} catch (error) {
|
||||
RRLogger(socket, "error", `Error during RR export: ${error.message}`);
|
||||
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 {
|
||||
await RRSelectedCustomer({
|
||||
socket,
|
||||
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
|
||||
});
|
||||
const cfg = resolveRRConfigFrom({}); // if you want per-call overrides, pass them in the payload and merge here
|
||||
const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
|
||||
cb?.(data);
|
||||
} catch (e) {
|
||||
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 {
|
||||
const { RrGetAdvisors } = require("../rr/rr-lookup");
|
||||
const data = await RrGetAdvisors({
|
||||
socket,
|
||||
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
|
||||
jobid,
|
||||
params
|
||||
});
|
||||
const cfg = resolveRRConfigFrom({});
|
||||
const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
|
||||
cb?.(data);
|
||||
} catch (e) {
|
||||
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 {
|
||||
const { RrGetParts } = require("../rr/rr-lookup");
|
||||
const data = await RrGetParts({
|
||||
socket,
|
||||
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
|
||||
jobid,
|
||||
params
|
||||
});
|
||||
const cfg = resolveRRConfigFrom({});
|
||||
const data = await lookupApi.getParts(socket, params || {}, cfg);
|
||||
cb?.(data);
|
||||
} catch (e) {
|
||||
RRLogger(socket, "error", `RR get parts error: ${e.message}`);
|
||||
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
|
||||
registerRoomAndBroadcastEvents(socket);
|
||||
registerUpdateEvents(socket);
|
||||
|
||||
Reference in New Issue
Block a user