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