feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint

This commit is contained in:
Dave
2025-10-08 13:57:34 -04:00
parent 2ffc4b81f4
commit de02b34a63
28 changed files with 2550 additions and 2443 deletions

22
package-lock.json generated
View File

@@ -65,6 +65,7 @@
"uuid": "^11.1.0", "uuid": "^11.1.0",
"winston": "^3.18.3", "winston": "^3.18.3",
"winston-cloudwatch": "^6.3.0", "winston-cloudwatch": "^6.3.0",
"xml-formatter": "^3.6.7",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1", "xmlbuilder2": "^3.1.1",
"yazl": "^3.3.1" "yazl": "^3.3.1"
@@ -11621,6 +11622,27 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/xml-formatter": {
"version": "3.6.7",
"resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.7.tgz",
"integrity": "sha512-IsfFYJQuoDqtUlKhm4EzeoBOb+fQwzQVeyxxAQ0sThn/nFnQmyLPTplqq4yRhaOENH/tAyujD2TBfIYzUKB6hg==",
"license": "MIT",
"dependencies": {
"xml-parser-xo": "^4.1.5"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/xml-parser-xo": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.5.tgz",
"integrity": "sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==",
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/xml2js": { "node_modules/xml2js": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",

View File

@@ -74,6 +74,7 @@
"uuid": "^11.1.0", "uuid": "^11.1.0",
"winston": "^3.18.3", "winston": "^3.18.3",
"winston-cloudwatch": "^6.3.0", "winston-cloudwatch": "^6.3.0",
"xml-formatter": "^3.6.7",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1", "xmlbuilder2": "^3.1.1",
"yazl": "^3.3.1" "yazl": "^3.3.1"

View File

@@ -123,7 +123,7 @@ const applyRoutes = ({ app }) => {
app.use("/payroll", require("./server/routes/payrollRoutes")); app.use("/payroll", require("./server/routes/payrollRoutes"));
app.use("/sso", require("./server/routes/ssoRoutes")); app.use("/sso", require("./server/routes/ssoRoutes"));
app.use("/integrations", require("./server/routes/intergrationRoutes")); app.use("/integrations", require("./server/routes/intergrationRoutes"));
app.use("/rr", require("./server/routes/rrRoutes")); app.use("/rr", require("./server/rr/rrRoutes"));
// Default route for forbidden access // Default route for forbidden access
app.get("/", (req, res) => { app.get("/", (req, res) => {

View File

@@ -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;

View 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 dont 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 thats 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];
}

View File

@@ -1,79 +1,50 @@
/** /**
* @file rr-constants.js * STAR-only constants for Reynolds & Reynolds (Rome/RCI)
* @description Central constants and configuration for Reynolds & Reynolds (R&R) integration. * Used by rr-helpers.js to build and send SOAP requests.
* Platform-level secrets (API base URL, username, password, ppsysId, dealer/store/branch) are loaded from .env
* Dealer-specific values (overrides) come from bodyshop.rr_configuration.
*/ */
const RR_TIMEOUT_MS = 30000; // 30-second SOAP call timeout exports.RR_NS = Object.freeze({
const RR_NAMESPACE_URI = "http://reynoldsandrey.com/"; SOAP_ENV: "http://schemas.xmlsoap.org/soap/envelope/",
const RR_DEFAULT_MAX_RESULTS = 25; SOAP_ENC: "http://schemas.xmlsoap.org/soap/encoding/",
XSD: "http://www.w3.org/2001/XMLSchema",
/** XSI: "http://www.w3.org/2001/XMLSchema-instance",
* Maps internal operation names to Reynolds & Reynolds SOAP actions. WSSE: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
* soapAction is sent as the SOAPAction header; URL selection happens in rr-helpers. WSU: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
*/ STAR_TRANSPORT: "http://www.starstandards.org/webservices/2005/10/transport",
const RR_ACTIONS = { STAR_BUSINESS: "http://www.starstandards.org/STAR"
GetAdvisors: { soapAction: "GetAdvisors" },
GetParts: { soapAction: "GetParts" },
CombinedSearch: { soapAction: "CombinedSearch" },
InsertCustomer: { soapAction: "CustomerInsert" },
UpdateCustomer: { soapAction: "CustomerUpdate" },
InsertServiceVehicle: { soapAction: "ServiceVehicleInsert" },
CreateRepairOrder: { soapAction: "RepairOrderInsert" },
UpdateRepairOrder: { soapAction: "RepairOrderUpdate" }
};
/**
* Default SOAP HTTP headers. SOAPAction is dynamically set per request.
*/
const RR_SOAP_HEADERS = {
"Content-Type": "text/xml; charset=utf-8",
SOAPAction: ""
};
/**
* Wraps the rendered XML body inside a SOAP envelope.
* @param {string} xmlBody - Inner request XML
* @param {string} [headerXml] - Optional header XML (already namespaced)
*/
const buildSoapEnvelope = (xmlBody, headerXml = "") => `
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="${RR_NAMESPACE_URI}">
<soapenv:Header>
${headerXml}
</soapenv:Header>
<soapenv:Body>
${xmlBody}
</soapenv:Body>
</soapenv:Envelope>
`;
/**
* Loads base configuration for R&R requests from environment variables.
* Dealer-specific overrides come from bodyshop.rr_configuration in the DB.
*/
const getBaseRRConfig = () => ({
// IMPORTANT: RCI Receive endpoint ends with .ashx
baseUrl: process.env.RR_API_BASE_URL || "https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx",
username: process.env.RR_API_USER || "",
password: process.env.RR_API_PASS || "",
ppsysId: process.env.RR_PPSYS_ID || "",
// Welcome Kit often provides these (used in SOAP header)
dealerNumber: process.env.RR_DEALER_NUMBER || "",
storeNumber: process.env.RR_STORE_NUMBER || "",
branchNumber: process.env.RR_BRANCH_NUMBER || "",
dealerDefault: process.env.RR_DEFAULT_DEALER || "ROME",
timeout: RR_TIMEOUT_MS
}); });
module.exports = { const RR_STAR_SOAP_ACTION = "http://www.starstandards.org/webservices/2005/10/transport/ProcessMessage";
RR_TIMEOUT_MS, exports.RR_SOAP_ACTION = RR_STAR_SOAP_ACTION;
RR_NAMESPACE_URI,
RR_DEFAULT_MAX_RESULTS, const RR_SOAP_HEADERS = {
RR_ACTIONS, "Content-Type": "text/xml; charset=utf-8",
RR_SOAP_HEADERS, SOAPAction: RR_STAR_SOAP_ACTION
buildSoapEnvelope, };
getBaseRRConfig
// All STAR-supported actions (mapped to Mustache templates)
exports.RR_ACTIONS = Object.freeze({
CombinedSearch: { template: "CombinedSearch" },
GetAdvisors: { template: "GetAdvisors" },
GetParts: { template: "GetParts" },
InsertCustomer: { template: "InsertCustomer" },
InsertServiceVehicle: { template: "InsertServiceVehicle" },
CreateRepairOrder: { template: "CreateRepairOrder" },
UpdateCustomer: { template: "UpdateCustomer" },
UpdateRepairOrder: { template: "UpdateRepairOrder" }
});
// Base config loader (environment-driven)
exports.getBaseRRConfig = function getBaseRRConfig() {
return {
baseUrl: process.env.RR_BASE_URL,
username: process.env.RR_USERNAME,
password: process.env.RR_PASSWORD,
ppsysId: process.env.RR_PPSYSID, // optional legacy identifier
dealerNumber: process.env.RR_DEALER_NUMBER,
storeNumber: process.env.RR_STORE_NUMBER,
branchNumber: process.env.RR_BRANCH_NUMBER || "01",
wssePasswordType: process.env.RR_WSSE_PASSWORD_TYPE || "Text",
timeout: Number(process.env.RR_TIMEOUT_MS || 30000)
};
}; };

View File

@@ -1,137 +1,99 @@
/** /**
* @file rr-customer.js * @file rr-customer.js
* @description Reynolds & Reynolds (Rome) Customer Insert/Update integration. * @description Rome (Reynolds & Reynolds) Customer Insert / Update integration.
* Builds request payloads using rr-mappers and executes via rr-helpers. * Maps internal customer objects to Rome XML schemas and executes RCI calls.
* All dealer-specific data (DealerNumber, LocationId, etc.) is read from the DB (bodyshop.rr_configuration).
*/ */
const { MakeRRCall, RRActions } = require("./rr-helpers"); const { MakeRRCall } = require("./rr-helpers");
const { assertRrOk } = require("./rr-error");
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers"); const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
const RRLogger = require("./rr-logger"); const RRLogger = require("./rr-logger");
const { client } = require("../graphql-client/graphql-client"); const { RrApiError } = require("./rr-error");
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
/** /**
* Fetch rr_configuration for the current bodyshop directly from DB. * Insert a new customer into Rome.
* This ensures we always have the latest Dealer/Location mapping. * @param {Socket} socket - WebSocket connection for logging context
* @param {Object} customer - Hasura customer record
* @param {Object} bodyshopConfig - DMS configuration
* @returns {Promise<Object>} result
*/ */
async function getDealerConfigFromDB(bodyshopId, logger) { async function insertCustomer(socket, customer, bodyshopConfig) {
const action = "InsertCustomer";
const template = "InsertCustomer";
try { try {
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId }); RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
const config = result?.bodyshops_by_pk?.rr_configuration || null;
if (!config) { const data = mapCustomerInsert(customer, bodyshopConfig);
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
}
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config); const resultXml = await MakeRRCall({
return config; action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig,
jobid: customer.id
});
RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
return {
success: true,
dms: "Rome",
action,
customerId: customer.id,
xml: resultXml
};
} catch (error) { } catch (error) {
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, { RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
bodyshopId,
message: error.message, message: error.message,
stack: error.stack stack: error.stack
}); });
throw error; throw new RrApiError(`RR InsertCustomer failed: ${error.message}`, "INSERT_CUSTOMER_ERROR");
} }
} }
/** /**
* CUSTOMER INSERT (Rome Customer Insert Specification 1.2) * Update an existing customer in Rome.
* Creates a new customer record in the DMS. * @param {Socket} socket
* * @param {Object} customer
* @param {object} options * @param {Object} bodyshopConfig
* @param {object} options.socket - socket.io connection or express req * @returns {Promise<Object>}
* @param {object} options.redisHelpers
* @param {object} options.JobData - normalized job record
*/ */
async function RrCustomerInsert({ socket, redisHelpers, JobData }) { async function updateCustomer(socket, customer, bodyshopConfig) {
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; const action = "UpdateCustomer";
const logger = socket?.logger || console; const template = "UpdateCustomer";
try { try {
RRLogger(socket, "info", "RR Customer Insert started", { jobid: JobData?.id, bodyshopId }); RRLogger(socket, "info", `Starting RR ${action} for customer ${customer.id}`);
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger); const data = mapCustomerUpdate(customer, bodyshopConfig);
// Build Mustache variables for the InsertCustomer.xml template const resultXml = await MakeRRCall({
const vars = mapCustomerInsert(JobData, dealerConfig); action,
body: { template, data },
const data = await MakeRRCall({
action: RRActions.CreateCustomer, // resolves to SOAPAction + URL
body: { template: "InsertCustomer", data: vars }, // render server/rr/xml-templates/InsertCustomer.xml
redisHelpers,
socket, socket,
jobid: JobData.id dealerConfig: bodyshopConfig,
jobid: customer.id
}); });
const response = assertRrOk(data, { apiName: "RR Create Customer" }); RRLogger(socket, "debug", `${action} completed successfully`, { customerId: customer.id });
RRLogger(socket, "debug", "RR Customer Insert success", {
jobid: JobData?.id,
dealer: dealerConfig?.dealerCode || dealerConfig?.dealer_code
});
return response; return {
success: true,
dms: "Rome",
action,
customerId: customer.id,
xml: resultXml
};
} catch (error) { } catch (error) {
RRLogger(socket, "error", `RR Customer Insert failed: ${error.message}`, { jobid: JobData?.id }); RRLogger(socket, "error", `Error in ${action} for customer ${customer.id}`, {
throw error; message: error.message,
} stack: error.stack
}
/**
* CUSTOMER UPDATE (Rome Customer Update Specification 1.2)
* Updates an existing RR customer record.
*
* @param {object} options
* @param {object} options.socket
* @param {object} options.redisHelpers
* @param {object} options.JobData
* @param {object} options.existingCustomer - current RR customer record (from Combined Search)
* @param {object} options.patch - updated fields from frontend
*/
async function RrCustomerUpdate({ socket, redisHelpers, JobData, existingCustomer, patch }) {
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
const logger = socket?.logger || console;
try {
RRLogger(socket, "info", "RR Customer Update started", {
jobid: JobData?.id,
bodyshopId,
existingCustomerId: existingCustomer?.CustomerId
}); });
throw new RrApiError(`RR UpdateCustomer failed: ${error.message}`, "UPDATE_CUSTOMER_ERROR");
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
// Build Mustache variables for the UpdateCustomer.xml template
const vars = mapCustomerUpdate(existingCustomer, patch, dealerConfig);
const data = await MakeRRCall({
action: RRActions.UpdateCustomer, // resolves to SOAPAction + URL
body: { template: "UpdateCustomer", data: vars }, // render server/rr/xml-templates/UpdateCustomer.xml
redisHelpers,
socket,
jobid: JobData.id
});
const response = assertRrOk(data, { apiName: "RR Update Customer" });
RRLogger(socket, "debug", "RR Customer Update success", {
jobid: JobData?.id,
customerId: existingCustomer?.CustomerId
});
return response;
} catch (error) {
RRLogger(socket, "error", `RR Customer Update failed: ${error.message}`, {
jobid: JobData?.id,
customerId: existingCustomer?.CustomerId
});
throw error;
} }
} }
module.exports = { module.exports = {
RrCustomerInsert, insertCustomer,
RrCustomerUpdate, updateCustomer
getDealerConfigFromDB
}; };

View File

@@ -1,103 +1,48 @@
/** /**
* @file rr-error.js * @file rr-error.js
* @description Centralized error class and assertion logic for Reynolds & Reynolds API calls. * @description Custom error types for the Reynolds & Reynolds (Rome) integration.
* Provides consistent handling across all RR modules (customer, repair order, lookups, etc.)
*/ */
/** /**
* Custom Error type for RR API responses * Base RR API Error class — always structured with a message and a code.
*/ */
class RrApiError extends Error { class RrApiError extends Error {
constructor(message, { reqId, url, apiName, errorData, status, statusText } = {}) { /**
* @param {string} message - Human-readable message
* @param {string} [code="RR_ERROR"] - Short machine-readable error code
* @param {Object} [details] - Optional structured metadata
*/
constructor(message, code = "RR_ERROR", details = {}) {
super(message); super(message);
this.name = "RrApiError"; this.name = "RrApiError";
this.reqId = reqId || null; this.code = code;
this.url = url || null; this.details = details;
this.apiName = apiName || null; }
this.errorData = errorData || null;
this.status = status || null; toJSON() {
this.statusText = statusText || null; return {
name: this.name,
code: this.code,
message: this.message,
details: this.details
};
} }
} }
/** /**
* Assert that a Reynolds & Reynolds response is successful. * Helper to normalize thrown errors into a consistent RrApiError instance.
*
* Expected success structure (based on Rome RR specs):
* {
* "SuccessFlag": true,
* "ErrorCode": "0",
* "ErrorMessage": "",
* "Data": { ... }
* }
*
* Or if SOAP/XML-based:
* {
* "Envelope": {
* "Body": {
* "Response": {
* "SuccessFlag": true,
* ...
* }
* }
* }
* }
*
* This helper unwraps and normalizes the response to detect any error cases.
*/ */
function assertRrOk(data, { apiName = "RR API Call", allowEmpty = false } = {}) { function toRrError(err, defaultCode = "RR_ERROR") {
if (!data && !allowEmpty) { if (!err) return new RrApiError("Unknown RR error", defaultCode);
throw new RrApiError(`${apiName} returned no data`, { apiName }); if (err instanceof RrApiError) return err;
} if (typeof err === "string") return new RrApiError(err, defaultCode);
const msg = err.message || "Unspecified RR error";
// Normalize envelope const code = err.code || defaultCode;
const response = const details = err.details || {};
data?.Envelope?.Body?.Response || return new RrApiError(msg, code, details);
data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]] ||
data?.Response ||
data;
// Handle array of errors or error objects
const errorBlock = response?.Errors || response?.Error || response?.Fault || null;
// Basic success conditions per RR documentation
const success =
response?.SuccessFlag === true ||
response?.ErrorCode === "0" ||
response?.ResultCode === "0" ||
(Array.isArray(errorBlock) && errorBlock.length === 0);
// If success, return normalized response
if (success || allowEmpty) {
return response?.Data || response;
}
// Construct contextual error info
const errorMessage = response?.ErrorMessage || response?.FaultString || response?.Message || "Unknown RR API error";
throw new RrApiError(`${apiName} failed: ${errorMessage}`, {
apiName,
errorData: response,
status: response?.ErrorCode || response?.ResultCode
});
}
/**
* Safely unwrap nested RR API responses for consistency across handlers.
*/
function extractRrResponseData(data) {
if (!data) return null;
return (
data?.Envelope?.Body?.Response?.Data ||
data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]]?.Data ||
data?.Data ||
data
);
} }
module.exports = { module.exports = {
RrApiError, RrApiError,
assertRrOk, toRrError
extractRrResponseData
}; };

View File

@@ -1,257 +1,319 @@
/** /**
* @file rr-helpers.js * STAR-only SOAP transport + template rendering for Reynolds & Reynolds (Rome/RCI).
* @description Core helper functions for Reynolds & Reynolds integration. * - Renders Mustache STAR business templates (rey_*Req rooted with STAR ns)
* Handles XML rendering, SOAP communication, and configuration merging. * - Builds STAR SOAP envelope (ProcessMessage/payload/content + ApplicationArea)
* - Posts to RCI endpoint with STAR SOAPAction (full URI)
* - Parses XML response (faults + STAR payload result)
*/ */
const fs = require("fs/promises"); const fs = require("fs/promises");
const path = require("path"); const path = require("path");
const mustache = require("mustache");
const axios = require("axios"); const axios = require("axios");
const { v4: uuidv4 } = require("uuid"); const mustache = require("mustache");
const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants"); const { XMLParser } = require("fast-xml-parser");
const RRLogger = require("./rr-logger"); const RRLogger = require("./rr-logger");
const { client } = require("../graphql-client/graphql-client"); const { RR_ACTIONS, RR_SOAP_HEADERS, RR_STAR_SOAP_ACTION, RR_NS, getBaseRRConfig } = require("./rr-constants");
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries"); const { RrApiError } = require("./rr-error");
const xmlFormatter = require("xml-formatter");
/* ------------------------------------------------------------------------------------------------
* Configuration
* ----------------------------------------------------------------------------------------------*/
/** /**
* Loads the rr_configuration JSON for a given bodyshop directly from the database. * Remove XML decl, collapse inter-tag whitespace, strip empty lines,
* Dealer-level settings only. Platform/secret defaults come from getBaseRRConfig(). * then pretty-print. Safe for XML because we only touch whitespace
* @param {string} bodyshopId * BETWEEN tags, not inside text nodes.
* @returns {Promise<object>} rr_configuration /**
* Collapse Mustache-induced whitespace and pretty print.
* - strips XML decl (inner)
* - removes lines that are only whitespace
* - collapses inter-tag whitespace
* - formats with consistent indentation
*/ */
async function getDealerConfig(bodyshopId) { function prettyPrintXml(xml) {
try { let s = xml;
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
const cfg = result?.bodyshops_by_pk?.rr_configuration || {}; // strip any inner XML declaration
return cfg; s = s.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
} catch (err) {
console.error(`[RR] Failed to load rr_configuration for bodyshop ${bodyshopId}:`, err.message); // remove lines that are only whitespace
return {}; s = s.replace(/^[\t ]*(?:\r?\n)/gm, "");
}
// collapse whitespace strictly between tags (not inside text nodes)
s = s.replace(/>\s+</g, "><");
// final pretty print
return xmlFormatter(s, {
indentation: " ",
collapseContent: true, // keep short elements on one line
lineSeparator: "\n",
strictMode: false
});
} }
/** // ---------- Public action map (compat with rr-test.js) ----------
* Helper to retrieve combined configuration (env + dealer) for calls. const RRActions = Object.fromEntries(Object.entries(RR_ACTIONS).map(([k]) => [k, { action: k }]));
* NOTE: This does not hit Redis. DB only (dealer overrides) + env secrets.
* @param {object} socket - Either a real socket or an Express req carrying bodyshopId on .bodyshopId // ---------- Template cache ----------
* @returns {Promise<object>} configuration const templateCache = new Map();
*/
async function resolveRRConfig(socket) { async function loadTemplate(templateName) {
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid; if (templateCache.has(templateName)) return templateCache.get(templateName);
const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {}; const filePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
return { ...getBaseRRConfig(), ...dealerCfg }; const tpl = await fs.readFile(filePath, "utf8");
templateCache.set(templateName, tpl);
return tpl;
} }
/* ------------------------------------------------------------------------------------------------
* Template rendering
* ----------------------------------------------------------------------------------------------*/
/**
* Loads and renders a Mustache XML template with provided data.
* @param {string} templateName - Name of XML file under server/rr/xml-templates/ (without .xml)
* @param {object} data - Template substitution object
* @returns {Promise<string>} Rendered XML string
*/
async function renderXmlTemplate(templateName, data) { async function renderXmlTemplate(templateName, data) {
const templatePath = path.join(__dirname, "xml-templates", `${templateName}.xml`); const tpl = await loadTemplate(templateName);
const xmlTemplate = await fs.readFile(templatePath, "utf8"); // Render and strip any XML declaration to keep a single root element for the BOD
return mustache.render(xmlTemplate, data); const rendered = mustache.render(tpl, data || {});
return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
} }
/** // ---------- Config resolution (STAR only) ----------
* Build a SOAP envelope with a rendered header + body. async function resolveRRConfig(_socket, bodyshopConfig) {
* Header comes from xml-templates/_EnvelopeHeader.xml. const envCfg = getBaseRRConfig();
* @param {string} renderedBodyXml
* @param {object} headerVars - values for header mustache
* @returns {Promise<string>}
*/
async function buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) {
const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars);
return ` if (bodyshopConfig && typeof bodyshopConfig === "object") {
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/"> return {
...envCfg,
baseUrl: bodyshopConfig.baseUrl || envCfg.baseUrl,
username: bodyshopConfig.username || envCfg.username,
password: bodyshopConfig.password || envCfg.password,
ppsysId: bodyshopConfig.ppsysId || envCfg.ppsysId,
dealerNumber: bodyshopConfig.dealer_number || envCfg.dealerNumber,
storeNumber: bodyshopConfig.store_number || envCfg.storeNumber,
branchNumber: bodyshopConfig.branch_number || envCfg.branchNumber,
wssePasswordType: bodyshopConfig.wssePasswordType || envCfg.wssePasswordType || "Text",
timeout: envCfg.timeout
};
}
return envCfg;
}
// ---------- Response parsing ----------
function parseRRResponse(xml) {
const parser = new XMLParser({
ignoreAttributes: false,
removeNSPrefix: true
});
const doc = parser.parse(xml);
// Envelope/Body
const body =
doc?.Envelope?.Body ||
doc?.["soapenv:Envelope"]?.["soapenv:Body"] ||
doc?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
doc?.["S:Envelope"]?.["S:Body"] ||
doc?.Body ||
doc;
// SOAP Fault?
const fault = body?.Fault || body?.["soap:Fault"];
if (fault) {
return {
success: false,
code: fault.faultcode || "SOAP_FAULT",
message: fault.faultstring || "Unknown SOAP Fault",
raw: xml
};
}
// STAR transport path: ProcessMessage/payload/content
const processMessage = body?.ProcessMessage || body?.["ns0:ProcessMessage"] || body?.["ProcessMessageResponse"];
if (processMessage?.payload?.content) {
const content = processMessage.payload.content;
if (content && typeof content === "object") {
const keys = Object.keys(content).filter((k) => k !== "@_id");
const respKey = keys.find((k) => /Resp$/.test(k)) || (keys[0] === "ApplicationArea" && keys[1]) || keys[0];
const respNode = respKey ? content[respKey] : content;
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || respNode?.StatusCode || "OK";
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || respNode?.StatusMessage || null;
return {
success: ["OK", "Success"].includes(String(resultCode)),
code: resultCode,
message: resultMessage,
raw: xml,
parsed: respNode
};
}
}
// Fallback: first element under Body (just in case)
const keys = body && typeof body === "object" ? Object.keys(body) : [];
const respNode = keys.length ? body[keys[0]] : body;
const resultCode = respNode?.ResultCode || respNode?.ResponseCode || "OK";
const resultMessage = respNode?.ResultMessage || respNode?.ResponseMessage || null;
return {
success: resultCode === "OK" || resultCode === "Success",
code: resultCode,
message: resultMessage,
raw: xml,
parsed: respNode
};
}
// ---------- STAR envelope helpers ----------
function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, Destination }) {
// Make sure we inject *inside* the STAR root, not before it.
// 1) Strip any XML declaration just in case (idempotent)
let xml = innerXml.replace(/^\s*<\?xml[^>]*\?>\s*/i, "");
const appArea = `
<ApplicationArea>
<CreationDateTime>${CreationDateTime}</CreationDateTime>
<BODId>${BODId}</BODId>
<Sender>
${Sender?.Component ? `<Component>${Sender.Component}</Component>` : ""}
${Sender?.Task ? `<Task>${Sender.Task}</Task>` : ""}
${Sender?.ReferenceId ? `<ReferenceId>${Sender.ReferenceId}</ReferenceId>` : ""}
</Sender>
<Destination>
<DestinationNameCode>RR</DestinationNameCode>
${Destination?.DealerNumber ? `<DealerNumber>${Destination.DealerNumber}</DealerNumber>` : ""}
${Destination?.StoreNumber ? `<StoreNumber>${Destination.StoreNumber}</StoreNumber>` : ""}
${Destination?.AreaNumber ? `<AreaNumber>${Destination.AreaNumber}</AreaNumber>` : ""}
</Destination>
</ApplicationArea>`.trim();
// Inject right after the opening tag of the root element (skip processing instructions)
// e.g. <rey_RomeGetAdvisorsReq ...> ==> insert ApplicationArea here
xml = xml.replace(/^(\s*<[^!?][^>]*>)/, `$1\n${appArea}\n`);
return xml;
}
async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}) {
const now = new Date().toISOString();
const payloadWithAppArea = wrapWithApplicationArea(innerBusinessXml, {
CreationDateTime: appArea.CreationDateTime || now,
BODId: appArea.BODId || `BOD-${Date.now()}`,
Sender: appArea.Sender || { Component: "Rome", Task: "SV", ReferenceId: "Update" },
Destination: appArea.Destination || {
DealerNumber: creds.dealerNumber,
StoreNumber: String(creds.storeNumber ?? "").padStart(2, "0"),
AreaNumber: String(creds.branchNumber || "01").padStart(2, "0")
}
});
return `<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenc="${RR_NS.SOAP_ENC}" xmlns:soapenv="${RR_NS.SOAP_ENV}" xmlns:xsd="${RR_NS.XSD}" xmlns:xsi="${RR_NS.XSI}">
<soapenv:Header> <soapenv:Header>
${headerXml} <wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="${RR_NS.WSSE}">
<wsse:UsernameToken>
<wsse:Username>${creds.username}</wsse:Username>
<wsse:Password>${creds.password}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header> </soapenv:Header>
<soapenv:Body> <soapenv:Body>
${renderedBodyXml} <ProcessMessage xmlns="${RR_NS.STAR_TRANSPORT}">
<payload xmlns:soap="${RR_NS.SOAP_ENV}" xmlns:xsi="${RR_NS.XSI}" xmlns:xsd="${RR_NS.XSD}" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wsse="${RR_NS.WSSE}" xmlns:wsu="${RR_NS.WSU}" xmlns="${RR_NS.STAR_TRANSPORT}">
<content id="content0">
${payloadWithAppArea}
</content>
</payload>
</ProcessMessage>
</soapenv:Body> </soapenv:Body>
</soapenv:Envelope> </soapenv:Envelope>`;
`.trim();
} }
/* ------------------------------------------------------------------------------------------------ // ---------- Main transport (STAR only) ----------
* Core SOAP caller
* ----------------------------------------------------------------------------------------------*/
/**
* Compute the full URL and SOAPAction for a given action spec.
* Allows either:
* - action: a key into RR_ACTIONS (e.g. "GetAdvisors")
* - action: a raw URL/spec
*/
function resolveActionTarget(action, baseUrl) {
if (typeof action === "string" && RR_ACTIONS[action]) {
const spec = RR_ACTIONS[action];
const soapAction = spec.soapAction || spec.action || action;
const cleanedBase = (spec.baseUrl || baseUrl || "").replace(/\/+$/, "");
const url = spec.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
return { url, soapAction };
}
if (action && typeof action === "object" && (action.url || action.soapAction || action.action)) {
const soapAction = action.soapAction || action.action || "";
const cleanedBase = (action.baseUrl || baseUrl || "").replace(/\/+$/, "");
const url = action.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
return { url, soapAction };
}
if (typeof action === "string") {
return { url: action, soapAction: "" };
}
throw new Error("Invalid RR action. Must be a known RR_ACTIONS key, an action spec, or a URL string.");
}
/**
* Constructs and sends a SOAP call to the Reynolds & Reynolds endpoint.
*
* Body can be one of:
* - string (already-rendered XML body)
* - { template: "TemplateName", data: {...} } to render server/rr/xml-templates/TemplateName.xml
*
* @param {object} params
* @param {string|object} params.action - RR action key (RR_ACTIONS) or a raw URL/spec
* @param {string|{template:string,data:object}} params.body - Rendered XML or template descriptor
* @param {object} params.socket - The socket or req object for context (used to resolve config & logging)
* @param {object} [params.redisHelpers]
* @param {string|number} [params.jobid]
* @param {object} [params.dealerConfig]
* @param {number} [params.retries=1]
* @returns {Promise<string>} Raw SOAP response text
*/
async function MakeRRCall({ async function MakeRRCall({
action, action,
body, body,
socket, socket,
// redisHelpers, dealerConfig, // optional per-shop overrides
jobid, retries = 1,
dealerConfig, jobid
retries = 1
}) { }) {
const correlationId = uuidv4(); if (!action || !RR_ACTIONS[action]) {
throw new Error(`Invalid RR action: ${action}`);
const effectiveConfig = dealerConfig || (await resolveRRConfig(socket));
const { url, soapAction } = resolveActionTarget(action, effectiveConfig.baseUrl);
// Render body if given by template descriptor
let renderedBody = body;
if (body && typeof body === "object" && body.template) {
renderedBody = await renderXmlTemplate(body.template, body.data || {});
} }
// Build header vars (from env + rr_configuration) const cfg = dealerConfig || (await resolveRRConfig(socket, undefined));
const headerVars = { const baseUrl = cfg.baseUrl;
PPSysId: effectiveConfig.ppsysid || process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID, if (!baseUrl) throw new Error("Missing RR base URL");
DealerNumber: effectiveConfig.dealer_number || effectiveConfig.dealer_id || process.env.RR_DEALER_NUMBER,
StoreNumber: effectiveConfig.store_number || process.env.RR_STORE_NUMBER,
BranchNumber: effectiveConfig.branch_number || process.env.RR_BRANCH_NUMBER,
Username: effectiveConfig.username || process.env.RR_API_USER || process.env.RR_USERNAME,
Password: effectiveConfig.password || process.env.RR_API_PASS || process.env.RR_PASSWORD,
CorrelationId: correlationId
};
// Build full SOAP envelope with proper header // Render STAR business body
const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars); const templateName = body?.template || action;
const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {});
RRLogger(socket, "info", `RR → ${soapAction || "SOAP"} request`, { // Build STAR envelope
let envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea);
const formattedEnvelope = prettyPrintXml(envelope);
// Guardrails
if (!formattedEnvelope.includes("<ProcessMessage") || !formattedEnvelope.includes("<ApplicationArea>")) {
throw new Error("STAR envelope malformed: missing ProcessMessage/ApplicationArea");
}
const headers = { ...RR_SOAP_HEADERS, SOAPAction: RR_STAR_SOAP_ACTION };
RRLogger(socket, "debug", `Sending RR SOAP request`, {
action,
soapAction: RR_STAR_SOAP_ACTION,
endpoint: baseUrl,
jobid, jobid,
url, mode: "STAR"
correlationId
}); });
const headers = { try {
...RR_SOAP_HEADERS, const { data: responseXml } = await axios.post(baseUrl, formattedEnvelope, {
SOAPAction: soapAction, headers,
"Content-Type": "text/xml; charset=utf-8", timeout: cfg.timeout
"X-Request-Id": correlationId // Some RCI tenants require Basic in addition to WSSE
}; // auth: { username: cfg.username, password: cfg.password }
});
let attempt = 0; const parsed = parseRRResponse(responseXml);
while (attempt <= retries) {
attempt += 1; if (!parsed.success) {
try { RRLogger(socket, "error", `RR ${action} failed`, {
const response = await axios.post(url, soapEnvelope, { code: parsed.code,
headers, message: parsed.message
timeout: effectiveConfig.timeout || 30000,
responseType: "text",
validateStatus: () => true
}); });
throw new RrApiError(parsed.message || `RR ${action} failed`, parsed.code || "RR_ERROR");
const text = response.data;
if (response.status >= 400) {
RRLogger(socket, "error", `RR HTTP ${response.status} on ${soapAction || url}`, {
status: response.status,
jobid,
correlationId,
snippet: text?.slice?.(0, 512)
});
if (response.status >= 500 && attempt <= retries) {
RRLogger(socket, "warn", `RR transient HTTP error; retrying (${attempt}/${retries})`, {
correlationId
});
continue;
}
throw new Error(`RR HTTP ${response.status}: ${response.statusText}`);
}
RRLogger(socket, "debug", `RR ← ${soapAction || "SOAP"} response`, {
jobid,
correlationId,
bytes: Buffer.byteLength(text || "", "utf8")
});
return text;
} catch (err) {
const transient = /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up|network error/i.test(
err?.message || ""
);
if (transient && attempt <= retries) {
RRLogger(socket, "warn", `RR transient network error; retrying (${attempt}/${retries})`, {
error: err.message,
correlationId
});
continue;
}
RRLogger(socket, "error", `RR ${soapAction || "SOAP"} failed`, {
error: err.message,
jobid,
correlationId
});
throw err;
} }
RRLogger(socket, "info", `RR ${action} success`, {
result: parsed.code,
message: parsed.message
});
return responseXml;
} catch (err) {
if (retries > 0) {
RRLogger(socket, "warn", `Retrying RR ${action} (${retries - 1} left)`, {
error: err.message
});
return MakeRRCall({
action,
body,
socket,
dealerConfig: cfg,
retries: retries - 1,
jobid
});
}
RRLogger(socket, "error", `RR ${action} failed permanently`, { error: err.message });
throw err;
} }
} }
/* ------------------------------------------------------------------------------------------------
* Exports
* ----------------------------------------------------------------------------------------------*/
const RRActions = RR_ACTIONS;
module.exports = { module.exports = {
MakeRRCall, MakeRRCall,
getDealerConfig,
renderXmlTemplate, renderXmlTemplate,
resolveRRConfig, resolveRRConfig,
parseRRResponse,
buildStarEnvelope,
RRActions RRActions
}; };

View File

@@ -1,133 +1,158 @@
/** /**
* @file rr-job-export.js * @file rr-job-export.js
* @description Orchestrates the full Reynolds & Reynolds DMS export flow. * @description End-to-end export of a Hasura "job" to Reynolds & Reynolds (Rome).
* Creates/updates customers, vehicles, and repair orders according to Rome specs. * Orchestrates Customer (insert/update), optional Vehicle insert, and RO (create/update),
* mirroring behavior of PBS/Fortellis exporters for parity.
*/ */
const { RrCustomerInsert, RrCustomerUpdate } = require("./rr-customer");
const { CreateRepairOrder, UpdateRepairOrder } = require("./rr-repair-orders");
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers");
const { assertRrOkXml, extractRrResponseData } = require("./rr-error");
const RRLogger = require("./rr-logger"); const RRLogger = require("./rr-logger");
const { mapServiceVehicleInsert } = require("./rr-mappers"); const { RrApiError } = require("./rr-error");
const customerApi = require("./rr-customer");
const roApi = require("./rr-repair-orders");
const { MakeRRCall } = require("./rr-helpers"); // for optional vehicle insert
const { mapServiceVehicle } = require("./rr-mappers");
/** /**
* Inserts a service vehicle record for the repair order. * Decide if we should CREATE or UPDATE an entity in Rome based on external IDs
* Follows the "Rome Insert Service Vehicle Interface Specification" via SOAP/XML.
*/ */
async function RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig }) { function decideAction({ customer, vehicle, job }) {
try { const hasCustId = !!(customer?.external_id || customer?.rr_customer_id);
RRLogger(socket, "info", "RR Insert Service Vehicle started", { jobid: JobData?.id }); const hasVehId = !!(vehicle?.external_id || vehicle?.rr_vehicle_id);
const hasRoId = !!(job?.external_id || job?.rr_repair_order_id || job?.dms_repair_order_id);
// Build Mustache variables for server/rr/xml-templates/InsertServiceVehicle.xml return {
const variables = mapServiceVehicleInsert(JobData, dealerConfig); customerAction: hasCustId ? "update" : "insert",
vehicleAction: hasVehId ? "skip" : "insert", // Rome often generates vehicle IDs on RO create; we insert only if we have enough data and no id
const xml = await MakeRRCall({ repairOrderAction: hasRoId ? "update" : "create"
action: RRActions.InsertServiceVehicle, };
body: { template: "InsertServiceVehicle", data: variables },
redisHelpers,
socket,
jobid: JobData.id,
dealerConfig
});
const ok = assertRrOkXml(xml, { apiName: "RR Insert Service Vehicle" });
const normalized = extractRrResponseData(ok, { action: "InsertServiceVehicle" });
RRLogger(socket, "debug", "RR Insert Service Vehicle success", {
jobid: JobData?.id,
vehicleId: normalized?.VehicleId || normalized?.vehicleId
});
return normalized;
} catch (error) {
RRLogger(socket, "error", `RR Insert Service Vehicle failed: ${error.message}`, { jobid: JobData?.id });
throw error;
}
} }
/** /**
* Full DMS export sequence for Reynolds & Reynolds. * Normalize a stage result to a consistent structure.
*
* 1. Ensure customer exists (insert or update)
* 2. Ensure vehicle exists/linked
* 3. Create or update repair order
*/ */
async function ExportJobToRR({ socket, redisHelpers, JobData }) { function stageOk(name, extra = {}) {
const jobid = JobData?.id; return { stage: name, success: true, ...extra };
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; }
function stageFail(name, error) {
return { stage: name, success: false, error: error?.message || String(error) };
}
RRLogger(socket, "info", "Starting RR job export", { jobid, bodyshopId }); /**
* Export a job into Rome (Customer → Vehicle → RepairOrder).
* @param {Socket} socket - logging context (may be null in batch)
* @param {Object} job - Hasura job object (must include customer, vehicle, lines, totals)
* @param {Object} bodyshopConfig - per-shop RR config (dealer/store/branch + creds)
* @param {Object} options - { insertVehicleIfMissing: boolean }
* @returns {Promise<Object>} normalized result
*/
async function exportJobToRome(socket, job, bodyshopConfig, options = {}) {
const { customer = {}, vehicle = {} } = job || {};
const { insertVehicleIfMissing = true } = options;
const actions = decideAction({ customer, vehicle, job });
const stages = [];
const summary = {
dms: "Rome",
jobid: job?.id,
ro_action: actions.repairOrderAction,
customer_action: actions.customerAction,
vehicle_action: insertVehicleIfMissing ? actions.vehicleAction : "skip"
};
RRLogger(socket, "info", `RR Export start`, summary);
// ---- 1) Customer ----
try { try {
// Pull dealer-level overrides once (DB), env/platform secrets come from rr-helpers internally. if (actions.customerAction === "insert") {
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {}; const res = await customerApi.insertCustomer(socket, customer, bodyshopConfig);
stages.push(stageOk("customer.insert"));
// summary.customer_xml = res.xml;
// STEP 1: CUSTOMER
//
RRLogger(socket, "info", "RR Step 1: Customer check/insert", { jobid });
let rrCustomerResult;
if (JobData?.rr_customer_id) {
rrCustomerResult = await RrCustomerUpdate({
socket,
redisHelpers,
JobData,
existingCustomer: { CustomerId: JobData.rr_customer_id },
patch: JobData.customer_patch
});
} else { } else {
rrCustomerResult = await RrCustomerInsert({ socket, redisHelpers, JobData }); const res = await customerApi.updateCustomer(socket, customer, bodyshopConfig);
stages.push(stageOk("customer.update"));
summary.customer_xml = res.xml;
} }
//
// STEP 2: VEHICLE
//
RRLogger(socket, "info", "RR Step 2: Vehicle insert", { jobid });
const rrVehicleResult = await RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig });
//
// STEP 3: REPAIR ORDER
//
RRLogger(socket, "info", "RR Step 3: Repair Order create/update", { jobid });
let rrRepairOrderResult;
if (JobData?.rr_ro_id) {
rrRepairOrderResult = await UpdateRepairOrder({ socket, redisHelpers, JobData });
} else {
rrRepairOrderResult = await CreateRepairOrder({ socket, redisHelpers, JobData });
}
//
// FINALIZE
//
RRLogger(socket, "info", "RR Export completed successfully", {
jobid,
rr_customer_id: rrCustomerResult?.CustomerId || rrCustomerResult?.customerId,
rr_vehicle_id: rrVehicleResult?.VehicleId || rrVehicleResult?.vehicleId,
rr_ro_id: rrRepairOrderResult?.RepairOrderId || rrRepairOrderResult?.repairOrderId
});
return {
success: true,
data: {
customer: rrCustomerResult,
vehicle: rrVehicleResult,
repairOrder: rrRepairOrderResult
}
};
} catch (error) { } catch (error) {
RRLogger(socket, "error", `RR job export failed: ${error.message}`, { jobid }); stages.push(stageFail(`customer.${actions.customerAction}`, error));
return { RRLogger(socket, "error", `RR customer ${actions.customerAction} failed`, {
success: false, jobid: job?.id,
error: error.message, error: error.message
stack: error.stack });
}; throw new RrApiError(`Customer ${actions.customerAction} failed: ${error.message}`, "RR_CUSTOMER_ERROR");
} }
// ---- 2) Vehicle (optional explicit insert) ----
if (insertVehicleIfMissing && actions.vehicleAction === "insert") {
try {
// Only insert when we have at least VIN or plate+state/year
const hasMinimumIdentity = !!(vehicle?.vin || (vehicle?.license_plate && vehicle?.license_state));
if (hasMinimumIdentity) {
const data = mapServiceVehicle(vehicle, customer, bodyshopConfig);
const xml = await MakeRRCall({
action: "InsertServiceVehicle",
body: { template: "InsertServiceVehicle", data },
socket,
dealerConfig: bodyshopConfig,
jobid: job?.id
});
stages.push(stageOk("vehicle.insert"));
summary.vehicle_xml = xml;
} else {
stages.push(stageOk("vehicle.skip", { reason: "insufficient_identity" }));
}
} catch (error) {
stages.push(stageFail("vehicle.insert", error));
RRLogger(socket, "error", `RR vehicle insert failed`, {
jobid: job?.id,
error: error.message
});
// Non-fatal for the overall export — many flows let RO creation create/associate vehicle.
}
} else {
stages.push(stageOk("vehicle.skip", { reason: actions.vehicleAction === "skip" ? "already_has_id" : "disabled" }));
}
// ---- 3) Repair Order ----
try {
let res;
if (actions.repairOrderAction === "create") {
res = await roApi.createRepairOrder(socket, job, bodyshopConfig);
stages.push(stageOk("ro.create"));
} else {
res = await roApi.updateRepairOrder(socket, job, bodyshopConfig);
stages.push(stageOk("ro.update"));
}
summary.ro_xml = res.xml;
} catch (error) {
stages.push(stageFail(`ro.${actions.repairOrderAction}`, error));
RRLogger(socket, "error", `RR RO ${actions.repairOrderAction} failed`, {
jobid: job?.id,
error: error.message
});
throw new RrApiError(`RepairOrder ${actions.repairOrderAction} failed: ${error.message}`, "RR_RO_ERROR");
}
const result = {
success: true,
...summary,
stages
};
RRLogger(socket, "info", `RR Export finished`, {
jobid: job?.id,
result: {
success: result.success,
customer_action: summary.customer_action,
vehicle_action: summary.vehicle_action,
ro_action: summary.ro_action
}
});
return result;
} }
module.exports = { module.exports = {
ExportJobToRR, exportJobToRome
RrServiceVehicleInsert
}; };

View File

@@ -1,50 +1,55 @@
/** /**
* @file rr-logger.js * @file rr-logger.js
* @description Centralized logger for Reynolds & Reynolds (RR) integrations. * @description Structured logger for Reynolds & Reynolds (Rome) integration.
* Emits logs to CloudWatch via logger util, and back to client sockets for live visibility. * Mirrors PBS/Fortellis log shape for consistent log parsing.
*/ */
const logger = require("../utils/logger"); const util = require("util");
const dayjs = require("dayjs");
/** /**
* Create a structured RR log event. * @typedef {Object} LogContext
* * @property {string} [jobid]
* @param {object} socket - The socket or Express request (both supported). * @property {string} [action]
* @param {"debug"|"info"|"warn"|"error"} level - Log level. * @property {string} [stage]
* @param {string} message - Human-readable log message. * @property {string} [endpoint]
* @param {object} [txnDetails] - Optional additional details (payloads, responses, etc.) * @property {Object} [meta]
*/ */
const RRLogger = (socket, level = "info", message, txnDetails = {}) => {
try {
// Normalize level to uppercase for CloudWatch
const levelUpper = level.toUpperCase();
// Safe email and job correlation /**
const userEmail = * Emit a structured log event to console, Socket.IO, or upstream logger.
socket?.user?.email || socket?.request?.user?.email || socket?.handshake?.auth?.email || "unknown@user"; * @param {Socket|null} socket - Optional socket for WsLogger passthrough
* @param {"info"|"debug"|"warn"|"error"} level
* @param {string} message - Primary log message
* @param {LogContext|any} [context]
*/
function RRLogger(socket, level, message, context = {}) {
const logEvent = {
source: "RR",
level,
timestamp: dayjs().toISOString(),
message,
...context
};
const jobid = socket?.JobData?.id || txnDetails?.jobid || null; // Console log (stdout/stderr)
const serialized = `[RR] ${logEvent.timestamp} [${level.toUpperCase()}] ${message}`;
// Main logging entry (to CloudWatch / file) if (level === "error" || level === "warn") {
logger.log("rr-log-event", levelUpper, userEmail, jobid, { console.error(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
wsmessage: message, } else {
txnDetails console.log(serialized, context ? util.inspect(context, { depth: 4, colors: false }) : "");
});
// Emit to live Socket.IO client if available
if (typeof socket.emit === "function") {
socket.emit("rr-log-event", {
level: levelUpper,
message,
txnDetails,
timestamp: new Date().toISOString()
});
}
} catch (err) {
// As a fallback, log directly to console
console.error("RRLogger internal error:", err);
console.error("Original message:", message, txnDetails);
} }
};
// Optional: forward to WsLogger (if your socket is configured that way)
try {
if (socket && typeof socket.emit === "function") {
socket.emit("rr-log-event", logEvent);
} else if (global.WsLogger && typeof global.WsLogger.createLogEvent === "function") {
global.WsLogger.createLogEvent(socket, level.toUpperCase(), message, context.jobid, context);
}
} catch (e) {
console.error("[RRLogger] forwarding error", e.message);
}
}
module.exports = RRLogger; module.exports = RRLogger;

View File

@@ -1,143 +1,136 @@
/** /**
* @file rr-lookup.js * @file rr-lookup.js
* @description Reynolds & Reynolds lookup operations * @description Rome (Reynolds & Reynolds) lookup operations — Advisors, Parts, and CombinedSearch
* (Combined Search, Get Advisors, Get Parts) via SOAP/XML templates.
*/ */
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers"); const { MakeRRCall, parseRRResponse } = require("./rr-helpers");
const { assertRrOkXml, extractRrResponseData } = require("./rr-error"); const { mapAdvisorLookup, mapPartsLookup, mapCombinedSearch } = require("./rr-mappers");
const { mapCombinedSearchVars, mapGetAdvisorsVars, mapGetPartsVars } = require("./rr-mappers");
const RRLogger = require("./rr-logger"); const RRLogger = require("./rr-logger");
const { RrApiError } = require("./rr-error");
/** /**
* Combined Search * Get a list of service advisors from Rome.
* Maps to "Search Customer Service Vehicle Combined" spec (Rome)
*
* @param {object} options
* @param {object} options.socket - Socket or Express req (used for auth + bodyshopId)
* @param {object} options.redisHelpers - (unused, kept for parity)
* @param {string} options.jobid - Job reference for correlation
* @param {Array<[string, string]>} [options.params] - e.g. [["VIN","1HG..."],["LastName","DOE"]]
*/ */
async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) { async function getAdvisors(socket, criteria = {}, bodyshopConfig) {
const action = "GetAdvisors";
const template = "GetAdvisors";
try { try {
RRLogger(socket, "info", "Starting RR Combined Search", { jobid, params }); RRLogger(socket, "info", `Starting RR ${action} lookup`);
const data = mapAdvisorLookup(criteria, bodyshopConfig);
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid; const resultXml = await MakeRRCall({
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {}; action,
body: { template, data },
// Build Mustache variables for server/rr/xml-templates/CombinedSearch.xml
const variables = mapCombinedSearchVars({ params, dealerConfig });
const xml = await MakeRRCall({
action: RRActions.CombinedSearch,
body: { template: "CombinedSearch", data: variables },
redisHelpers,
socket, socket,
jobid dealerConfig: bodyshopConfig
}); });
// Validate + normalize const parsed = parseRRResponse(resultXml);
const ok = assertRrOkXml(xml, { apiName: "RR Combined Search", allowEmpty: true }); if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
const normalized = extractRrResponseData(ok, { action: "CombinedSearch" });
RRLogger(socket, "debug", "RR Combined Search complete", { const advisors = parsed.parsed?.Advisors?.Advisor || parsed.parsed?.AdvisorList?.Advisor || [];
jobid, const advisorList = Array.isArray(advisors) ? advisors : [advisors];
count: Array.isArray(normalized) ? normalized.length : 0
}); RRLogger(socket, "debug", `${action} lookup returned ${advisorList.length} advisors`);
return normalized; return { success: true, dms: "Rome", action, advisors: advisorList };
} catch (error) { } catch (error) {
RRLogger(socket, "error", `RR Combined Search failed: ${error.message}`, { jobid }); RRLogger(socket, "error", `Error in ${action} lookup`, {
throw error; message: error.message,
stack: error.stack
});
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_ADVISORS_ERROR");
} }
} }
/** /**
* Get Advisors * Get parts information from Rome.
* Maps to "Get Advisors Specification" (Rome)
*
* @param {object} options
* @param {object} options.socket
* @param {object} options.redisHelpers
* @param {string} options.jobid
* @param {Array<[string, string]>} [options.params]
*/ */
async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) { async function getParts(socket, criteria = {}, bodyshopConfig) {
const action = "GetParts";
const template = "GetParts";
try { try {
RRLogger(socket, "info", "Starting RR Get Advisors", { jobid, params }); RRLogger(socket, "info", `Starting RR ${action} lookup`);
const data = mapPartsLookup(criteria, bodyshopConfig);
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid; const resultXml = await MakeRRCall({
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {}; action,
body: { template, data },
// Build Mustache variables for server/rr/xml-templates/GetAdvisors.xml
const variables = mapGetAdvisorsVars({ params, dealerConfig });
const xml = await MakeRRCall({
action: RRActions.GetAdvisors,
body: { template: "GetAdvisors", data: variables },
redisHelpers,
socket, socket,
jobid dealerConfig: bodyshopConfig
}); });
const ok = assertRrOkXml(xml, { apiName: "RR Get Advisors", allowEmpty: true }); const parsed = parseRRResponse(resultXml);
const normalized = extractRrResponseData(ok, { action: "GetAdvisors" }); if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
RRLogger(socket, "debug", "RR Get Advisors complete", { const parts = parsed.parsed?.Parts?.Part || parsed.parsed?.PartList?.Part || [];
jobid, const partList = Array.isArray(parts) ? parts : [parts];
count: Array.isArray(normalized) ? normalized.length : 0
}); RRLogger(socket, "debug", `${action} lookup returned ${partList.length} parts`);
return normalized; return { success: true, dms: "Rome", action, parts: partList };
} catch (error) { } catch (error) {
RRLogger(socket, "error", `RR Get Advisors failed: ${error.message}`, { jobid }); RRLogger(socket, "error", `Error in ${action} lookup`, {
throw error; message: error.message,
stack: error.stack
});
throw new RrApiError(`RR ${action} failed: ${error.message}`, "GET_PARTS_ERROR");
} }
} }
/** /**
* Get Parts * Perform a combined customer / vehicle / company search.
* Maps to "Get Part Specification" (Rome) * Equivalent to Rome CombinedSearchRq / Resp.
* * @param {Socket} socket
* @param {object} options * @param {Object} criteria - { VIN, LicensePlate, CustomerName, Phone, Email }
* @param {object} options.socket * @param {Object} bodyshopConfig
* @param {object} options.redisHelpers * @returns {Promise<Object>} { customers, vehicles, companies }
* @param {string} options.jobid
* @param {Array<[string, string]>} [options.params]
*/ */
async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) { async function combinedSearch(socket, criteria = {}, bodyshopConfig) {
const action = "CombinedSearch";
const template = "CombinedSearch";
try { try {
RRLogger(socket, "info", "Starting RR Get Parts", { jobid, params }); RRLogger(socket, "info", `Starting RR ${action} request`);
const data = mapCombinedSearch(criteria, bodyshopConfig);
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid; const resultXml = await MakeRRCall({
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {}; action,
body: { template, data },
// Build Mustache variables for server/rr/xml-templates/GetParts.xml
const variables = mapGetPartsVars({ params, dealerConfig });
const xml = await MakeRRCall({
action: RRActions.GetParts,
body: { template: "GetParts", data: variables },
redisHelpers,
socket, socket,
jobid dealerConfig: bodyshopConfig
}); });
const ok = assertRrOkXml(xml, { apiName: "RR Get Parts", allowEmpty: true }); const parsed = parseRRResponse(resultXml);
const normalized = extractRrResponseData(ok, { action: "GetParts" }); if (!parsed.success) throw new RrApiError(parsed.message, parsed.code);
RRLogger(socket, "debug", "RR Get Parts complete", { const customers = parsed.parsed?.Customers?.Customer || [];
jobid, const vehicles = parsed.parsed?.Vehicles?.ServiceVehicle || [];
count: Array.isArray(normalized) ? normalized.length : 0 const companies = parsed.parsed?.Companies?.Company || [];
});
return normalized; const result = {
customers: Array.isArray(customers) ? customers : [customers],
vehicles: Array.isArray(vehicles) ? vehicles : [vehicles],
companies: Array.isArray(companies) ? companies : [companies]
};
RRLogger(
socket,
"debug",
`${action} returned ${result.customers.length} customers, ${result.vehicles.length} vehicles, ${result.companies.length} companies`
);
return { success: true, dms: "Rome", action, ...result };
} catch (error) { } catch (error) {
RRLogger(socket, "error", `RR Get Parts failed: ${error.message}`, { jobid }); RRLogger(socket, "error", `Error in ${action}`, {
throw error; message: error.message,
stack: error.stack
});
throw new RrApiError(`RR ${action} failed: ${error.message}`, "COMBINED_SEARCH_ERROR");
} }
} }
module.exports = { module.exports = {
RrCombinedSearch, getAdvisors,
RrGetAdvisors, getParts,
RrGetParts combinedSearch
}; };

View File

@@ -1,424 +1,412 @@
// server/rr/rr-mappers.js /**
// ----------------------------------------------------------------------------- * @file rr-mappers.js
// Centralized mapping for Reynolds & Reynolds (RR) XML templates. * @description Maps internal ImEX (Hasura) entities into Rome (Reynolds & Reynolds) XML structures.
// These functions take our domain objects (JobData, txEnvelope, current/patch) * Each function returns a plain JS object that matches Mustache templates in xml-templates/.
// and produce the Mustache variable objects expected by the RR XML templates in */
// /server/rr/xml-templates.
const dayjs = require("dayjs");
/**
* Utility: formats date/time to R&Rs preferred format (ISO or yyyy-MM-dd).
*/
const formatDate = (val) => {
if (!val) return undefined;
return dayjs(val).format("YYYY-MM-DD");
};
/**
* Utility: safely pick numeric values and stringify for XML.
*/
const num = (val) => (val != null ? String(val) : undefined);
const toBoolStr = (v) => (v === true ? "true" : v === false ? "false" : undefined);
const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined && v !== null && v !== "");
// //
// NOTE: This is still scaffolding. Where “TODO (spec)” appears, fill in the // ===================== CUSTOMER =====================
// exact RR field semantics (type restrictions, enums, required/optional) based
// on the Rome RR PDFs you shared.
// //
// Templates these map into (variable names must match):
// - InsertCustomer.xml: <rr:CustomerInsertRq/>
// - UpdateCustomer.xml: <rr:CustomerUpdateRq/>
// - InsertServiceVehicle.xml: <rr:ServiceVehicleAddRq/>
// - CreateRepairOrder.xml: <rr:RepairOrderInsertRq/>
// - UpdateRepairOrder.xml: <rr:RepairOrderChgRq/>
//
// All map* functions below return a plain object shaped for Mustache rendering.
// -----------------------------------------------------------------------------
const _ = require("lodash"); /**
* Map internal customer record to Rome CustomerInsertRq.
*/
function mapCustomerInsert(customer, bodyshopConfig) {
if (!customer) return {};
// Keep this consistent with other providers (sanitize strings for XML) return {
const REPLACE_SPECIAL = /[^a-zA-Z0-9 .,\n#\-()/]+/g; DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: bodyshopConfig?.dealer_number,
StoreNumber: bodyshopConfig?.store_number,
BranchNumber: bodyshopConfig?.branch_number,
RequestId: `CUST-INSERT-${customer.id}`,
Environment: process.env.NODE_ENV,
function sanitize(v) { CustomerId: customer.external_id || undefined,
if (v === null || v === undefined) return null; CustomerType: customer.type || "RETAIL",
return String(v).replace(REPLACE_SPECIAL, "").trim(); CompanyName: customer.company_name,
} FirstName: customer.first_name,
MiddleName: customer.middle_name,
LastName: customer.last_name,
PreferredName: customer.display_name || customer.first_name,
ActiveFlag: customer.active ? "true" : "false",
function upper(v) { CustomerGroup: customer.group_name,
const s = sanitize(v); TaxExempt: customer.tax_exempt ? "true" : "false",
return s ? s.toUpperCase() : null; DiscountLevel: num(customer.discount_level),
} PreferredLanguage: customer.language || "EN",
function asNumberOrNull(v) { Addresses: (customer.addresses || []).map((a) => ({
if (v === null || v === undefined || v === "") return null; AddressType: a.type || "BILLING",
const n = Number(v); AddressLine1: a.line1,
return Number.isFinite(n) ? n : null; AddressLine2: a.line2,
} City: a.city,
State: a.state,
PostalCode: a.postal_code,
Country: a.country || "US"
})),
function normalizePostal(raw) { Phones: (customer.phones || []).map((p) => ({
if (!raw) return null; PhoneNumber: p.number,
const s = String(raw).toUpperCase().replace(/\s+/g, ""); PhoneType: p.type || "MOBILE",
// If Canadian format (A1A1A1), keep as-is. Otherwise return raw sanitized. Preferred: p.preferred ? "true" : "false"
return s.length === 6 ? `${s.slice(0, 3)} ${s.slice(3)}` : sanitize(raw); })),
Emails: (customer.emails || []).map((e) => ({
EmailAddress: e.address,
EmailType: e.type || "WORK",
Preferred: e.preferred ? "true" : "false"
})),
Insurance: customer.insurance
? {
CompanyName: customer.insurance.company,
PolicyNumber: customer.insurance.policy,
ExpirationDate: formatDate(customer.insurance.expiration_date),
ContactName: customer.insurance.contact_name,
ContactPhone: customer.insurance.contact_phone
}
: undefined,
LinkedAccounts: (customer.linked_accounts || []).map((a) => ({
Type: a.type,
AccountNumber: a.account_number,
CreditLimit: num(a.credit_limit)
})),
Notes: customer.notes?.length ? { Items: customer.notes.map((n) => n.text || n) } : undefined
};
} }
/** /**
* Compose the dealer section used by every template. * Map internal customer record to Rome CustomerUpdateRq.
* We prefer dealer-level rr_configuration first; fallback to env.
*/ */
function buildDealerVars(dealerCfg = {}) { function mapCustomerUpdate(customer, bodyshopConfig) {
if (!customer) return {};
return { return {
DealerCode: dealerCfg.dealerCode || process.env.RR_DEALER_CODE || null, ...mapCustomerInsert(customer, bodyshopConfig),
DealerName: dealerCfg.dealerName || process.env.RR_DEALER_NAME || null, RequestId: `CUST-UPDATE-${customer.id}`
DealerNumber: dealerCfg.dealerNumber || process.env.RR_DEALER_NUMBER || null,
StoreNumber: dealerCfg.storeNumber || process.env.RR_STORE_NUMBER || null,
BranchNumber: dealerCfg.branchNumber || process.env.RR_BRANCH_NUMBER || null
}; };
} }
/* ------------------------------- Phones/Emails ------------------------------- */ //
// ===================== VEHICLE =====================
//
function mapPhones({ ph1, ph2, mobile }) { /**
// TODO (spec): adjust PhoneType enumerations if RR requires strict codes. * Map vehicle to Rome ServiceVehicleAddRq.
const out = []; */
if (ph1) out.push({ PhoneNumber: sanitize(ph1), PhoneType: "HOME" }); function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
if (ph2) out.push({ PhoneNumber: sanitize(ph2), PhoneType: "WORK" }); if (!vehicle) return {};
if (mobile) out.push({ PhoneNumber: sanitize(mobile), PhoneType: "MOBILE" });
return out;
}
function mapEmails({ email }) {
if (!email) return [];
// TODO (spec): include EmailType (e.g., PERSONAL/WORK) if RR mandates it.
return [{ EmailAddress: sanitize(email), EmailType: "PERSONAL" }];
}
/* -------------------------------- Addresses -------------------------------- */
function mapPostalAddressFromJob(job) {
return [
{
AddressLine1: sanitize(job.ownr_addr1),
AddressLine2: sanitize(job.ownr_addr2),
City: upper(job.ownr_city),
State: upper(job.ownr_st || job.ownr_state),
PostalCode: normalizePostal(job.ownr_zip),
Country: upper(job.ownr_ctry) || "USA"
}
].filter((addr) => Object.values(addr).some(Boolean));
}
/* --------------------------------- Customer -------------------------------- */
function mapCustomerInsert(job, dealerCfg = {}) {
const dealer = buildDealerVars(dealerCfg);
const isCompany = Boolean(job?.ownr_co_nm && String(job.ownr_co_nm).trim() !== "");
return { return {
...dealer, DealerCode: bodyshopConfig?.dealer_code || "ROME",
// Envelope metadata (optional) DealerNumber: bodyshopConfig?.dealer_number,
RequestId: job?.id || null, StoreNumber: bodyshopConfig?.store_number,
Environment: process.env.NODE_ENV || "development", BranchNumber: bodyshopConfig?.branch_number,
RequestId: `VEH-${vehicle.id}`,
// Customer node (see InsertCustomer.xml) CustomerId: ownerCustomer?.external_id,
CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
CompanyName: isCompany ? upper(job.ownr_co_nm) : null,
FirstName: !isCompany ? upper(job.ownr_fn) : null,
LastName: !isCompany ? upper(job.ownr_ln) : null,
ActiveFlag: "Y",
Addresses: mapPostalAddressFromJob(job), VIN: vehicle.vin,
Phones: mapPhones({ ph1: job.ownr_ph1, ph2: job.ownr_ph2, mobile: job.ownr_mobile }), UnitNumber: vehicle.unit_number,
Emails: mapEmails({ email: job.ownr_ea }), StockNumber: vehicle.stock_number,
Year: num(vehicle.year),
Make: vehicle.make,
Model: vehicle.model,
Trim: vehicle.trim,
BodyStyle: vehicle.body_style,
Transmission: vehicle.transmission,
Engine: vehicle.engine,
FuelType: vehicle.fuel_type,
DriveType: vehicle.drive_type,
Color: vehicle.color,
LicensePlate: vehicle.license_plate,
LicenseState: vehicle.license_state,
Odometer: num(vehicle.odometer),
OdometerUnits: vehicle.odometer_units || "KM",
InServiceDate: formatDate(vehicle.in_service_date),
// Optional blocks (keep null unless you truly have values) Insurance: vehicle.insurance
DriverLicense: null, // { LicenseNumber, LicenseState, ExpirationDate } ? {
Insurance: null, // { CompanyName, PolicyNumber, ExpirationDate } CompanyName: vehicle.insurance.company,
Notes: null // { Note } PolicyNumber: vehicle.insurance.policy,
ExpirationDate: formatDate(vehicle.insurance.expiration_date)
}
: undefined,
Warranty: vehicle.warranty
? {
WarrantyCompany: vehicle.warranty.company,
WarrantyNumber: vehicle.warranty.number,
WarrantyType: vehicle.warranty.type,
ExpirationDate: formatDate(vehicle.warranty.expiration_date)
}
: undefined,
VehicleNotes: vehicle.notes?.length ? { Items: vehicle.notes.map((n) => n.text || n) } : undefined
}; };
} }
function mapCustomerUpdate(existingCustomer, patch = {}, dealerCfg = {}) { //
const dealer = buildDealerVars(dealerCfg); // ===================== REPAIR ORDER =====================
// We merge and normalize so callers can pass minimal deltas //
const merged = _.merge({}, existingCustomer || {}, patch || {});
const id =
merged?.CustomerId ||
merged?.customerId ||
merged?.id ||
merged?.customer?.id ||
patch?.CustomerId ||
patch?.customerId ||
null;
// Derive company vs individual /**
const isCompany = Boolean(merged?.CompanyName || merged?.customerName?.companyName); * Map internal job to Rome RepairOrderInsertRq.
*/
function mapRepairOrderCreate(job, bodyshopConfig) {
if (!job) return {};
const nameBlock = { const cust = job.customer || {};
CompanyName: isCompany ? upper(merged?.CompanyName || merged?.customerName?.companyName) : null, const veh = job.vehicle || {};
FirstName: !isCompany ? upper(merged?.FirstName || merged?.customerName?.firstName) : null,
LastName: !isCompany ? upper(merged?.LastName || merged?.customerName?.lastName) : null
};
// Addresses return {
const addr = DealerCode: bodyshopConfig?.dealer_code || "ROME",
merged?.Addresses || DealerNumber: bodyshopConfig?.dealer_number,
merged?.postalAddress || StoreNumber: bodyshopConfig?.store_number,
(merged?.addressLine1 || merged?.addressLine2 || merged?.city BranchNumber: bodyshopConfig?.branch_number,
? [ RequestId: `RO-${job.id}`,
{ Environment: process.env.NODE_ENV,
AddressLine1: sanitize(merged?.addressLine1),
AddressLine2: sanitize(merged?.addressLine2), RepairOrderNumber: job.ro_number,
City: upper(merged?.city), DmsRepairOrderId: job.external_id,
State: upper(merged?.state || merged?.province),
PostalCode: normalizePostal(merged?.postalCode), OpenDate: formatDate(job.open_date),
Country: upper(merged?.country) || "USA" PromisedDate: formatDate(job.promised_date),
CloseDate: formatDate(job.close_date),
ServiceAdvisorId: job.advisor_id,
TechnicianId: job.technician_id,
Department: job.department,
ProfitCenter: job.profit_center,
ROType: job.ro_type,
Status: job.status,
IsBodyShop: "true",
DRPFlag: job.drp_flag ? "true" : "false",
Customer: {
CustomerId: cust.external_id,
CustomerName: cust.full_name,
PhoneNumber: cust.phone,
EmailAddress: cust.email
},
Vehicle: {
VehicleId: veh.external_id,
VIN: veh.vin,
LicensePlate: veh.license_plate,
Year: num(veh.year),
Make: veh.make,
Model: veh.model,
Odometer: num(veh.odometer),
Color: veh.color
},
JobLines: (job.joblines || []).map((l, i) => ({
Sequence: i + 1,
ParentSequence: l.parent_sequence,
LineType: l.line_type,
Category: l.category,
OpCode: l.op_code,
Description: l.description,
LaborHours: num(l.labor_hours),
LaborRate: num(l.labor_rate),
PartNumber: l.part_number,
PartDescription: l.part_description,
Quantity: num(l.quantity),
UnitPrice: num(l.unit_price),
ExtendedPrice: num(l.extended_price),
DiscountAmount: num(l.discount_amount),
TaxCode: l.tax_code,
GLAccount: l.gl_account,
ControlNumber: l.control_number,
Taxes: l.taxes?.length
? {
Items: l.taxes.map((t) => ({
Code: t.code,
Amount: num(t.amount),
Rate: num(t.rate)
}))
} }
] : undefined
: null);
// Phones & Emails
const phones = merged?.Phones || merged?.contactMethods?.phones || [];
const emails = merged?.Emails || merged?.contactMethods?.emailAddresses || [];
return {
...dealer,
RequestId: merged?.RequestId || null,
Environment: process.env.NODE_ENV || "development",
CustomerId: id,
CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
...nameBlock,
ActiveFlag: merged?.ActiveFlag || "Y",
Addresses: addr,
Phones: phones.map((p) => ({ PhoneNumber: sanitize(p.PhoneNumber || p.number), PhoneType: p.PhoneType || p.type })),
Emails: emails.map((e) => ({
EmailAddress: sanitize(e.EmailAddress || e.address),
EmailType: e.EmailType || e.type || "PERSONAL"
})), })),
// Optional Totals: {
DriverLicense: merged?.DriverLicense || null, Currency: job.currency || "CAD",
Insurance: merged?.Insurance || null, LaborTotal: num(job.totals?.labor),
Notes: merged?.Notes || null PartsTotal: num(job.totals?.parts),
}; MiscTotal: num(job.totals?.misc),
} DiscountTotal: num(job.totals?.discount),
TaxTotal: num(job.totals?.tax),
GrandTotal: num(job.totals?.grand)
},
/* --------------------------------- Vehicle --------------------------------- */ Payments: job.payments?.length
function mapVehicleInsertFromJob(job, dealerCfg = {}, opts = {}) {
// opts: { customerId }
const dealer = buildDealerVars(dealerCfg);
return {
...dealer,
RequestId: job?.id || null,
Environment: process.env.NODE_ENV || "development",
CustomerId: opts?.customerId || null,
VIN: upper(job?.v_vin),
Year: asNumberOrNull(job?.v_model_yr),
Make: upper(job?.v_make),
Model: upper(job?.v_model),
Trim: upper(job?.v_trim),
BodyStyle: upper(job?.v_body),
Transmission: upper(job?.v_transmission),
Engine: upper(job?.v_engine),
FuelType: upper(job?.v_fuel),
Color: upper(job?.v_color),
Odometer: asNumberOrNull(job?.odometer_out || job?.kmout),
LicensePlate: upper(job?.plate_no),
LicenseState: upper(job?.plate_state),
Ownership: null,
Insurance: null,
VehicleNotes: null,
Warranty: null
};
}
/* ------------------------------- Repair Orders ------------------------------ */
function mapRepairOrderAddFromJob(job, txEnvelope = {}, dealerCfg = {}) {
const dealer = buildDealerVars(dealerCfg);
const customerVars = {
CustomerId: job?.customer?.id || txEnvelope?.customerId || null,
CustomerName:
upper(job?.ownr_co_nm) || [upper(job?.ownr_fn), upper(job?.ownr_ln)].filter(Boolean).join(" ").trim() || null,
PhoneNumber: sanitize(job?.ownr_ph1 || job?.ownr_mobile || job?.ownr_ph2),
EmailAddress: sanitize(job?.ownr_ea)
};
const vehicleVars = {
VIN: upper(job?.v_vin),
LicensePlate: upper(job?.plate_no),
Year: asNumberOrNull(job?.v_model_yr),
Make: upper(job?.v_make),
Model: upper(job?.v_model),
Odometer: asNumberOrNull(job?.odometer_out || job?.kmout),
Color: upper(job?.v_color)
};
return {
...dealer,
RequestId: job?.id || null,
Environment: process.env.NODE_ENV || "development",
RepairOrderNumber: sanitize(job?.ro_number) || sanitize(txEnvelope?.reference) || null,
OpenDate: txEnvelope?.openedAt || job?.actual_in || null,
PromisedDate: txEnvelope?.promisedAt || job?.promise_date || null,
CloseDate: txEnvelope?.closedAt || job?.invoice_date || null,
ServiceAdvisorId: txEnvelope?.advisorId || job?.service_advisor_id || null,
TechnicianId: txEnvelope?.technicianId || job?.technician_id || null,
ROType: txEnvelope?.roType || "CUSTOMER_PAY", // TODO (spec): map from our job type(s)
Status: txEnvelope?.status || "OPEN",
CustomerId: customerVars.CustomerId,
CustomerName: customerVars.CustomerName,
PhoneNumber: customerVars.PhoneNumber,
EmailAddress: customerVars.EmailAddress,
VIN: vehicleVars.VIN,
LicensePlate: vehicleVars.LicensePlate,
Year: vehicleVars.Year,
Make: vehicleVars.Make,
Model: vehicleVars.Model,
Odometer: vehicleVars.Odometer,
Color: vehicleVars.Color,
JobLines: (job?.joblines || txEnvelope?.lines || []).map((ln, idx) => mapJobLineToRRLine(ln, idx + 1)),
Totals: txEnvelope?.totals
? { ? {
LaborTotal: asNumberOrNull(txEnvelope.totals.labor), Items: job.payments.map((p) => ({
PartsTotal: asNumberOrNull(txEnvelope.totals.parts), PayerType: p.payer_type,
MiscTotal: asNumberOrNull(txEnvelope.totals.misc), PayerName: p.payer_name,
TaxTotal: asNumberOrNull(txEnvelope.totals.tax), Amount: num(p.amount),
GrandTotal: asNumberOrNull(txEnvelope.totals.total) Method: p.method,
Reference: p.reference,
ControlNumber: p.control_number
}))
} }
: null, : undefined,
Insurance: txEnvelope?.insurance Insurance: job.insurance
? { ? {
CompanyName: upper(txEnvelope.insurance.company), CompanyName: job.insurance.company,
ClaimNumber: sanitize(txEnvelope.insurance.claim), ClaimNumber: job.insurance.claim_number,
AdjusterName: upper(txEnvelope.insurance.adjuster), AdjusterName: job.insurance.adjuster_name,
AdjusterPhone: sanitize(txEnvelope.insurance.phone) AdjusterPhone: job.insurance.adjuster_phone
} }
: null, : undefined,
Notes: txEnvelope?.story ? { Note: sanitize(txEnvelope.story) } : null Notes: job.notes?.length ? { Items: job.notes.map((n) => n.text || n) } : undefined
}; };
} }
function mapRepairOrderChangeFromJob(current, delta = {}, dealerCfg = {}) { /**
// current: existing RO (our cached shape) * Map for repair order updates.
// delta: patch object describing header fields and line changes */
const dealer = buildDealerVars(dealerCfg); function mapRepairOrderUpdate(job, bodyshopConfig) {
return {
...mapRepairOrderCreate(job, bodyshopConfig),
RequestId: `RO-UPDATE-${job.id}`
};
}
const added = (delta.addedLines || []).map((ln, i) => //
mapJobLineToRRLine(ln, ln.Sequence || ln.seq || i + 1, { includePayType: true }) // ===================== LOOKUPS =====================
); //
const updated = (delta.updatedLines || []).map((ln) => ({
...mapJobLineToRRLine(ln, ln.Sequence || ln.seq, { includePayType: true }),
ChangeType: ln.ChangeType || ln.change || null,
LineId: ln.LineId || null
}));
const removed = (delta.removedLines || []).map((ln) => ({
LineId: ln.LineId || null,
Sequence: ln.Sequence || ln.seq || null,
OpCode: upper(ln.OpCode || ln.opCode) || null,
Reason: sanitize(ln.Reason || ln.reason) || null
}));
const totals = delta?.totals function mapAdvisorLookup(criteria, bodyshopConfig) {
? { return {
LaborTotal: asNumberOrNull(delta.totals.labor), DealerCode: bodyshopConfig?.dealer_code || "ROME",
PartsTotal: asNumberOrNull(delta.totals.parts), DealerNumber: bodyshopConfig?.dealer_number,
MiscTotal: asNumberOrNull(delta.totals.misc), StoreNumber: bodyshopConfig?.store_number,
TaxTotal: asNumberOrNull(delta.totals.tax), BranchNumber: bodyshopConfig?.branch_number,
GrandTotal: asNumberOrNull(delta.totals.total) RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
} SearchCriteria: {
: null; Department: criteria.department || "Body Shop",
Status: criteria.status || "ACTIVE"
}
};
}
const insurance = delta?.insurance function mapPartsLookup(criteria, bodyshopConfig) {
? { return {
CompanyName: upper(delta.insurance.company), DealerCode: bodyshopConfig?.dealer_code || "ROME",
ClaimNumber: sanitize(delta.insurance.claim), DealerNumber: bodyshopConfig?.dealer_number,
AdjusterName: upper(delta.insurance.adjuster), StoreNumber: bodyshopConfig?.store_number,
AdjusterPhone: sanitize(delta.insurance.phone) BranchNumber: bodyshopConfig?.branch_number,
} RequestId: `LOOKUP-PART-${Date.now()}`,
: null; SearchCriteria: {
PartNumber: criteria.part_number,
Description: criteria.description,
Make: criteria.make,
Model: criteria.model,
Year: num(criteria.year),
Category: criteria.category,
MaxResults: criteria.max_results || 25
}
};
}
const notes = function mapCombinedSearch(criteria = {}, bodyshopConfig) {
Array.isArray(delta?.notes) && delta.notes.length // accept nested or flat input
? { Items: delta.notes.map((n) => sanitize(n)).filter(Boolean) } const c = criteria || {};
: null; const cust = c.customer || c.Customer || {};
const veh = c.vehicle || c.Vehicle || {};
const comp = c.company || c.Company || {};
// build optional blocks only if they have at least one value
const customerBlock = {
FirstName: cust.firstName || cust.FirstName || c.firstName,
LastName: cust.lastName || cust.LastName || c.lastName,
PhoneNumber: cust.phoneNumber || cust.PhoneNumber || c.phoneNumber || c.phone,
EmailAddress: cust.email || cust.EmailAddress || c.email,
CompanyName: cust.companyName || cust.CompanyName || c.companyName,
CustomerId: cust.customerId || cust.CustomerId || c.customerId
};
const vehicleBlock = {
VIN: veh.vin || veh.VIN || c.vin,
LicensePlate: veh.licensePlate || veh.LicensePlate || c.licensePlate,
Make: veh.make || veh.Make || c.make,
Model: veh.model || veh.Model || c.model,
Year: veh.year != null ? String(veh.year) : c.year != null ? String(c.year) : undefined,
VehicleId: veh.vehicleId || veh.VehicleId || c.vehicleId
};
const companyBlock = {
Name: comp.name || comp.Name || c.companyName,
Phone: comp.phone || comp.Phone || c.companyPhone
};
return { return {
...dealer, // Dealer / routing (aligns with your other mappers)
RequestId: delta?.RequestId || current?.RequestId || null, STAR_NS: require("./rr-constants").RR_NS.STAR,
Environment: process.env.NODE_ENV || "development", MaxRecs: criteria.maxResults || criteria.MaxResults || 50,
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerName: bodyshopConfig?.dealer_name,
DealerNumber: bodyshopConfig?.dealer_number,
StoreNumber: bodyshopConfig?.store_number,
BranchNumber: bodyshopConfig?.branch_number,
RepairOrderId: current?.RepairOrderId || delta?.RepairOrderId || null, RequestId: c.requestId || `COMBINED-${Date.now()}`,
RepairOrderNumber: delta?.RepairOrderNumber || current?.RepairOrderNumber || null, Environment: process.env.NODE_ENV,
Status: delta?.Status || null,
ROType: delta?.ROType || null,
OpenDate: delta?.OpenDate || null,
PromisedDate: delta?.PromisedDate || null,
CloseDate: delta?.CloseDate || null,
ServiceAdvisorId: delta?.ServiceAdvisorId || null,
TechnicianId: delta?.TechnicianId || null,
LocationCode: delta?.LocationCode || null,
Department: delta?.Department || null,
PurchaseOrder: delta?.PurchaseOrder || null,
// Optional customer/vehicle patches // Only include these blocks when they have content; Mustache {{#Block}} respects undefined
Customer: delta?.Customer || null, Customer: hasAny(customerBlock) ? customerBlock : undefined,
Vehicle: delta?.Vehicle || null, Vehicle: hasAny(vehicleBlock) ? vehicleBlock : undefined, // template wraps as <rr:ServiceVehicle>…</rr:ServiceVehicle>
Company: hasAny(companyBlock) ? companyBlock : undefined,
// Line changes // Search behavior flags
AddedJobLines: added.length ? added : null, SearchMode: c.searchMode || c.SearchMode, // EXACT | PARTIAL
UpdatedJobLines: updated.length ? updated : null, ExactMatch: toBoolStr(c.exactMatch ?? c.ExactMatch),
RemovedJobLines: removed.length ? removed : null, PartialMatch: toBoolStr(c.partialMatch ?? c.PartialMatch),
CaseInsensitive: toBoolStr(c.caseInsensitive ?? c.CaseInsensitive),
Totals: totals, // Result shaping (default to true when unspecified)
Insurance: insurance, ReturnCustomers: toBoolStr(c.returnCustomers ?? c.ReturnCustomers ?? true),
Notes: notes ReturnVehicles: toBoolStr(c.returnVehicles ?? c.ReturnVehicles ?? true),
ReturnCompanies: toBoolStr(c.returnCompanies ?? c.ReturnCompanies ?? true),
// Paging / sorting
MaxResults: c.maxResults ?? c.MaxResults,
PageNumber: c.pageNumber ?? c.PageNumber,
SortBy: c.sortBy ?? c.SortBy, // e.g., NAME, VIN, PARTNUMBER
SortDirection: c.sortDirection ?? c.SortDirection // ASC | DESC
}; };
} }
/* ------------------------------- Line Mapping ------------------------------- */
function mapJobLineToRRLine(line, sequenceFallback, opts = {}) {
// opts.includePayType => include PayType when present (CUST|INS|WARR|INT)
const qty = asNumberOrNull(line?.Quantity || line?.qty || line?.part_qty || 1);
const unit = asNumberOrNull(line?.UnitPrice || line?.price || line?.unitPrice);
const ext = asNumberOrNull(line?.ExtendedPrice || (qty && unit ? qty * unit : line?.extended));
return {
Sequence: asNumberOrNull(line?.Sequence || line?.seq) || asNumberOrNull(sequenceFallback),
OpCode: upper(line?.OpCode || line?.opCode || line?.opcode),
Description: sanitize(line?.Description || line?.description || line?.desc || line?.story),
LaborHours: asNumberOrNull(line?.LaborHours || line?.laborHours),
LaborRate: asNumberOrNull(line?.LaborRate || line?.laborRate),
PartNumber: upper(line?.PartNumber || line?.partNumber || line?.part_no),
PartDescription: sanitize(line?.PartDescription || line?.partDescription || line?.part_desc),
Quantity: qty,
UnitPrice: unit,
ExtendedPrice: ext,
TaxCode: upper(line?.TaxCode || line?.taxCode) || null,
PayType: opts.includePayType ? upper(line?.PayType || line?.payType) || null : undefined,
Reason: sanitize(line?.Reason || line?.reason) || null
};
}
// -----------------------------------------------------------------------------
module.exports = { module.exports = {
// Customer
mapCustomerInsert, mapCustomerInsert,
mapCustomerUpdate, mapCustomerUpdate,
mapServiceVehicle,
// Vehicle mapRepairOrderCreate,
mapVehicleInsertFromJob, mapRepairOrderUpdate,
mapAdvisorLookup,
// Repair orders mapPartsLookup,
mapRepairOrderAddFromJob, mapCombinedSearch
mapRepairOrderChangeFromJob,
mapJobLineToRRLine,
// shared utils (handy in tests)
buildDealerVars,
_sanitize: sanitize,
_upper: upper,
_normalizePostal: normalizePostal
}; };

View File

@@ -1,144 +1,99 @@
/** /**
* @file rr-repair-orders.js * @file rr-repair-orders.js
* @description Reynolds & Reynolds (Rome) Repair Order Create & Update. * @description Rome (Reynolds & Reynolds) Repair Order Integration.
* Implements the "Create Body Shop Management Repair Order" and * Handles creation and updates of repair orders (BSMRepairOrderRq/Resp).
* "Update Body Shop Management Repair Order" specifications.
*/ */
const { MakeRRCall, RRActions } = require("./rr-helpers"); const { MakeRRCall } = require("./rr-helpers");
const { assertRrOk } = require("./rr-error");
const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers"); const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers");
const RRLogger = require("./rr-logger"); const RRLogger = require("./rr-logger");
const { client } = require("../graphql-client/graphql-client"); const { RrApiError } = require("./rr-error");
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
/** /**
* Fetch rr_configuration for the current bodyshop directly from DB. * Create a new repair order in Rome.
* Dealer-specific configuration is mandatory for RR operations. * @param {Socket} socket - active socket connection
* @param {Object} job - Hasura job object (including vehicle, customer, joblines)
* @param {Object} bodyshopConfig - DMS config for current bodyshop
* @returns {Promise<Object>} normalized result
*/ */
async function getDealerConfigFromDB(bodyshopId, logger) { async function createRepairOrder(socket, job, bodyshopConfig) {
const action = "CreateRepairOrder";
const template = "CreateRepairOrder"; // maps to xml-templates/CreateRepairOrder.xml
try { try {
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId }); RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`);
const config = result?.bodyshops_by_pk?.rr_configuration || null;
if (!config) { const data = mapRepairOrderCreate(job, bodyshopConfig);
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
}
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config); const resultXml = await MakeRRCall({
return config; action,
body: { template, data },
socket,
dealerConfig: bodyshopConfig,
jobid: job.id
});
RRLogger(socket, "debug", `${action} completed successfully`, { jobid: job.id });
return {
success: true,
dms: "Rome",
jobid: job.id,
action,
xml: resultXml
};
} catch (error) { } catch (error) {
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, { RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, {
bodyshopId,
message: error.message, message: error.message,
stack: error.stack stack: error.stack
}); });
throw error; throw new RrApiError(`RR CreateRepairOrder failed: ${error.message}`, "CREATE_RO_ERROR");
} }
} }
/** /**
* CREATE REPAIR ORDER * Update an existing repair order in Rome.
* Based on "Rome Create Body Shop Management Repair Order Specification" * @param {Socket} socket
* * @param {Object} job
* @param {object} options * @param {Object} bodyshopConfig
* @param {object} options.socket - socket or express request * @returns {Promise<Object>}
* @param {object} options.redisHelpers
* @param {object} options.JobData - internal job object
* @param {object} [options.txEnvelope] - transaction metadata (advisor, timestamps, etc.)
*/ */
async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { async function updateRepairOrder(socket, job, bodyshopConfig) {
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; const action = "UpdateRepairOrder";
const logger = socket?.logger || console; const template = "UpdateRepairOrder";
try { try {
RRLogger(socket, "info", "RR Create Repair Order started", { RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`);
jobid: JobData?.id,
bodyshopId
});
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger); const data = mapRepairOrderUpdate(job, bodyshopConfig);
// Build Mustache variables for server/rr/xml-templates/CreateRepairOrder.xml const resultXml = await MakeRRCall({
const vars = mapRepairOrderCreate({ JobData, txEnvelope, dealerConfig }); action,
body: { template, data },
const data = await MakeRRCall({
action: RRActions.CreateRepairOrder, // resolves SOAPAction+URL
body: { template: "CreateRepairOrder", data: vars }, // render XML template
redisHelpers,
socket, socket,
jobid: JobData.id dealerConfig: bodyshopConfig,
jobid: job.id
}); });
const response = assertRrOk(data, { apiName: "RR Create Repair Order" }); RRLogger(socket, "debug", `${action} completed successfully`, { jobid: job.id });
RRLogger(socket, "debug", "RR Create Repair Order success", { return {
jobid: JobData?.id, success: true,
dealer: dealerConfig?.dealer_code || dealerConfig?.dealerCode dms: "Rome",
}); jobid: job.id,
action,
return response; xml: resultXml
};
} catch (error) { } catch (error) {
RRLogger(socket, "error", `RR Create Repair Order failed: ${error.message}`, { RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, {
jobid: JobData?.id message: error.message,
stack: error.stack
}); });
throw error; throw new RrApiError(`RR UpdateRepairOrder failed: ${error.message}`, "UPDATE_RO_ERROR");
}
}
/**
* UPDATE REPAIR ORDER
* Based on "Rome Update Body Shop Management Repair Order Specification"
*
* @param {object} options
* @param {object} options.socket
* @param {object} options.redisHelpers
* @param {object} options.JobData
* @param {object} [options.txEnvelope]
*/
async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
const logger = socket?.logger || console;
try {
RRLogger(socket, "info", "RR Update Repair Order started", {
jobid: JobData?.id,
bodyshopId,
rr_ro_id: JobData?.rr_ro_id
});
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
// Build Mustache variables for server/rr/xml-templates/UpdateRepairOrder.xml
const vars = mapRepairOrderUpdate({ JobData, txEnvelope, dealerConfig });
const data = await MakeRRCall({
action: RRActions.UpdateRepairOrder, // resolves SOAPAction+URL
body: { template: "UpdateRepairOrder", data: vars }, // render XML template
redisHelpers,
socket,
jobid: JobData.id
});
const response = assertRrOk(data, { apiName: "RR Update Repair Order" });
RRLogger(socket, "debug", "RR Update Repair Order success", {
jobid: JobData?.id,
rr_ro_id: JobData?.rr_ro_id
});
return response;
} catch (error) {
RRLogger(socket, "error", `RR Update Repair Order failed: ${error.message}`, {
jobid: JobData?.id,
rr_ro_id: JobData?.rr_ro_id
});
throw error;
} }
} }
module.exports = { module.exports = {
CreateRepairOrder, createRepairOrder,
UpdateRepairOrder, updateRepairOrder
getDealerConfigFromDB
}; };

View File

@@ -1,127 +1,191 @@
// node server/rr/rr-test.js #!/usr/bin/env node
/** /**
* @file rr-test.js * RR smoke test / CLI (STAR-only)
* @description Diagnostic test script for Reynolds & Reynolds (R&R) integration.
* Run with: NODE_ENV=development node server/rr/rr-test.js
*/ */
const path = require("path"); const path = require("path");
require("dotenv").config({ const fs = require("fs");
path: path.resolve(__dirname, "../../", `.env.${process.env.NODE_ENV || "development"}`) const dotenv = require("dotenv");
}); const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers");
const fs = require("fs/promises");
const mustache = require("mustache");
const { getBaseRRConfig } = require("./rr-constants"); const { getBaseRRConfig } = require("./rr-constants");
const { RRActions, MakeRRCall } = require("./rr-helpers");
const RRLogger = require("./rr-logger");
// --- Mock socket + redis helpers for standalone test // Load env file for local runs
const socket = { const defaultEnvPath = path.resolve(__dirname, "../../.env.development");
bodyshopId: process.env.TEST_BODYSHOP_ID || null, if (fs.existsSync(defaultEnvPath)) {
user: { email: "test@romeonline.io" }, const result = dotenv.config({ path: defaultEnvPath });
emit: (event, data) => console.log(`[SOCKET EVENT] ${event}`, data), if (result?.parsed) {
logger: console console.log(
}; `${defaultEnvPath}\n[dotenv@${require("dotenv/package.json").version}] injecting env (${Object.keys(result.parsed).length}) from ../../.env.development`
);
}
}
const redisHelpers = { // Parse CLI args
setSessionData: async () => {}, const argv = process.argv.slice(2);
getSessionData: async () => {}, const args = { _: [] };
setSessionTransactionData: async () => {}, for (let i = 0; i < argv.length; i++) {
getSessionTransactionData: async () => {}, const a = argv[i];
clearSessionTransactionData: async () => {}
};
(async () => { if (a.startsWith("--")) {
try { const eq = a.indexOf("=");
console.log("=== Reynolds & Reynolds Integration Test ==="); if (eq > -1) {
console.log("NODE_ENV:", process.env.NODE_ENV); const k = a.slice(2, eq);
const v = a.slice(eq + 1);
const baseCfg = getBaseRRConfig(); args[k] = v;
console.log("Base R&R Config (from env):", { } else {
baseUrl: baseCfg.baseUrl, const k = a.slice(2);
hasUser: !!baseCfg.username || !!process.env.RR_API_USER || !!process.env.RR_USERNAME, const next = argv[i + 1];
hasPass: !!baseCfg.password || !!process.env.RR_API_PASS || !!process.env.RR_PASSWORD, if (next && !next.startsWith("-")) {
timeout: baseCfg.timeout args[k] = next;
}); i++; // consume value
} else {
// ---- test variables for GetAdvisors args[k] = true; // boolean flag
const templateVars = {
DealerCode: process.env.RR_DEALER_NAME || "ROME",
DealerName: "Rome Collision Test",
SearchCriteria: {
Department: "Body Shop",
Status: "ACTIVE"
} }
}; }
} else if (a.startsWith("-") && a.length > 1) {
// simple short flag handling: -a value
const k = a.slice(1);
const next = argv[i + 1];
if (next && !next.startsWith("-")) {
args[k] = next;
i++;
} else {
args[k] = true;
}
} else {
args._.push(a);
}
}
// Dealer/Store/Branch/PPSysId can come from rr_configuration or env; for test we override: function toIntOr(defaultVal, maybe) {
const dealerConfigOverride = { const n = parseInt(maybe, 10);
// baseUrl can also be overridden here if you want return Number.isFinite(n) ? n : defaultVal;
ppsysid: process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID || "TEST-PPSYSID", }
dealer_number: process.env.RR_DEALER_NUMBER || "12345",
store_number: process.env.RR_STORE_NUMBER || "01",
branch_number: process.env.RR_BRANCH_NUMBER || "001",
// creds (optional here; MakeRRCall will fallback to env if omitted)
username: process.env.RR_API_USER || process.env.RR_USERNAME || "Rome",
password: process.env.RR_API_PASS || process.env.RR_PASSWORD || "secret"
};
// Show the first ~600 chars of the envelope we will send (by rendering the template + header) // ✅ fixed guard clause
// NOTE: This is just for printing; MakeRRCall will rebuild with proper header internally. function pickActionName(raw) {
const templatePath = path.join(__dirname, "xml-templates", "GetAdvisors.xml"); if (!raw || typeof raw !== "string") return "ping";
const tpl = await fs.readFile(templatePath, "utf8"); const x = raw.toLowerCase();
const renderedBody = mustache.render(tpl, templateVars); if (x === "combined" || x === "combinedsearch" || x === "comb") return "combined";
if (x === "advisors" || x === "advisor" || x === "getadvisors") return "advisors";
if (x === "parts" || x === "getparts" || x === "part") return "parts";
if (x === "ping") return "ping";
return x;
}
// Build a preview envelope using the same helper used by MakeRRCall function buildBodyForAction(action, args, cfg) {
const { renderXmlTemplate } = require("./rr-helpers"); switch (action) {
const headerPreview = await renderXmlTemplate("_EnvelopeHeader", { case "ping":
PPSysId: dealerConfigOverride.ppsysid, case "advisors": {
DealerNumber: dealerConfigOverride.dealer_number, const max = toIntOr(1, args.max);
StoreNumber: dealerConfigOverride.store_number, const data = {
BranchNumber: dealerConfigOverride.branch_number, DealerCode: cfg.dealerNumber,
Username: dealerConfigOverride.username, DealerNumber: cfg.dealerNumber,
Password: dealerConfigOverride.password, StoreNumber: cfg.storeNumber,
CorrelationId: "preview-correlation" BranchNumber: cfg.branchNumber,
}); SearchCriteria: {
const previewEnvelope = ` AdvisorId: args.advisorId,
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/"> FirstName: args.first || args.firstname,
<soapenv:Header> LastName: args.last || args.lastname,
${headerPreview} Department: args.department,
</soapenv:Header> Status: args.status || "ACTIVE",
<soapenv:Body> IncludeInactive: args.includeInactive ? "true" : undefined,
${renderedBody} MaxResults: max
</soapenv:Body> }
</soapenv:Envelope>`.trim(); };
return { template: "GetAdvisors", data, appArea: {} };
console.log("\n--- Rendered SOAP Envelope (first 600 chars) ---\n");
console.log(previewEnvelope.slice(0, 600));
console.log("... [truncated]\n");
// If we don't have a base URL, skip the live call
if (!baseCfg.baseUrl) {
console.warn("\n⚠ No RR baseUrl defined. Skipping live call.\n");
return;
} }
console.log(`--- Sending SOAP Request: ${RRActions.GetAdvisors.action} ---\n`); case "combined": {
const max = toIntOr(10, args.max);
const data = {
DealerNumber: cfg.dealerNumber,
StoreNumber: cfg.storeNumber,
BranchNumber: cfg.branchNumber,
Customer: {
FirstName: args.first,
LastName: args.last,
PhoneNumber: args.phone,
EmailAddress: args.email
},
Vehicle: {
VIN: args.vin,
LicensePlate: args.plate
},
MaxResults: max
};
return { template: "CombinedSearch", data, appArea: {} };
}
const responseXml = await MakeRRCall({ case "parts": {
action: "GetAdvisors", const max = toIntOr(5, args.max);
baseUrl: process.env.RR_API_BASE_URL, const data = {
body: { template: "GetAdvisors", data: templateVars }, DealerNumber: cfg.dealerNumber,
dealerConfig: dealerConfigOverride, StoreNumber: cfg.storeNumber,
redisHelpers, BranchNumber: cfg.branchNumber,
socket, SearchCriteria: {
jobid: "test-job", PartNumber: args.part,
retries: 1 Description: args.desc,
}); Make: args.make,
Model: args.model,
Year: args.year,
MaxResults: max
}
};
return { template: "GetParts", data, appArea: {} };
}
RRLogger(socket, "info", "RR test successful", { bytes: Buffer.byteLength(responseXml, "utf8") }); default:
console.log("\n✅ Test completed successfully.\n"); throw new Error(`Unsupported action: ${action}`);
} catch (error) {
console.error("\n❌ Test failed:", error.message);
console.error(error.stack);
} }
})(); }
async function main() {
const action = pickActionName(args.action || args.a || args._[0]);
const rrAction =
action === "ping"
? "GetAdvisors"
: action === "advisors"
? "GetAdvisors"
: action === "combined"
? "CombinedSearch"
: action === "parts"
? "GetParts"
: action;
const cfg = getBaseRRConfig();
const body = buildBodyForAction(action, args, cfg);
const templateName = body.template || rrAction;
try {
const xml = await renderXmlTemplate(templateName, body.data);
console.log("✅ Templates verified.");
} catch (e) {
console.error("❌ Template verification failed:", e.message);
process.exit(1);
}
if (args.dry) {
const business = await renderXmlTemplate(templateName, body.data);
const envelope = await buildStarEnvelope(business, cfg, body.appArea);
console.log("\n--- FULL SOAP ENVELOPE ---\n");
console.log(envelope);
console.log("\n(dry run) 🚫 Skipping network call.");
return;
}
try {
console.log(`\n▶ Calling Rome action: ${rrAction}`);
const xml = await MakeRRCall({ action: rrAction, body, dealerConfig: cfg });
console.log("\n✅ RR call succeeded.\n");
console.log(xml);
} catch (err) {
console.dir(err);
console.error("[RR] rr-test failed", { message: err.message, stack: err.stack });
process.exit(1);
}
}
main().catch((e) => {
process.exit(1);
});

View File

@@ -1,97 +1,123 @@
/** /**
* RR WSDL / SOAP XML Transport Layer (thin wrapper) * @file rr-wsdl.js
* ------------------------------------------------- * @description Lightweight service description + utilities for the Rome (R&R) SOAP actions.
* Delegates to rr-helpers.MakeRRCall (which handles: * - Maps actions to SOAPAction headers (from rr-constants)
* - fetching dealer config from DB via resolveRRConfig * - Maps actions to Mustache template filenames (xml-templates/*.xml)
* - rendering Mustache XML templates * - Provides verification helpers to ensure templates exist
* - building SOAP envelope + headers * - Provides normalized SOAP headers used by the transport
* - axios POST + retries
*
* Use this when you prefer the "action + variables" style and (optionally)
* want a parsed Body node back instead of raw XML.
*/ */
const { XMLParser } = require("fast-xml-parser"); const path = require("path");
const logger = require("../utils/logger"); const fs = require("fs/promises");
const { MakeRRCall, resolveRRConfig, renderXmlTemplate } = require("./rr-helpers"); const { RR_ACTIONS, RR_SOAP_HEADERS } = require("./rr-constants");
// Map friendly action names to template filenames (no envelope here; helpers add it) // ---- Action <-> Template wiring ----
const RR_ACTION_MAP = { // Keep action names consistent with rr-helpers / rr-lookup / rr-repair-orders / rr-customer
CustomerInsert: { file: "InsertCustomer.xml" }, const ACTION_TEMPLATES = Object.freeze({
CustomerUpdate: { file: "UpdateCustomer.xml" }, InsertCustomer: "InsertCustomer",
ServiceVehicleInsert: { file: "InsertServiceVehicle.xml" }, UpdateCustomer: "UpdateCustomer",
CombinedSearch: { file: "CombinedSearch.xml" }, InsertServiceVehicle: "InsertServiceVehicle",
GetParts: { file: "GetParts.xml" }, CreateRepairOrder: "CreateRepairOrder",
GetAdvisors: { file: "GetAdvisors.xml" }, UpdateRepairOrder: "UpdateRepairOrder",
CreateRepairOrder: { file: "CreateRepairOrder.xml" }, GetAdvisors: "GetAdvisors",
UpdateRepairOrder: { file: "UpdateRepairOrder.xml" } GetParts: "GetParts",
}; CombinedSearch: "CombinedSearch"
});
/** /**
* Optionally render just the body XML for a given action (no SOAP envelope). * Get the SOAPAction string for a known action.
* Mostly useful for diagnostics/tests. * Throws if action is unknown.
*/ */
async function buildRRXml(action, variables = {}) { function getSoapAction(action) {
const entry = RR_ACTION_MAP[action]; const entry = RR_ACTIONS[action];
if (!entry) throw new Error(`Unknown RR action: ${action}`); if (!entry) {
const templateName = entry.file.replace(/\.xml$/i, ""); const known = Object.keys(RR_ACTIONS).join(", ");
return renderXmlTemplate(templateName, variables); throw new Error(`Unknown RR action "${action}". Known: ${known}`);
}
return entry.soapAction;
} }
/** /**
* Send an RR SOAP request using helpers (action + variables). * Get the template filename (without extension) for a known action.
* @param {object} opts * e.g., "CreateRepairOrder" -> "CreateRepairOrder"
* @param {string} opts.action One of RR_ACTION_MAP keys (and RR_ACTIONS in rr-constants)
* @param {object} opts.variables Mustache variables for the body template
* @param {object} opts.socket Socket/req for context (bodyshopId + auth)
* @param {boolean} [opts.raw=false] If true, returns raw XML string
* @param {number} [opts.retries=1] Transient retry attempts (5xx/network)
* @returns {Promise<string|object>} Raw XML (raw=true) or parsed Body node
*/ */
async function sendRRRequest({ action, variables = {}, socket, raw = false, retries = 1 }) { function getTemplateForAction(action) {
const entry = RR_ACTION_MAP[action]; const tpl = ACTION_TEMPLATES[action];
if (!entry) throw new Error(`Unknown RR action: ${action}`); if (!tpl) {
const known = Object.keys(ACTION_TEMPLATES).join(", ");
throw new Error(`No template mapping for RR action "${action}". Known: ${known}`);
}
return tpl;
}
const templateName = entry.file.replace(/\.xml$/i, ""); /**
const dealerConfig = await resolveRRConfig(socket); * Build headers for a SOAP request, including SOAPAction.
* Consumers: rr-helpers (transport).
*/
function buildSoapHeadersForAction(action) {
return {
...RR_SOAP_HEADERS,
SOAPAction: getSoapAction(action)
};
}
// Let MakeRRCall render + envelope + post /**
const xml = await MakeRRCall({ * List all known actions with their SOAPAction + template.
* Useful for diagnostics (e.g., /rr/actions route).
*/
function listActions() {
return Object.keys(ACTION_TEMPLATES).map((action) => ({
action, action,
body: { template: templateName, data: variables }, soapAction: getSoapAction(action),
socket, template: getTemplateForAction(action)
dealerConfig, }));
retries }
});
if (raw) return xml; /**
* Verify that every required template exists in xml-templates/.
* Returns an array of issues; empty array means all good.
*/
async function verifyTemplatesExist() {
const issues = [];
const baseDir = path.join(__dirname, "xml-templates");
try { for (const [action, tpl] of Object.entries(ACTION_TEMPLATES)) {
const parser = new XMLParser({ ignoreAttributes: false }); const filePath = path.join(baseDir, `${tpl}.xml`);
const parsed = parser.parse(xml); try {
const stat = await fs.stat(filePath);
if (!stat.isFile()) {
issues.push({ action, template: tpl, error: "Not a regular file" });
}
} catch {
issues.push({ action, template: tpl, error: `Missing file: ${filePath}` });
}
}
return issues;
}
// Try several common namespace variants for Envelope/Body /**
const bodyNode = * Quick assert that throws if any template is missing.
parsed?.Envelope?.Body || * You can call this once during boot and log the result.
parsed?.["soapenv:Envelope"]?.["soapenv:Body"] || */
parsed?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] || async function assertTemplates() {
parsed?.["S:Envelope"]?.["S:Body"] || const issues = await verifyTemplatesExist();
parsed; if (issues.length) {
const msg =
return bodyNode; "RR xml-templates verification failed:\n" +
} catch (err) { issues.map((i) => ` - ${i.action} -> ${i.template}.xml :: ${i.error}`).join("\n");
logger.log("rr-wsdl-parse-error", "ERROR", "RR", null, { throw new Error(msg);
action,
message: err.message,
stack: err.stack
});
// If parsing fails, return raw so caller can inspect
return xml;
} }
} }
module.exports = { module.exports = {
sendRRRequest, // Maps / helpers
buildRRXml, ACTION_TEMPLATES,
RR_ACTION_MAP listActions,
getSoapAction,
getTemplateForAction,
buildSoapHeadersForAction,
// Verification
verifyTemplatesExist,
assertTemplates
}; };

193
server/rr/rrRoutes.js Normal file
View 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;

View File

@@ -1,73 +1,15 @@
<rr:CombinedSearchRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeCustServVehCombReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
<!-- Optional request metadata --> <!-- NOTE: ApplicationArea is injected by buildStarEnvelope(); do not include it here. -->
{{#RequestId}} <CustServVehCombReq>
<rr:RequestId>{{RequestId}}</rr:RequestId> <QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}>
{{/RequestId}} {{#Customer.PhoneNumber}}<Phone Num="{{Customer.PhoneNumber}}"/>{{/Customer.PhoneNumber}}
{{#Environment}}
<rr:Environment>{{Environment}}</rr:Environment>
{{/Environment}}
<rr:Dealer> {{#Customer.FirstName}}<FirstName>{{Customer.FirstName}}</FirstName>{{/Customer.FirstName}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> {{#Customer.LastName}}<LastName>{{Customer.LastName}}</LastName>{{/Customer.LastName}}
{{#DealerName}} {{#Customer.EmailAddress}}<EMail>{{Customer.EmailAddress}}</EMail>{{/Customer.EmailAddress}}
<rr:DealerName>{{DealerName}}</rr:DealerName>
{{/DealerName}}
{{#DealerNumber}}
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
{{/DealerNumber}}
{{#StoreNumber}}
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
{{/StoreNumber}}
{{#BranchNumber}}
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
{{/BranchNumber}}
</rr:Dealer>
<rr:SearchCriteria> {{#Vehicle.VIN}}<VIN>{{Vehicle.VIN}}</VIN>{{/Vehicle.VIN}}
{{#Customer}} {{#Vehicle.LicensePlate}}<LicensePlate>{{Vehicle.LicensePlate}}</LicensePlate>{{/Vehicle.LicensePlate}}
<rr:Customer> </QueryData>
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}} </CustServVehCombReq>
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}} </rey_RomeCustServVehCombReq>
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
</rr:Customer>
{{/Customer}}
{{#Vehicle}}
<rr:ServiceVehicle>
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
</rr:ServiceVehicle>
{{/Vehicle}}
{{#Company}}
<rr:Company>
{{#Name}}<rr:Name>{{Name}}</rr:Name>{{/Name}}
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
</rr:Company>
{{/Company}}
<!-- Search behavior flags (all optional) -->
{{#SearchMode}}<rr:SearchMode>{{SearchMode}}</rr:SearchMode>{{/SearchMode}}
{{#ExactMatch}}<rr:ExactMatch>{{ExactMatch}}</rr:ExactMatch>{{/ExactMatch}}
{{#PartialMatch}}<rr:PartialMatch>{{PartialMatch}}</rr:PartialMatch>{{/PartialMatch}}
{{#CaseInsensitive}}<rr:CaseInsensitive>{{CaseInsensitive}}</rr:CaseInsensitive>{{/CaseInsensitive}}
<!-- Result shaping (all optional) -->
{{#ReturnCustomers}}<rr:ReturnCustomers>{{ReturnCustomers}}</rr:ReturnCustomers>{{/ReturnCustomers}}
{{#ReturnVehicles}}<rr:ReturnVehicles>{{ReturnVehicles}}</rr:ReturnVehicles>{{/ReturnVehicles}}
{{#ReturnCompanies}}<rr:ReturnCompanies>{{ReturnCompanies}}</rr:ReturnCompanies>{{/ReturnCompanies}}
<!-- Paging/sorting (all optional) -->
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
</rr:SearchCriteria>
</rr:CombinedSearchRq>

View File

@@ -1,158 +1,117 @@
<rr:RepairOrderInsertRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeCreateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
<!-- Optional request metadata --> <BSMRepairOrderReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <RepairOrder>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} <RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>
{{#DmsRepairOrderId}}<DmsRepairOrderId>{{DmsRepairOrderId}}</DmsRepairOrderId>{{/DmsRepairOrderId}}
<rr:Dealer> {{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> {{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} {{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} {{#Department}}<Department>{{Department}}</Department>{{/Department}}
</rr:Dealer> {{#ProfitCenter}}<ProfitCenter>{{ProfitCenter}}</ProfitCenter>{{/ProfitCenter}}
{{#ROType}}<ROType>{{ROType}}</ROType>{{/ROType}}
<rr:RepairOrder> {{#Status}}<Status>{{Status}}</Status>{{/Status}}
<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber> {{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
{{#DmsRepairOrderId}}<rr:DmsRepairOrderId>{{DmsRepairOrderId}}</rr:DmsRepairOrderId>{{/DmsRepairOrderId}} {{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
<Customer>
<!-- Core dates --> {{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}} {{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}} {{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}} {{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
{{#Address}}
<!-- People & routing --> <Address>
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}} {{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}} {{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}} {{#City}}<City>{{City}}</City>{{/City}}
{{#ProfitCenter}}<rr:ProfitCenter>{{ProfitCenter}}</rr:ProfitCenter>{{/ProfitCenter}} {{#State}}<State>{{State}}</State>{{/State}}
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
<!-- Type & status --> {{#Country}}<Country>{{Country}}</Country>{{/Country}}
{{#ROType}}<rr:ROType>{{ROType}}</rr:ROType>{{/ROType}} </Address>
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}} {{/Address}}
{{#IsBodyShop}}<rr:IsBodyShop>{{IsBodyShop}}</rr:IsBodyShop>{{/IsBodyShop}} </Customer>
{{#DRPFlag}}<rr:DRPFlag>{{DRPFlag}}</rr:DRPFlag>{{/DRPFlag}} <ServiceVehicle>
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
<!-- Customer --> {{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
<rr:Customer> {{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
<rr:CustomerId>{{CustomerId}}</rr:CustomerId> {{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}} {{#Make}}<Make>{{Make}}</Make>{{/Make}}
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}} {{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}} {{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
<!-- Optional address if you have it --> </ServiceVehicle>
{{#Address}} {{#JobLines}}
<rr:Address> <JobLine>
{{#Line1}}<rr:Line1>{{Line1}}</rr:Line1>{{/Line1}} <Sequence>{{Sequence}}</Sequence>
{{#Line2}}<rr:Line2>{{Line2}}</rr:Line2>{{/Line2}} {{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
{{#City}}<rr:City>{{City}}</rr:City>{{/City}} {{#LineType}}<LineType>
{{#State}}<rr:State>{{State}}</rr:State>{{/State}} {{LineType}}</LineType>{{/LineType}
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}} {{#Category}}<Category>
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}} {{Category}}</Category>{{/Category}}
</rr:Address> {{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
{{/Address}} {{#Description}}<Description>{{Description}}</Description>{{/Description}}
</rr:Customer> {{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
<!-- Vehicle --> {{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
<rr:Vehicle> {{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}} {{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}} {{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}} {{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}} {{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}} {{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}} {{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}} {{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}} {{#Taxes}}
</rr:Vehicle> <Taxes>
{{#Items}}
<!-- Job lines --> <Tax>
{{#JobLines}} <Code>{{Code}}</Code>
<rr:JobLine> <Amount>{{Amount}}</Amount>
<rr:Sequence>{{Sequence}}</rr:Sequence> {{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
{{#ParentSequence}}<rr:ParentSequence>{{ParentSequence}}</rr:ParentSequence>{{/ParentSequence}} </Tax>
{{/Items}}
{{#LineType}}<rr:LineType> </Taxes>
{{LineType}}</rr:LineType>{{/LineType}} <!-- LABOR | PART | MISC | FEE | DISCOUNT --> {{/Taxes}}
{{#Category}}<rr:Category> </JobLine>
{{Category}}</rr:Category>{{/Category}} <!-- e.g., BODY, PAINT, GLASS --> {{/JobLines}}
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}} {{#Totals}}
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}} <Totals>
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
<!-- Labor fields --> {{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}} {{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}} {{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
{{#DiscountTotal}}<DiscountTotal>{{DiscountTotal}}</DiscountTotal>{{/DiscountTotal}}
<!-- Part fields --> {{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}} <GrandTotal>{{GrandTotal}}</GrandTotal>
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}} </Totals>
{{/Totals}}
<!-- Amounts --> {{#Payments}}
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}} <Payments>
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
{{#DiscountAmount}}<rr:DiscountAmount>{{DiscountAmount}}</rr:DiscountAmount>{{/DiscountAmount}}
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
{{#GLAccount}}<rr:GLAccount>{{GLAccount}}</rr:GLAccount>{{/GLAccount}}
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
<!-- Tax details (optional) -->
{{#Taxes}}
<rr:Taxes>
{{#Items}} {{#Items}}
<rr:Tax> <Payment>
<rr:Code>{{Code}}</rr:Code> <PayerType>{{PayerType}}</PayerType>
<rr:Amount>{{Amount}}</rr:Amount> {{#PayerName}}<PayerName>{{PayerName}}</PayerName>{{/PayerName}}
{{#Rate}}<rr:Rate>{{Rate}}</rr:Rate>{{/Rate}} <Amount>{{Amount}}</Amount>
</rr:Tax> {{#Method}}<Method>{{Method}}</Method>{{/Method}}
{{#Reference}}<Reference>{{Reference}}</Reference>{{/Reference}}
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
</Payment>
{{/Items}} {{/Items}}
</rr:Taxes> </Payments>
{{/Taxes}} {{/Payments}}
</rr:JobLine> {{#Insurance}}
{{/JobLines}} <Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
<!-- Totals --> {{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
{{#Totals}} {{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
<rr:Totals> {{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}} </Insurance>
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}} {{/Insurance}}
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}} {{#Notes}}
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}} <Notes>
{{#DiscountTotal}}<rr:DiscountTotal>{{DiscountTotal}}</rr:DiscountTotal>{{/DiscountTotal}} {{#Items}}<Note>{{.}}</Note>{{/Items}}
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}} </Notes>
<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal> {{/Notes}}
</rr:Totals> </RepairOrder>
{{/Totals}} </BSMRepairOrderReq>
</rey_RomeCreateBSMRepairOrderReq>
<!-- Payers/Payments (optional) -->
{{#Payments}}
<rr:Payments>
{{#Items}}
<rr:Payment>
<rr:PayerType>{{PayerType}}</rr:PayerType> <!-- CUSTOMER | INSURANCE | WARRANTY | FLEET -->
{{#PayerName}}<rr:PayerName>{{PayerName}}</rr:PayerName>{{/PayerName}}
<rr:Amount>{{Amount}}</rr:Amount>
{{#Method}}<rr:Method>{{Method}}</rr:Method>{{/Method}}
{{#Reference}}<rr:Reference>{{Reference}}</rr:Reference>{{/Reference}}
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
</rr:Payment>
{{/Items}}
</rr:Payments>
{{/Payments}}
<!-- Insurance block (optional) -->
{{#Insurance}}
<rr:Insurance>
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
</rr:Insurance>
{{/Insurance}}
<!-- Notes -->
{{#Notes}}
<rr:Notes>
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
</rr:Notes>
{{/Notes}}
</rr:RepairOrder>
</rr:RepairOrderInsertRq>

View File

@@ -1,34 +1,15 @@
<rr:GetAdvisorsRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeGetAdvisorsReq xmlns="http://www.starstandards.org/STAR" revision="1.0">
<!-- Optional request metadata --> <GetAdvisorsReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <QueryData{{#SearchCriteria.MaxResults}} MaxRecs="{{SearchCriteria.MaxResults}}"{{/SearchCriteria.MaxResults}}>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} {{#SearchCriteria.AdvisorId}}<AdvisorID>{{SearchCriteria.AdvisorId}}</AdvisorID>{{/SearchCriteria.AdvisorId}}
{{#SearchCriteria.FirstName}}<FirstName>{{SearchCriteria.FirstName}}</FirstName>{{/SearchCriteria.FirstName}}
<rr:Dealer> {{#SearchCriteria.LastName}}<LastName>{{SearchCriteria.LastName}}</LastName>{{/SearchCriteria.LastName}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> {{#SearchCriteria.Department}}<Department>{{SearchCriteria.Department}}</Department>{{/SearchCriteria.Department}}
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#SearchCriteria.Status}}<Status>{{SearchCriteria.Status}}</Status>{{/SearchCriteria.Status}}
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} {{#SearchCriteria.IncludeInactive}}<IncludeInactive>{{SearchCriteria.IncludeInactive}}</IncludeInactive>{{/SearchCriteria.IncludeInactive}}
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{#SearchCriteria.PageNumber}}<PageNumber>{{SearchCriteria.PageNumber}}</PageNumber>{{/SearchCriteria.PageNumber}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} {{#SearchCriteria.SortBy}}<SortBy>{{SearchCriteria.SortBy}}</SortBy>{{/SearchCriteria.SortBy}}
</rr:Dealer> {{#SearchCriteria.SortDirection}}<SortDirection>{{SearchCriteria.SortDirection}}</SortDirection>{{/SearchCriteria.SortDirection}}
</QueryData>
{{#SearchCriteria}} </GetAdvisorsReq>
<rr:SearchCriteria> </rey_RomeGetAdvisorsReq>
{{#AdvisorId}}<rr:AdvisorId>{{AdvisorId}}</rr:AdvisorId>{{/AdvisorId}}
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}} <!-- ACTIVE | INACTIVE -->
{{#SearchMode}}<rr:SearchMode>
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
{{#Email}}<rr:Email>{{Email}}</rr:Email>{{/Email}}
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
{{#IncludeInactive}}<rr:IncludeInactive>{{IncludeInactive}}</rr:IncludeInactive>{{/IncludeInactive}}
<!-- Optional paging/sorting -->
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
</rr:SearchCriteria>
{{/SearchCriteria}}
</rr:GetAdvisorsRq>

View File

@@ -1,50 +1,25 @@
<rr:GetPartRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeGetPartsReq xmlns="{{STAR_NS}}" revision="1.0">
<!-- Optional request metadata --> <GetPartReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <QueryData{{#MaxResults}} MaxRecs="{{MaxResults}}"{{/MaxResults}}{{#PageNumber}} Page="{{PageNumber}}"{{/PageNumber}}>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} {{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
<rr:Dealer> {{#Make}}<Make>{{Make}}</Make>{{/Make}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> {{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} {{#Vendor}}<Vendor>{{Vendor}}</Vendor>{{/Vendor}}
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{#Category}}<Category>{{Category}}</Category>{{/Category}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} {{#Brand}}<Brand>{{Brand}}</Brand>{{/Brand}}
</rr:Dealer> {{#IsOEM}}<IsOEM>{{IsOEM}}</IsOEM>{{/IsOEM}}
{{#IsAftermarket}}<IsAftermarket>{{IsAftermarket}}</IsAftermarket>{{/IsAftermarket}}
<rr:SearchCriteria> {{#InStock}}<InStock>{{InStock}}</InStock>{{/InStock}}
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}} {{#Warehouse}}<Warehouse>{{Warehouse}}</Warehouse>{{/Warehouse}}
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}} {{#Location}}<Location>{{Location}}</Location>{{/Location}}
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}} {{#MinPrice}}<MinPrice>{{MinPrice}}</MinPrice>{{/MinPrice}}
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}} {{#MaxPrice}}<MaxPrice>{{MaxPrice}}</MaxPrice>{{/MaxPrice}}
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}} {{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
{{#Vendor}}<rr:Vendor>{{Vendor}}</rr:Vendor>{{/Vendor}} {{#SearchMode}}<SearchMode>{{SearchMode}}</SearchMode>{{/SearchMode}}
{{#Category}}<rr:Category>{{Category}}</rr:Category>{{/Category}} {{#SortBy}}<SortBy>{{SortBy}}</SortBy>{{/SortBy}}
{{#SortDirection}}<SortDirection>{{SortDirection}}</SortDirection>{{/SortDirection}}
<!-- Optional classification flags --> </QueryData>
{{#Brand}}<rr:Brand>{{Brand}}</rr:Brand>{{/Brand}} </GetPartReq>
{{#IsOEM}}<rr:IsOEM>{{IsOEM}}</rr:IsOEM>{{/IsOEM}} <!-- true | false --> </rey_RomeGetPartsReq>
{{#IsAftermarket}}<rr:IsAftermarket>{{IsAftermarket}}</rr:IsAftermarket>{{/IsAftermarket}}
<!-- Availability / inventory -->
{{#InStock}}<rr:InStock>{{InStock}}</rr:InStock>{{/InStock}} <!-- true | false -->
{{#Warehouse}}<rr:Warehouse>{{Warehouse}}</rr:Warehouse>{{/Warehouse}}
{{#Location}}<rr:Location>{{Location}}</rr:Location>{{/Location}}
<!-- Pricing filters -->
{{#MinPrice}}<rr:MinPrice>{{MinPrice}}</rr:MinPrice>{{/MinPrice}}
{{#MaxPrice}}<rr:MaxPrice>{{MaxPrice}}</rr:MaxPrice>{{/MaxPrice}}
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
<!-- Search behavior -->
{{#SearchMode}}<rr:SearchMode>
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
<!-- Paging / sorting -->
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
{{#SortBy}}<rr:SortBy>
{{SortBy}}</rr:SortBy>{{/SortBy}} <!-- e.g., PARTNUMBER, DESCRIPTION, PRICE -->
{{#SortDirection}}<rr:SortDirection>
{{SortDirection}}</rr:SortDirection>{{/SortDirection}} <!-- ASC | DESC -->
</rr:SearchCriteria>
</rr:GetPartRq>

View File

@@ -1,102 +1,63 @@
<rr:CustomerInsertRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeCustomerInsertReq xmlns="{{STAR_NS}}" revision="1.0">
<!-- Optional request metadata --> <CustomerInsertReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <Customer>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} {{#CustomerNumber}}<CustomerNumber>{{CustomerNumber}}</CustomerNumber>{{/CustomerNumber}}
{{#CustomerType}}<CustomerType>
<rr:Dealer> {{CustomerType}}</CustomerType>{{/CustomerType}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> <CustomerName>{{CustomerName}}</CustomerName>
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} {{#Language}}<Language>{{Language}}</Language>{{/Language}}
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{#GroupName}}<GroupName>{{GroupName}}</GroupName>{{/GroupName}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} {{#TaxExempt}}<TaxExempt>{{TaxExempt}}</TaxExempt>{{/TaxExempt}}
</rr:Dealer> {{#DiscountLevel}}<DiscountLevel>{{DiscountLevel}}</DiscountLevel>{{/DiscountLevel}}
{{#Active}}<Active>{{Active}}</Active>{{/Active}}
<rr:Customer> {{#Addresses}}
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}} <Address>
{{#CustomerType}}<rr:CustomerType> {{#Type}}<Type>{{Type}}</Type>{{/Type}}
{{CustomerType}}</rr:CustomerType>{{/CustomerType}} <!-- RETAIL | FLEET | INTERNAL --> {{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}} {{#City}}<City>{{City}}</City>{{/City}}
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}} {{#State}}<State>{{State}}</State>{{/State}}
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}} {{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}} {{#Country}}<Country>{{Country}}</Country>{{/Country}}
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}} </Address>
{{/Addresses}}
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}} {{#Phones}}
<Phone>
<!-- Optional customer classification --> <Type>{{Type}}</Type>
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}} <Number>{{Number}}</Number>
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}} {{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}} {{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}} </Phone>
{{/Phones}}
<!-- Addresses --> {{#Emails}}
{{#Addresses}} <Email>
<rr:Address> <Type>{{Type}}</Type>
{{#AddressType}}<rr:AddressType> <Address>{{Address}}</Address>
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING --> {{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}} </Email>
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}} {{/Emails}}
{{#City}}<rr:City>{{City}}</rr:City>{{/City}} {{#Insurance}}
{{#State}}<rr:State>{{State}}</rr:State>{{/State}} <Insurance>
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}} {{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}} {{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
</rr:Address> {{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{/Addresses}} {{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
<!-- Phones --> </Insurance>
{{#Phones}} {{/Insurance}}
<rr:Phone> {{#LinkedAccounts}}
<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber> <LinkedAccount>
{{#PhoneType}}<rr:PhoneType> <Type>{{Type}}</Type>
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK --> <AccountNumber>{{AccountNumber}}</AccountNumber>
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}} {{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
</rr:Phone> </LinkedAccount>
{{/Phones}} {{/LinkedAccounts}}
{{#Notes}}
<!-- Emails --> <Notes>
{{#Emails}} {{#Items}}<Note>{{.}}</Note>{{/Items}}
<rr:Email> </Notes>
<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress> {{/Notes}}
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}} </Customer>
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}} </CustomerInsertReq>
</rr:Email> </rey_RomeCustomerInsertReq>
{{/Emails}}
<!-- Driver's License -->
{{#DriverLicense}}
<rr:DriverLicense>
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
</rr:DriverLicense>
{{/DriverLicense}}
<!-- Insurance -->
{{#Insurance}}
<rr:Insurance>
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
</rr:Insurance>
{{/Insurance}}
<!-- Optional linked accounts -->
{{#LinkedAccounts}}
<rr:LinkedAccount>
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
</rr:LinkedAccount>
{{/LinkedAccounts}}
<!-- Notes -->
{{#Notes}}
<rr:Notes>
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
</rr:Notes>
{{/Notes}}
</rr:Customer>
</rr:CustomerInsertRq>

View File

@@ -1,83 +1,57 @@
<rr:ServiceVehicleAddRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeServVehicleInsertReq xmlns="{{STAR_NS}}" revision="1.0">
<!-- Optional request metadata --> <ServVehicleInsertReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <ServiceVehicle>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} {{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
<rr:Dealer> {{#UnitNumber}}<UnitNumber>{{UnitNumber}}</UnitNumber>{{/UnitNumber}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> {{#StockNumber}}<StockNumber>{{StockNumber}}</StockNumber>{{/StockNumber}}
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} {{#Make}}<Make>{{Make}}</Make>{{/Make}}
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} {{#Trim}}<Trim>{{Trim}}</Trim>{{/Trim}}
</rr:Dealer> {{#BodyStyle}}<BodyStyle>{{BodyStyle}}</BodyStyle>{{/BodyStyle}}
{{#Transmission}}<Transmission>{{Transmission}}</Transmission>{{/Transmission}}
<rr:ServiceVehicle> {{#Engine}}<Engine>{{Engine}}</Engine>{{/Engine}}
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}} {{#FuelType}}<FuelType>{{FuelType}}</FuelType>{{/FuelType}}
{{#DriveType}}<DriveType>{{DriveType}}</DriveType>{{/DriveType}}
<!-- Identity --> {{#Color}}<Color>{{Color}}</Color>{{/Color}}
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}} {{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
{{#UnitNumber}}<rr:UnitNumber>{{UnitNumber}}</rr:UnitNumber>{{/UnitNumber}} {{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
{{#StockNumber}}<rr:StockNumber>{{StockNumber}}</rr:StockNumber>{{/StockNumber}} {{#RegistrationExpiry}}<RegistrationExpiry>{{RegistrationExpiry}}</RegistrationExpiry>{{/RegistrationExpiry}}
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
<!-- Descriptive --> {{#OdometerUnits}}<OdometerUnits>
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}} {{OdometerUnits}}</OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}} {{#InServiceDate}}<InServiceDate>{{InServiceDate}}</InServiceDate>{{/InServiceDate}}
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}} {{#Ownership}}
{{#Trim}}<rr:Trim>{{Trim}}</rr:Trim>{{/Trim}} <Ownership>
{{#BodyStyle}}<rr:BodyStyle>{{BodyStyle}}</rr:BodyStyle>{{/BodyStyle}} {{#OwnerId}}<OwnerId>{{OwnerId}}</OwnerId>{{/OwnerId}}
{{#Transmission}}<rr:Transmission>{{Transmission}}</rr:Transmission>{{/Transmission}} {{#OwnerName}}<OwnerName>{{OwnerName}}</OwnerName>{{/OwnerName}}
{{#Engine}}<rr:Engine>{{Engine}}</rr:Engine>{{/Engine}} {{#OwnershipType}}<OwnershipType>
{{#FuelType}}<rr:FuelType>{{FuelType}}</rr:FuelType>{{/FuelType}} {{OwnershipType}}</OwnershipType>{{/OwnershipType}}
{{#DriveType}}<rr:DriveType>{{DriveType}}</rr:DriveType>{{/DriveType}} </Ownership>
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}} {{/Ownership}}
{{#Insurance}}
<!-- Registration --> <Insurance>
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}} {{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}} {{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
{{#RegistrationExpiry}}<rr:RegistrationExpiry>{{RegistrationExpiry}}</rr:RegistrationExpiry>{{/RegistrationExpiry}} {{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
<!-- Odometer --> {{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}} </Insurance>
{{#OdometerUnits}}<rr:OdometerUnits> {{/Insurance}}
{{OdometerUnits}}</rr:OdometerUnits>{{/OdometerUnits}} <!-- MI | KM --> {{#Warranty}}
{{#InServiceDate}}<rr:InServiceDate>{{InServiceDate}}</rr:InServiceDate>{{/InServiceDate}} <Warranty>
{{#WarrantyCompany}}<WarrantyCompany>{{WarrantyCompany}}</WarrantyCompany>{{/WarrantyCompany}}
<!-- Ownership --> {{#WarrantyNumber}}<WarrantyNumber>{{WarrantyNumber}}</WarrantyNumber>{{/WarrantyNumber}}
{{#Ownership}} {{#WarrantyType}}<WarrantyType>{{WarrantyType}}</WarrantyType>{{/WarrantyType}}
<rr:Ownership> {{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{#OwnerId}}<rr:OwnerId>{{OwnerId}}</rr:OwnerId>{{/OwnerId}} </Warranty>
{{#OwnerName}}<rr:OwnerName>{{OwnerName}}</rr:OwnerName>{{/OwnerName}} {{/Warranty}}
{{#OwnershipType}}<rr:OwnershipType> {{#VehicleNotes}}
{{OwnershipType}}</rr:OwnershipType>{{/OwnershipType}} <!-- OWNER | LEASED | FLEET --> <Notes>
</rr:Ownership> {{#Items}}<Note>{{.}}</Note>{{/Items}}
{{/Ownership}} </Notes>
{{/VehicleNotes}}
<!-- Insurance --> </ServiceVehicle>
{{#Insurance}} </ServVehicleInsertReq>
<rr:Insurance> </rey_RomeServVehicleInsertReq>
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
</rr:Insurance>
{{/Insurance}}
<!-- Warranty -->
{{#Warranty}}
<rr:Warranty>
{{#WarrantyCompany}}<rr:WarrantyCompany>{{WarrantyCompany}}</rr:WarrantyCompany>{{/WarrantyCompany}}
{{#WarrantyNumber}}<rr:WarrantyNumber>{{WarrantyNumber}}</rr:WarrantyNumber>{{/WarrantyNumber}}
{{#WarrantyType}}<rr:WarrantyType>{{WarrantyType}}</rr:WarrantyType>{{/WarrantyType}}
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
</rr:Warranty>
{{/Warranty}}
<!-- Notes -->
{{#VehicleNotes}}
<rr:Notes>
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
</rr:Notes>
{{/VehicleNotes}}
</rr:ServiceVehicle>
</rr:ServiceVehicleAddRq>

View File

@@ -1,107 +1,83 @@
<rr:CustomerUpdateRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeCustomerUpdateReq xmlns="{{STAR_NS}}" revision="1.0">
<!-- Optional request metadata --> <CustomerUpdateReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <Customer>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} <CustomerId>{{CustomerId}}</CustomerId>
{{#CustomerType}}<CustomerType>
{{CustomerType}}</CustomerType>{{/CustomerType}}
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
{{#DisplayName}}<DisplayName>{{DisplayName}}</DisplayName>{{/DisplayName}}
{{#PreferredName}}<PreferredName>{{PreferredName}}</PreferredName>{{/PreferredName}}
{{#Language}}<Language>{{Language}}</Language>{{/Language}}
{{#GroupName}}<GroupName>{{GroupName}}</GroupName>{{/GroupName}}
{{#TaxExempt}}<TaxExempt>{{TaxExempt}}</TaxExempt>{{/TaxExempt}}
{{#DiscountLevel}}<DiscountLevel>{{DiscountLevel}}</DiscountLevel>{{/DiscountLevel}}
{{#Active}}<Active>{{Active}}</Active>{{/Active}}
{{#Addresses}}
<Address>
{{#AddressId}}<AddressId>{{AddressId}}</AddressId>{{/AddressId}}
{{#Type}}<Type>
{{Type}}</Type>{{/Type}}
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
{{#City}}<City>{{City}}</City>{{/City}}
{{#State}}<State>{{State}}</State>{{/State}}
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
{{#IsPrimary}}<IsPrimary>{{IsPrimary}}</IsPrimary>{{/IsPrimary}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</Address>
{{/Addresses}}
{{#Phones}}
<Phone>
{{#PhoneId}}<PhoneId>{{PhoneId}}</PhoneId>{{/PhoneId}}
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
{{#Number}}<Number>{{Number}}</Number>{{/Number}}
{{#Extension}}<Extension>{{Extension}}</Extension>{{/Extension}}
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</Phone>
{{/Phones}}
{{#Emails}}
<Email>
{{#EmailId}}<EmailId>{{EmailId}}</EmailId>{{/EmailId}}
{{#Type}}<Type>{{Type}}</Type>{{/Type}}
{{#Address}}<Address>{{Address}}</Address>{{/Address}}
{{#Preferred}}<Preferred>{{Preferred}}</Preferred>{{/Preferred}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</Email>
{{/Emails}}
{{#DriverLicense}}
<DriverLicense>
{{#LicenseNumber}}<LicenseNumber>{{LicenseNumber}}</LicenseNumber>{{/LicenseNumber}}
{{#LicenseState}}<LicenseState>{{LicenseState}}</LicenseState>{{/LicenseState}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
</DriverLicense>
{{/DriverLicense}}
{{#Insurance}}
<Insurance>
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<PolicyNumber>{{PolicyNumber}}</PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<ExpirationDate>{{ExpirationDate}}</ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<ContactName>{{ContactName}}</ContactName>{{/ContactName}}
{{#ContactPhone}}<ContactPhone>{{ContactPhone}}</ContactPhone>{{/ContactPhone}}
</Insurance>
{{/Insurance}}
{{#LinkedAccounts}}
<LinkedAccount>
{{#AccountId}}<AccountId>{{AccountId}}</AccountId>{{/AccountId}}
<Type>{{Type}}
</Type>
{{#AccountNumber}}<AccountNumber>{{AccountNumber}}</AccountNumber>{{/AccountNumber}}
{{#CreditLimit}}<CreditLimit>{{CreditLimit}}</CreditLimit>{{/CreditLimit}}
{{#IsDeleted}}<IsDeleted>{{IsDeleted}}</IsDeleted>{{/IsDeleted}}
</LinkedAccount>
{{/LinkedAccounts}}
<rr:Dealer> {{#Notes}}
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> <Notes>
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#Items}}<Note>{{.}}</Note>{{/Items}}
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} </Notes>
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{/Notes}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} </Customer>
</rr:Dealer> </CustomerUpdateReq>
</rey_RomeCustomerUpdateReq>
<rr:Customer>
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
{{#CustomerType}}<rr:CustomerType>{{CustomerType}}</rr:CustomerType>{{/CustomerType}}
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
<!-- Addresses -->
{{#Addresses}}
<rr:Address>
{{#AddressId}}<rr:AddressId>{{AddressId}}</rr:AddressId>{{/AddressId}}
{{#AddressType}}<rr:AddressType>
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
{{#IsPrimary}}<rr:IsPrimary>{{IsPrimary}}</rr:IsPrimary>{{/IsPrimary}}
</rr:Address>
{{/Addresses}}
<!-- Phones -->
{{#Phones}}
<rr:Phone>
{{#PhoneId}}<rr:PhoneId>{{PhoneId}}</rr:PhoneId>{{/PhoneId}}
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
{{#PhoneType}}<rr:PhoneType>
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
{{#IsDeleted}}<rr:IsDeleted>
{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}} <!-- Mark for deletion -->
</rr:Phone>
{{/Phones}}
<!-- Emails -->
{{#Emails}}
<rr:Email>
{{#EmailId}}<rr:EmailId>{{EmailId}}</rr:EmailId>{{/EmailId}}
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
</rr:Email>
{{/Emails}}
<!-- Driver's License -->
{{#DriverLicense}}
<rr:DriverLicense>
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
</rr:DriverLicense>
{{/DriverLicense}}
<!-- Insurance -->
{{#Insurance}}
<rr:Insurance>
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
</rr:Insurance>
{{/Insurance}}
<!-- Linked Accounts -->
{{#LinkedAccounts}}
<rr:LinkedAccount>
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
</rr:LinkedAccount>
{{/LinkedAccounts}}
<!-- Notes -->
{{#Notes}}
<rr:Notes>
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
</rr:Notes>
{{/Notes}}
</rr:Customer>
</rr:CustomerUpdateRq>

View File

@@ -1,135 +1,139 @@
<rr:RepairOrderChgRq xmlns:rr="http://reynoldsandrey.com/"> <rey_RomeUpdateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
<!-- Optional request metadata --> <BSMRepairOrderChgReq>
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}} <RepairOrder>
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}} {{#RepairOrderId}}<RepairOrderId>{{RepairOrderId}}</RepairOrderId>{{/RepairOrderId}}
{{#RepairOrderNumber}}<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>{{/RepairOrderNumber}}
<rr:Dealer> {{#Status}}<Status>
<rr:DealerCode>{{DealerCode}}</rr:DealerCode> {{Status}}</Status>{{/Status}}
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}} {{#ROType}}<ROType>
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}} {{ROType}}</ROType>{{/ROType}}
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}} {{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}} {{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
</rr:Dealer> {{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
<rr:RepairOrder> {{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
<!-- Identity --> {{#LocationCode}}<LocationCode>{{LocationCode}}</LocationCode>{{/LocationCode}}
{{#RepairOrderId}}<rr:RepairOrderId>{{RepairOrderId}}</rr:RepairOrderId>{{/RepairOrderId}} {{#Department}}<Department>{{Department}}</Department>{{/Department}}
{{#RepairOrderNumber}}<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>{{/RepairOrderNumber}} {{#PurchaseOrder}}<PurchaseOrder>{{PurchaseOrder}}</PurchaseOrder>{{/PurchaseOrder}}
{{#Customer}}
<!-- Header fields that may be patched --> <Customer>
{{#Status}}<rr:Status> {{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
{{Status}}</rr:Status>{{/Status}} <!-- e.g., OPEN|IN_PROGRESS|CLOSED --> {{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
{{#ROType}}<rr:ROType> {{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
{{ROType}}</rr:ROType>{{/ROType}} <!-- e.g., INSURANCE|CUSTOMER_PAY --> {{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}} </Customer>
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}} {{/Customer}}
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}} {{#Vehicle}}
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}} <ServiceVehicle>
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}} {{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
{{#LocationCode}}<rr:LocationCode>{{LocationCode}}</rr:LocationCode>{{/LocationCode}} {{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}} {{#LicensePlate}}<LicensePlate>{{LicensePlate}}</LicensePlate>{{/LicensePlate}}
{{#PurchaseOrder}}<rr:PurchaseOrder>{{PurchaseOrder}}</rr:PurchaseOrder>{{/PurchaseOrder}} {{#Year}}<Year>{{Year}}</Year>{{/Year}}
{{#Make}}<Make>{{Make}}</Make>{{/Make}}
<!-- Optional customer patch --> {{#Model}}<Model>{{Model}}</Model>{{/Model}}
{{#Customer}} {{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
<rr:Customer> {{#Color}}<Color>{{Color}}</Color>{{/Color}}
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}} </ServiceVehicle>
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}} {{/Vehicle}}
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}} {{#AddedJobLines}}
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}} <AddedJobLines>
</rr:Customer> {{#Items}}
{{/Customer}} <JobLine>
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
<!-- Optional vehicle patch --> {{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
{{#Vehicle}} {{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
<rr:Vehicle> {{#Description}}<Description>{{Description}}</Description>{{/Description}}
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}} {{#LineType}}<LineType>
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}} {{LineType}}</LineType>{{/LineType}}
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}} {{#Category}}<Category>
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}} {{Category}}</Category>{{/Category}}
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}} {{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}} {{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}} {{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
</rr:Vehicle> {{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
{{/Vehicle}} {{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
<!-- Line changes: use one of AddedJobLines / UpdatedJobLines / RemovedJobLines --> {{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
{{#AddedJobLines}} {{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
<rr:AddedJobLine> {{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}} {{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}} {{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}} {{#Taxes}}
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}} <Taxes>
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}} {{#Items}}
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}} <Tax>
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}} <Code>{{Code}}</Code>
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}} <Amount>{{Amount}}</Amount>
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}} {{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}} </Tax>
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}} {{/Items}}
{{#PayType}}<rr:PayType> </Taxes>
{{PayType}}</rr:PayType>{{/PayType}} <!-- CUST|INS|WARR|INT --> {{/Taxes}}
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}} {{#PayType}}<PayType>
</rr:AddedJobLine> {{PayType}}</PayType>{{/PayType}}
{{/AddedJobLines}} {{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
</JobLine>
{{#UpdatedJobLines}} {{/Items}}
<rr:UpdatedJobLine> </AddedJobLines>
<!-- Identify the existing line either by Sequence or LineId --> {{/AddedJobLines}}
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}} {{#UpdatedJobLines}}
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}} <UpdatedJobLines>
{{#ChangeType}}<rr:ChangeType> {{#Items}}
{{ChangeType}}</rr:ChangeType>{{/ChangeType}} <!-- PRICE|QTY|DESC|OPCODE|PAYTYPE --> <JobLine>
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}} {{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}} {{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}} {{#ChangeType}}<ChangeType>
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}} {{ChangeType}}</ChangeType>{{/ChangeType}}
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}} {{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}} {{#Description}}<Description>{{Description}}</Description>{{/Description}}
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}} {{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}} {{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}} {{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}} {{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
{{#PayType}}<rr:PayType>{{PayType}}</rr:PayType>{{/PayType}} {{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}} {{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
</rr:UpdatedJobLine> {{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
{{/UpdatedJobLines}} {{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
{{#PayType}}<PayType>{{PayType}}</PayType>{{/PayType}}
{{#RemovedJobLines}} {{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
<rr:RemovedJobLine> </JobLine>
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}} {{/Items}}
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}} </UpdatedJobLines>
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}} {{/UpdatedJobLines}}
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}} {{#RemovedJobLines}}
</rr:RemovedJobLine> <RemovedJobLines>
{{/RemovedJobLines}} {{#Items}}
<JobLine>
<!-- Totals (optional patch if RR expects header totals on change) --> {{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
{{#Totals}} {{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
<rr:Totals> {{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}} {{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}} </JobLine>
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}} {{/Items}}
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}} </RemovedJobLines>
{{#GrandTotal}}<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>{{/GrandTotal}} {{/RemovedJobLines}}
</rr:Totals> {{#Totals}}
{{/Totals}} <Totals>
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
<!-- Insurance (optional update) --> {{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
{{#Insurance}} {{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
<rr:Insurance> {{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}} {{#GrandTotal}}<GrandTotal>{{GrandTotal}}</GrandTotal>{{/GrandTotal}}
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}} </Totals>
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}} {{/Totals}}
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}} {{#Insurance}}
</rr:Insurance> <Insurance>
{{/Insurance}} {{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
{{#ClaimNumber}}<ClaimNumber>{{ClaimNumber}}</ClaimNumber>{{/ClaimNumber}}
<!-- Notes (append or replace depending on RR semantics) --> {{#AdjusterName}}<AdjusterName>{{AdjusterName}}</AdjusterName>{{/AdjusterName}}
{{#Notes}} {{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
<rr:Notes> </Insurance>
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}} {{/Insurance}}
</rr:Notes> {{#Notes}}
{{/Notes}} <Notes>
</rr:RepairOrder> {{#Items}}<Note>{{.}}</Note>{{/Items}}
</rr:RepairOrderChgRq> </Notes>
{{/Notes}}
</RepairOrder>
</BSMRepairOrderChgReq>
</rey_RomeUpdateBSMRepairOrderReq>

View File

@@ -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>

View File

@@ -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>

View File

@@ -3,7 +3,24 @@ const FortellisLogger = require("../fortellis/fortellis-logger");
const RRLogger = require("../rr/rr-logger"); const RRLogger = require("../rr/rr-logger");
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis"); const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const { RRJobExport, RRSelectedCustomer } = require("../rr/rr-job-export"); const { exportJobToRome } = require("../rr/rr-job-export");
const lookupApi = require("../rr/rr-lookup");
const RRCalculateAllocations = require("../rr/rr-calculate-allocations");
function resolveRRConfigFrom(payload = {}) {
// Back-compat: allow txEnvelope.config from old callers
const cfg = payload.config || payload.bodyshopConfig || payload.txEnvelope?.config || {};
return {
baseUrl: cfg.baseUrl || process.env.RR_BASE_URL,
username: cfg.username || process.env.RR_USERNAME,
password: cfg.password || process.env.RR_PASSWORD,
ppsysId: cfg.ppsysId || process.env.RR_PPSYSID,
dealer_number: cfg.dealer_number || process.env.RR_DEALER_NUMBER,
store_number: cfg.store_number || process.env.RR_STORE_NUMBER,
branch_number: cfg.branch_number || process.env.RR_BRANCH_NUMBER,
rrTransport: (cfg.rrTransport || process.env.RR_TRANSPORT || "STAR").toUpperCase()
};
}
const redisSocketEvents = ({ const redisSocketEvents = ({
io, io,
@@ -340,75 +357,35 @@ const redisSocketEvents = ({
}; };
const registerRREvents = (socket) => { const registerRREvents = (socket) => {
socket.on("rr-export-job", async ({ jobid, txEnvelope }) => { // Orchestrated Export (Customer → Vehicle → Repair Order)
socket.on("rr-export-job", async (payload = {}) => {
try { try {
await RRJobExport({ // Back-compat: old callers: { jobid, txEnvelope }; new: { job, config, options }
socket, // Prefer direct job/config, otherwise try txEnvelope.{job,config}
redisHelpers: { const job = payload.job || payload.txEnvelope?.job;
setSessionData, const options = payload.options || payload.txEnvelope?.options || {};
getSessionData, const cfg = resolveRRConfigFrom(payload);
addUserSocketMapping,
removeUserSocketMapping, if (!job) {
refreshUserSocketTTL, RRLogger(socket, "error", "RR export missing job payload");
getUserSocketMappingByBodyshop, return;
setSessionTransactionData, }
getSessionTransactionData,
clearSessionTransactionData const result = await exportJobToRome(socket, job, cfg, options);
}, // Broadcast to bodyshop room for UI to pick up
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }, const room = getBodyshopRoom(socket.bodyshopId);
jobid, io.to(room).emit("rr-export-job:result", { jobid: job.id, result });
txEnvelope
});
} catch (error) { } catch (error) {
RRLogger(socket, "error", `Error during RR export: ${error.message}`); RRLogger(socket, "error", `Error during RR export: ${error.message}`);
logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack }); logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack });
} }
}); });
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId }) => { // Combined search
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
try { try {
await RRSelectedCustomer({ const cfg = resolveRRConfigFrom({}); // if you want per-call overrides, pass them in the payload and merge here
socket, const data = await lookupApi.combinedSearch(socket, params || {}, cfg);
redisHelpers: {
setSessionData,
getSessionData,
addUserSocketMapping,
removeUserSocketMapping,
refreshUserSocketTTL,
getUserSocketMappingByBodyshop,
setSessionTransactionData,
getSessionTransactionData,
clearSessionTransactionData
},
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
jobid,
selectedCustomerId
});
} catch (error) {
RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`);
logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack });
}
});
socket.on("rr-calculate-allocations", async (jobid, callback) => {
try {
const allocations = await CdkCalculateAllocations(socket, jobid);
callback(allocations);
} catch (error) {
RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`);
logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack });
}
});
socket.on("rr-lookup-combined", async ({ jobid, params }, cb) => {
try {
const { RrCombinedSearch } = require("../rr/rr-lookup");
const data = await RrCombinedSearch({
socket,
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
jobid,
params
});
cb?.(data); cb?.(data);
} catch (e) { } catch (e) {
RRLogger(socket, "error", `RR combined lookup error: ${e.message}`); RRLogger(socket, "error", `RR combined lookup error: ${e.message}`);
@@ -416,15 +393,11 @@ const redisSocketEvents = ({
} }
}); });
socket.on("rr-get-advisors", async ({ jobid, params }, cb) => { // Get Advisors
socket.on("rr-get-advisors", async ({ jobid, params } = {}, cb) => {
try { try {
const { RrGetAdvisors } = require("../rr/rr-lookup"); const cfg = resolveRRConfigFrom({});
const data = await RrGetAdvisors({ const data = await lookupApi.getAdvisors(socket, params || {}, cfg);
socket,
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
jobid,
params
});
cb?.(data); cb?.(data);
} catch (e) { } catch (e) {
RRLogger(socket, "error", `RR get advisors error: ${e.message}`); RRLogger(socket, "error", `RR get advisors error: ${e.message}`);
@@ -432,23 +405,46 @@ const redisSocketEvents = ({
} }
}); });
socket.on("rr-get-parts", async ({ jobid, params }, cb) => { // Get Parts
socket.on("rr-get-parts", async ({ jobid, params } = {}, cb) => {
try { try {
const { RrGetParts } = require("../rr/rr-lookup"); const cfg = resolveRRConfigFrom({});
const data = await RrGetParts({ const data = await lookupApi.getParts(socket, params || {}, cfg);
socket,
redisHelpers: { setSessionTransactionData, getSessionTransactionData },
jobid,
params
});
cb?.(data); cb?.(data);
} catch (e) { } catch (e) {
RRLogger(socket, "error", `RR get parts error: ${e.message}`); RRLogger(socket, "error", `RR get parts error: ${e.message}`);
cb?.(null); cb?.(null);
} }
}); });
};
// (Optional) Selected customer — only keep this if you actually implement it for RR
socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId } = {}) => {
try {
// If you dont have an RRSelectedCustomer implementation now, either:
// 1) no-op with a log, or
// 2) emit a structured event UI can handle as "not supported".
RRLogger(socket, "info", "rr-selected-customer not implemented for RR (no-op)", {
jobid,
selectedCustomerId
});
// If later you add support, call your implementation here.
} catch (error) {
RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`);
logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack });
}
});
// Calculate allocations (unchanged — CDK utility)
socket.on("rr-calculate-allocations", async (jobid, callback) => {
try {
const allocations = await RRCalculateAllocations(socket, jobid);
callback(allocations);
} catch (error) {
RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`);
logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack });
}
});
};
// Call Handlers // Call Handlers
registerRoomAndBroadcastEvents(socket); registerRoomAndBroadcastEvents(socket);
registerUpdateEvents(socket); registerUpdateEvents(socket);