From 2ffc4b81f45a402abe6f91faf46c07558c9228ea Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 7 Oct 2025 16:45:06 -0400 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint --- ...p-info.responsibilitycenters.component.jsx | 3 + .../shop-info.rr-configuration.component.jsx | 130 ++++ client/src/graphql/bodyshop.queries.js | 2 + package-lock.json | 77 ++- package.json | 2 + server/routes/rrRoutes.js | 264 ++++--- server/rr/rr-constants.js | 79 +++ server/rr/rr-customer.js | 161 +++-- server/rr/rr-error.js | 140 ++-- server/rr/rr-helpers.js | 651 ++++++------------ server/rr/rr-job-export.js | 646 +++-------------- server/rr/rr-logger.js | 71 +- server/rr/rr-lookup.js | 189 +++-- server/rr/rr-mappers.js | 619 ++++++++++------- server/rr/rr-repair-orders.js | 182 +++-- server/rr/rr-test.js | 127 ++++ server/rr/rr-wsdl.js | 97 +++ server/rr/xml-templates/CombinedSearch.xml | 73 ++ server/rr/xml-templates/CreateRepairOrder.xml | 158 +++++ server/rr/xml-templates/GetAdvisors.xml | 34 + server/rr/xml-templates/GetParts.xml | 50 ++ server/rr/xml-templates/InsertCustomer.xml | 102 +++ .../rr/xml-templates/InsertServiceVehicle.xml | 83 +++ server/rr/xml-templates/UpdateCustomer.xml | 107 +++ server/rr/xml-templates/UpdateRepairOrder.xml | 135 ++++ server/rr/xml-templates/_EnvelopeFooter.xml | 17 + server/rr/xml-templates/_EnvelopeHeader.xml | 29 + server/web-sockets/redisSocketEvents.js | 8 - 28 files changed, 2594 insertions(+), 1642 deletions(-) create mode 100644 client/src/components/shop-info/shop-info.rr-configuration.component.jsx create mode 100644 server/rr/rr-constants.js create mode 100644 server/rr/rr-test.js create mode 100644 server/rr/rr-wsdl.js create mode 100644 server/rr/xml-templates/CombinedSearch.xml create mode 100644 server/rr/xml-templates/CreateRepairOrder.xml create mode 100644 server/rr/xml-templates/GetAdvisors.xml create mode 100644 server/rr/xml-templates/GetParts.xml create mode 100644 server/rr/xml-templates/InsertCustomer.xml create mode 100644 server/rr/xml-templates/InsertServiceVehicle.xml create mode 100644 server/rr/xml-templates/UpdateCustomer.xml create mode 100644 server/rr/xml-templates/UpdateRepairOrder.xml create mode 100644 server/rr/xml-templates/_EnvelopeFooter.xml create mode 100644 server/rr/xml-templates/_EnvelopeHeader.xml diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 53f780c6b..9fde88e7c 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -14,6 +14,7 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component"; +import ShopInfoRRConfigurationComponent from "./shop-info.rr-configuration.component"; const SelectorDiv = styled.div` .ant-form-item .ant-select { @@ -63,6 +64,8 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) && ( <> + {bodyshop.rr_dealerid && } + {bodyshop.cdk_dealerid && ( {form.getFieldValue("cdk_dealerid")} )} diff --git a/client/src/components/shop-info/shop-info.rr-configuration.component.jsx b/client/src/components/shop-info/shop-info.rr-configuration.component.jsx new file mode 100644 index 000000000..1f2662c22 --- /dev/null +++ b/client/src/components/shop-info/shop-info.rr-configuration.component.jsx @@ -0,0 +1,130 @@ +import { Card, Divider, Form, Input, Select, Switch, Tooltip } from "antd"; + +const { Option } = Select; + +/** + * Reynolds & Reynolds Configuration Section + * Stored under bodyshop.rr_configuration (JSONB) + * + * NOTE: + * - Do NOT put credentials/endpoints here. Those live in env secrets. + * - These are dealer/location-specific values that the backend reads from rr_configuration. + * Keys match server usage: dealer_code, dealer_name, location_id, store_number, branch_number, etc. + */ +export default function ShopInfoRRConfigurationComponent() { + return ( + + {/* Dealer / Location identifiers (dealer-specific, not secrets) */} + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Feature flags (safe to store in config) */} + + + + + + + + + + + + + + + {/* Optional UX/format defaults */} + + + + + + Timezone{" "} + + + + + } + name={["rr_configuration", "timezone"]} + rules={[{ required: true, message: "Timezone is required" }]} + > + + + + ); +} diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 5b9980295..28c81b05b 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -124,6 +124,7 @@ export const QUERY_BODYSHOP = gql` md_email_cc timezone ss_configuration + rr_configuration md_from_emails last_name_first md_parts_order_comment @@ -258,6 +259,7 @@ export const UPDATE_SHOP = gql` md_email_cc timezone ss_configuration + rr_configuration md_from_emails last_name_first md_parts_order_comment diff --git a/package-lock.json b/package-lock.json index 50c4cf179..a7d5ca9c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "dinero.js": "^1.9.1", "dotenv": "^17.2.3", "express": "^4.21.1", + "fast-xml-parser": "^5.3.0", "firebase-admin": "^13.5.0", "graphql": "^16.11.0", "graphql-request": "^6.1.0", @@ -48,6 +49,7 @@ "moment": "^2.30.1", "moment-timezone": "^0.6.0", "multer": "^2.0.2", + "mustache": "^4.2.0", "node-persist": "^4.0.4", "nodemailer": "^6.10.0", "phone": "^3.1.67", @@ -1221,18 +1223,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/@aws-sdk/xml-builder/node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/@aws/lambda-invoke-store": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", @@ -1713,6 +1703,38 @@ "node": ">=14" } }, + "node_modules/@google-cloud/storage/node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@google-cloud/storage/node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/@google-cloud/storage/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -5895,23 +5917,18 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.0.tgz", + "integrity": "sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", - "optional": true, "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -8158,6 +8175,15 @@ "node": ">= 10.16.0" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mutexify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", @@ -10327,17 +10353,16 @@ "license": "MIT" }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/stubs": { "version": "3.0.0", diff --git a/package.json b/package.json index be663afa2..0c61159e1 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dinero.js": "^1.9.1", "dotenv": "^17.2.3", "express": "^4.21.1", + "fast-xml-parser": "^5.3.0", "firebase-admin": "^13.5.0", "graphql": "^16.11.0", "graphql-request": "^6.1.0", @@ -57,6 +58,7 @@ "moment": "^2.30.1", "moment-timezone": "^0.6.0", "multer": "^2.0.2", + "mustache": "^4.2.0", "node-persist": "^4.0.4", "nodemailer": "^6.10.0", "phone": "^3.1.67", diff --git a/server/routes/rrRoutes.js b/server/routes/rrRoutes.js index 04f1b0efa..b22fa6489 100644 --- a/server/routes/rrRoutes.js +++ b/server/routes/rrRoutes.js @@ -1,12 +1,8 @@ -// ----------------------------------------------------------------------------- -// RR (Reynolds & Reynolds) HTTP routes -// - Mirrors /cdk shape so the UI can switch providers with minimal changes -// - Uses validateFirebaseIdTokenMiddleware + withUserGraphQLClientMiddleware -// - Calls into rr/* modules which wrap MakeRRCall from rr-helpers -// -// TODO:RR — As you wire the real RR endpoints + schemas, adjust the request -// bodies, query params, and response normalization inside rr/* files. -// ----------------------------------------------------------------------------- +/** + * @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(); @@ -16,143 +12,197 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl const { RrCombinedSearch, RrGetAdvisors, RrGetParts } = require("../rr/rr-lookup"); const { RrCustomerInsert, RrCustomerUpdate } = require("../rr/rr-customer"); -// NOTE: correct filename is rr-repair-orders.js (plural) const { CreateRepairOrder, UpdateRepairOrder } = require("../rr/rr-repair-orders"); +const { ExportJobToRR } = require("../rr/rr-job-export"); +const RRLogger = require("../rr/rr-logger"); -// Require auth on all RR routes (keep parity with /cdk) +/** + * Apply global middlewares: + * - Firebase token validation (auth) + * - GraphQL client injection (Hasura access) + */ router.use(validateFirebaseIdTokenMiddleware); +router.use(withUserGraphQLClientMiddleware); -// ----------------------------------------------------------------------------- -// Accounting parity / scaffolding -// ----------------------------------------------------------------------------- +/** + * Health check / diagnostic route + */ +router.get("/", async (req, res) => { + res.status(200).json({ provider: "Reynolds & Reynolds (Rome)", status: "OK" }); +}); -// Reuse CDK allocations for now; keep the endpoint name identical to /cdk -router.post("/calculate-allocations", withUserGraphQLClientMiddleware, async (req, res) => { +/** + * Full DMS export for a single job + * POST /rr/job/export + * Body: { JobData: {...} } + */ +router.post("/job/export", async (req, res) => { try { - const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default; - const result = await CalculateAllocations(req, req.body.jobid, true); // verbose=true (like Fortellis flow) - res.status(200).json({ data: result }); - } catch (e) { - req.logger?.log("rr-calc-allocations-route", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); + 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 }); } }); -// Placeholder for a future RR "get vehicles" endpoint to match /cdk/getvehicles -router.post("/getvehicles", withUserGraphQLClientMiddleware, async (_req, res) => { - res.status(501).json({ error: "RR getvehicles not implemented yet" }); -}); - -// ----------------------------------------------------------------------------- -// Lookup endpoints -// ----------------------------------------------------------------------------- - -// GET /rr/lookup/combined?vin=...&lastName=... -router.get("/lookup/combined", async (req, res) => { - try { - const params = Object.entries(req.query); // [["vin","..."], ["lastName","..."]] - const data = await RrCombinedSearch({ socket: req, redisHelpers: req.sessionUtils, jobid: "ad-hoc", params }); - res.status(200).json({ data }); - } catch (e) { - req.logger?.log("rr-lookup-combined", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); - } -}); - -// GET /rr/advisors?locationId=... -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({ data }); - } catch (e) { - req.logger?.log("rr-get-advisors", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); - } -}); - -// GET /rr/parts?partNumber=...&make=... -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({ data }); - } catch (e) { - req.logger?.log("rr-get-parts", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); - } -}); - -// ----------------------------------------------------------------------------- -// Customer endpoints -// ----------------------------------------------------------------------------- - -// POST /rr/customer/insert -// Body: { ...JobData-like shape used by rr-mappers } +/** + * Customer insert + * POST /rr/customer/insert + */ router.post("/customer/insert", async (req, res) => { try { - const data = await RrCustomerInsert({ socket: req, redisHelpers: req.sessionUtils, JobData: req.body }); - res.status(200).json({ data }); - } catch (e) { - req.logger?.log("rr-customer-insert", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); + 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 }); } }); -// PUT /rr/customer/update/:id -// Body: { JobData, existingCustomer, patch } +/** + * 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: req.body?.JobData, - existingCustomer: req.body?.existingCustomer, - patch: req.body?.patch + JobData, + existingCustomer, + patch }); - res.status(200).json({ data }); - } catch (e) { - req.logger?.log("rr-customer-update", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); + + 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 }); } }); -// ----------------------------------------------------------------------------- -// Repair Order endpoints -// ----------------------------------------------------------------------------- - -// POST /rr/repair-order/create -// Body: { JobData, txEnvelope } +/** + * 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: req.body?.JobData, - txEnvelope: req.body?.txEnvelope + JobData, + txEnvelope }); - res.status(200).json({ data }); - } catch (e) { - req.logger?.log("rr-ro-create", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); + + 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 }); } }); -// PUT /rr/repair-order/update/:id -// Body: { JobData, txEnvelope } +/** + * 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: req.body?.JobData, - txEnvelope: req.body?.txEnvelope + JobData, + txEnvelope }); - res.status(200).json({ data }); - } catch (e) { - req.logger?.log("rr-ro-update", "ERROR", "api", "rr", { message: e.message, stack: e.stack }); - res.status(500).json({ error: e.message }); + + 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-constants.js b/server/rr/rr-constants.js new file mode 100644 index 000000000..94fbf5160 --- /dev/null +++ b/server/rr/rr-constants.js @@ -0,0 +1,79 @@ +/** + * @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. + */ + +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 +}); + +module.exports = { + RR_TIMEOUT_MS, + RR_NAMESPACE_URI, + RR_DEFAULT_MAX_RESULTS, + RR_ACTIONS, + RR_SOAP_HEADERS, + buildSoapEnvelope, + getBaseRRConfig +}; diff --git a/server/rr/rr-customer.js b/server/rr/rr-customer.js index 317714ae7..f6a1a5d44 100644 --- a/server/rr/rr-customer.js +++ b/server/rr/rr-customer.js @@ -1,66 +1,137 @@ -// ----------------------------------------------------------------------------- -// RR Customer endpoints (create/update) wired through MakeRRCall. -// Shapes are mapped via rr-mappers.js and validated via rr-error.js. -// -// What’s still missing (complete when you wire to the PDFs): -// - Final request envelopes & field names in rr-mappers.js -// - Definitive success/error envelope checks in rr-error.js -// - Any RR-specific headers (dealer/tenant/site) once known -// ----------------------------------------------------------------------------- +/** + * @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). + */ const { MakeRRCall, RRActions } = require("./rr-helpers"); const { assertRrOk } = require("./rr-error"); 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"); /** - * Create a customer in RR. + * Fetch rr_configuration for the current bodyshop directly from DB. + * This ensures we always have the latest Dealer/Location mapping. + */ +async function getDealerConfigFromDB(bodyshopId, logger) { + try { + const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId }); + const config = result?.bodyshops_by_pk?.rr_configuration || null; + + if (!config) { + throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`); + } + + logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config); + return config; + } catch (error) { + logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, { + bodyshopId, + message: error.message, + stack: error.stack + }); + throw error; + } +} + +/** + * CUSTOMER INSERT (Rome Customer Insert Specification 1.2) + * Creates a new customer record in the DMS. * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - redisHelpers API (not used here directly) - * @param {Object} deps.JobData - Rome Job data used to build the payload - * @returns {Promise} RR response (envelope TBD) + * @param {object} options + * @param {object} options.socket - socket.io connection or express req + * @param {object} options.redisHelpers + * @param {object} options.JobData - normalized job record */ async function RrCustomerInsert({ socket, redisHelpers, JobData }) { - // Map JobData -> RR "Customer Insert" request body - const body = mapCustomerInsert(JobData); + const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; + const logger = socket?.logger || console; - const data = await MakeRRCall({ - ...RRActions.CreateCustomer, // POST /customer/v1/ - body, - redisHelpers, - socket, - jobid: JobData?.id - }); + try { + RRLogger(socket, "info", "RR Customer Insert started", { jobid: JobData?.id, bodyshopId }); - // TODO: assertRrOk should be updated once RR’s success envelope is finalized - return assertRrOk(data, { apiName: "RR Create Customer" }); + const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger); + + // 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, + socket, + jobid: JobData.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 + }); + + return response; + } catch (error) { + RRLogger(socket, "error", `RR Customer Insert failed: ${error.message}`, { jobid: JobData?.id }); + throw error; + } } /** - * Update an existing customer in RR. + * CUSTOMER UPDATE (Rome Customer Update Specification 1.2) + * Updates an existing RR customer record. * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - * @param {Object} deps.JobData - context only (job id for correlation) - * @param {Object} deps.existingCustomer - Current RR customer record - * @param {Object} deps.patch - Minimal delta from UI to apply onto existingCustomer - * @returns {Promise} RR response + * @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 }) { - // Build a merged/normalized payload for RR Update - const body = mapCustomerUpdate(existingCustomer, patch); + const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; + const logger = socket?.logger || console; - const data = await MakeRRCall({ - ...RRActions.UpdateCustomer, // PUT /customer/v1/ (append id inside body/path per final spec) - body, - redisHelpers, - socket, - jobid: JobData?.id - }); + try { + RRLogger(socket, "info", "RR Customer Update started", { + jobid: JobData?.id, + bodyshopId, + existingCustomerId: existingCustomer?.CustomerId + }); - return assertRrOk(data, { apiName: "RR Update Customer" }); + const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger); + + // Build Mustache variables for the UpdateCustomer.xml template + const vars = mapCustomerUpdate(existingCustomer, patch, dealerConfig); + + const data = await MakeRRCall({ + action: RRActions.UpdateCustomer, // resolves to SOAPAction + URL + body: { template: "UpdateCustomer", data: vars }, // render server/rr/xml-templates/UpdateCustomer.xml + redisHelpers, + socket, + jobid: JobData.id + }); + + const response = assertRrOk(data, { apiName: "RR Update Customer" }); + RRLogger(socket, "debug", "RR Customer Update success", { + jobid: JobData?.id, + customerId: existingCustomer?.CustomerId + }); + + return response; + } catch (error) { + RRLogger(socket, "error", `RR Customer Update failed: ${error.message}`, { + jobid: JobData?.id, + customerId: existingCustomer?.CustomerId + }); + throw error; + } } -module.exports = { RrCustomerInsert, RrCustomerUpdate }; +module.exports = { + RrCustomerInsert, + RrCustomerUpdate, + getDealerConfigFromDB +}; diff --git a/server/rr/rr-error.js b/server/rr/rr-error.js index a93241304..efc8cdadc 100644 --- a/server/rr/rr-error.js +++ b/server/rr/rr-error.js @@ -1,67 +1,103 @@ -// ----------------------------------------------------------------------------- -// Error handling utilities for Reynolds & Reynolds (RR) API calls. -// This mirrors Fortellis/CDK error helpers so the call pipeline stays uniform. -// -// TODO:RR — Replace the heuristics in assertRrOk with the *actual* envelope and -// status semantics from the Rome RR specs. Examples in the PDFs may show: -// - Success -// - Some message -// - or a SuccessFlag/ReturnCode element in the JSON/XML response. -// ----------------------------------------------------------------------------- +/** + * @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.) + */ +/** + * Custom Error type for RR API responses + */ class RrApiError extends Error { - /** - * @param {string} message - Human-readable message - * @param {object} opts - * @param {string} [opts.reqId] - Internal request identifier - * @param {string} [opts.url] - Target URL of the API call - * @param {string} [opts.apiName] - Which API was invoked (for context) - * @param {object} [opts.errorData] - Raw error payload from RR - * @param {number} [opts.status] - HTTP status code - * @param {string} [opts.statusText] - HTTP status text - */ constructor(message, { reqId, url, apiName, errorData, status, statusText } = {}) { super(message); this.name = "RrApiError"; - this.reqId = reqId; - this.url = url; - this.apiName = apiName; - this.errorData = errorData; - this.status = status; - this.statusText = statusText; + this.reqId = reqId || null; + this.url = url || null; + this.apiName = apiName || null; + this.errorData = errorData || null; + this.status = status || null; + this.statusText = statusText || null; } } /** - * Assert that an RR API response is considered "OK". - * Throws RrApiError otherwise. + * Assert that a Reynolds & Reynolds response is successful. * - * @param {*} data - Parsed response object from MakeRRCall - * @param {object} opts - * @param {string} opts.apiName - Which API we're checking (for error messages) - * @param {boolean} [opts.allowEmpty=false] - If true, allow null/empty results - * @returns {*} - The same data if valid + * Expected success structure (based on Rome RR specs): + * { + * "SuccessFlag": true, + * "ErrorCode": "0", + * "ErrorMessage": "", + * "Data": { ... } + * } + * + * Or if SOAP/XML-based: + * { + * "Envelope": { + * "Body": { + * "Response": { + * "SuccessFlag": true, + * ... + * } + * } + * } + * } + * + * This helper unwraps and normalizes the response to detect any error cases. */ -function assertRrOk(data, { apiName, allowEmpty = false } = {}) { - // TODO:RR — Update logic to exactly match RR's success envelope. - // Possible patterns to confirm from PDFs: - // - data.Status?.code === "0" - // - data.Return?.successFlag === true - // - data.Errors is missing or empty - // - // For now, we use a simple heuristic fallback. - - const hasErrors = - data == null || - data.error || - (Array.isArray(data.errors) && data.errors.length > 0) || - (data.Status && data.Status.severity === "ERROR"); - - if (!allowEmpty && hasErrors) { - throw new RrApiError(`${apiName} returned an error`, { errorData: data, apiName }); +function assertRrOk(data, { apiName = "RR API Call", allowEmpty = false } = {}) { + if (!data && !allowEmpty) { + throw new RrApiError(`${apiName} returned no data`, { apiName }); } - return data; + // 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 + }); } -module.exports = { RrApiError, assertRrOk }; +/** + * Safely unwrap nested RR API responses for consistency across handlers. + */ +function extractRrResponseData(data) { + if (!data) return null; + + return ( + data?.Envelope?.Body?.Response?.Data || + data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]]?.Data || + data?.Data || + data + ); +} + +module.exports = { + RrApiError, + assertRrOk, + extractRrResponseData +}; diff --git a/server/rr/rr-helpers.js b/server/rr/rr-helpers.js index 39789b51c..5d3af464f 100644 --- a/server/rr/rr-helpers.js +++ b/server/rr/rr-helpers.js @@ -1,488 +1,257 @@ /** - * RR (Reynolds & Reynolds) helper module - * ----------------------------------------------------------------------------- - * Responsibilities - * - Load env (.env.{NODE_ENV}) - * - Provide token retrieval with simple Redis-backed caching - * - Normalized HTTP caller (MakeRRCall) with request-id + idempotency key - * - URL constructor w/ path + query params - * - Optional delayed/batch polling stub (DelayedCallback) - * - Central action registry (RRActions) with prod/uat base URLs (PLACEHOLDERS) - * - Common cache enums + TTL + transaction-type helper (parity with Fortellis) - * - * What’s missing / TODOs to make this “real” (per RR/Rome PDFs you provided): - * - Implement the actual RR auth/token flow inside getRRToken() - * - Replace all RRActions URLs with the final endpoints from the RR spec - * - Confirm final header names (e.g., X-Request-Id, Idempotency-Key) - * - If RR uses async “batch/status/result”, adapt DelayedCallback() to spec - * - Confirm success/error envelope and centralize in rr-error.js + * @file rr-helpers.js + * @description Core helper functions for Reynolds & Reynolds integration. + * Handles XML rendering, SOAP communication, and configuration merging. */ +const fs = require("fs/promises"); const path = require("path"); -require("dotenv").config({ - path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) -}); +const mustache = require("mustache"); +const axios = require("axios"); +const { v4: uuidv4 } = require("uuid"); +const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants"); +const RRLogger = require("./rr-logger"); +const { client } = require("../graphql-client/graphql-client"); +const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries"); -const uuid = require("uuid").v4; -const AxiosLib = require("axios").default; -const axios = AxiosLib.create(); -const axiosCurlirize = require("axios-curlirize").default; +/* ------------------------------------------------------------------------------------------------ + * Configuration + * ----------------------------------------------------------------------------------------------*/ -const logger = require("../utils/logger"); -const { RrApiError } = require("./rr-error"); - -// Emit curl equivalents for dev troubleshooting (safe to disable in prod) -axiosCurlirize(axios, (result /*, err */) => { +/** + * 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 + */ +async function getDealerConfig(bodyshopId) { try { - const { command } = result; - // Pipe to your centralized logger if preferred: - // logger.log("rr-axios-curl", "DEBUG", "api", null, { command }); - if (process.env.NODE_ENV !== "production") { - console.log("*** rr axios (curl):", command); - } - } catch { - // Best-effort only - } -}); - -const isProduction = process.env.NODE_ENV === "production"; - -/** - * Transaction key namespace (mirrors Fortellis' getTransactionType) - * Used to partition per-job Redis session hashes. - */ -const getTransactionType = (jobid) => `rr:${jobid}`; - -/** - * Default per-transaction TTL for RR data cached in Redis (seconds). - * Keep parity with the Fortellis helper to avoid drift. - */ -const defaultRRTTL = 60 * 60; // 1 hour - -/** - * Namespaced keys stored under each transaction hash (parity with Fortellis) - * These are referenced across rr-job-export.js (and friends). - */ -const RRCacheEnums = { - txEnvelope: "txEnvelope", - DMSBatchTxn: "DMSBatchTxn", - SubscriptionMeta: "SubscriptionMeta", // kept for parity; not used yet for RR - DepartmentId: "DepartmentId", // kept for parity; not used yet for RR - JobData: "JobData", - DMSVid: "DMSVid", - DMSVeh: "DMSVeh", - DMSVehCustomer: "DMSVehCustomer", - DMSCustList: "DMSCustList", - DMSCust: "DMSCust", - selectedCustomerId: "selectedCustomerId", - DMSTransHeader: "DMSTransHeader", - transWips: "transWips", - DmsBatchTxnPost: "DmsBatchTxnPost", - DMSVehHistory: "DMSVehHistory" -}; - -/** - * Provider-level token cache. - * We reuse redisHelpers.setSessionData/getSessionData with a synthetic "socketId" - * so we don’t need direct access to the Redis client here. - */ -const RR_PROVIDER_TOKEN_BUCKET = "rr:provider-token"; // becomes key: "socket:rr:provider-token" -const RR_PROVIDER_TOKEN_FIELD = "token"; - -/** - * Fetch an RR access token. - * TODO: Implement the *actual* RR auth flow per the spec (client credentials - * or whatever RCI requires). This stub uses an env or a fixed dev token. - * - * @param {Object} deps - * @param {Object} deps.redisHelpers - Your redisHelpers API - * @returns {Promise} accessToken - */ -async function getRRToken({ redisHelpers }) { - try { - // Try the cache first - const cached = await redisHelpers.getSessionData(RR_PROVIDER_TOKEN_BUCKET, RR_PROVIDER_TOKEN_FIELD); - if (cached?.accessToken && cached?.expiresAt && Date.now() < cached.expiresAt - 5000) { - return cached.accessToken; - } - - // TODO: Replace with real RR auth call. For now, fallback to env. - const accessToken = process.env.RR_FAKE_TOKEN || "rr-dev-token"; - // Artificial ~55m expiry (adjust to actual token TTL) - const expiresAt = Date.now() + 55 * 60 * 1000; - - await redisHelpers.setSessionData( - RR_PROVIDER_TOKEN_BUCKET, - RR_PROVIDER_TOKEN_FIELD, - { accessToken, expiresAt }, - 60 * 60 // TTL safety net - ); - - return accessToken; - } catch (error) { - logger.log("rr-get-token-error", "ERROR", "api", "rr", { - message: error?.message, - stack: error?.stack - }); - // Keep local dev moving even if cache errors - return process.env.RR_FAKE_TOKEN || "rr-dev-token"; + 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 {}; } } /** - * Construct a full URL including optional path segment and query params. - * Matches the function signature used elsewhere in the codebase. - * - * @param {Object} args - * @param {string} args.url - base URL (may or may not end with "/") - * @param {string} [args.pathParams] - string appended to URL (no leading slash) - * @param {Array<[string,string]>} [args.requestSearchParams] - pairs converted to query params - * @returns {string} + * 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 */ -function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) { - // normalize: ensure exactly one trailing slash on base - url = url.replace(/\/+$/, "/"); - const fullPath = pathParams ? `${url}${pathParams}` : url; - const query = new URLSearchParams(requestSearchParams).toString(); - return query ? `${fullPath}?${query}` : fullPath; +async function resolveRRConfig(socket) { + const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid; + const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {}; + return { ...getBaseRRConfig(), ...dealerCfg }; +} + +/* ------------------------------------------------------------------------------------------------ + * 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); } /** - * Optional delayed/batch polling flow (placeholder). - * If RR returns a "check later" envelope, use this to poll until "complete". - * Adjust the header names and result shapes once you have the real spec. - * - * @param {Object} args - * @param {Object} args.delayMeta - body returned from initial RR call with status link(s) - * @param {string} args.access_token - token to reuse for polling - * @param {string} args.reqId - correlation id - * @returns {Promise} + * 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 DelayedCallback({ delayMeta, access_token, reqId }) { - // Stub example — adapt to RR if they do a batch/status-result pattern - for (let attempt = 0; attempt < 5; attempt++) { - await sleep((delayMeta?.checkStatusAfterSeconds || 2) * 1000); +async function buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) { + const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars); - const statusUrl = delayMeta?._links?.status?.href; - if (!statusUrl) { - return { error: "No status URL provided by RR batch envelope." }; - } + return ` + + + ${headerXml} + + + ${renderedBodyXml} + + + `.trim(); +} - const statusResult = await axios.get(statusUrl, { - headers: { - Authorization: `Bearer ${access_token}`, - "X-Request-Id": reqId - } - }); +/* ------------------------------------------------------------------------------------------------ + * Core SOAP caller + * ----------------------------------------------------------------------------------------------*/ - if (statusResult?.data?.status === "complete") { - const resultUrl = statusResult?.data?._links?.result?.href; - if (!resultUrl) return statusResult.data; - const batchResult = await axios.get(resultUrl, { - headers: { - Authorization: `Bearer ${access_token}`, - "X-Request-Id": reqId - } - }); - return batchResult.data; - } +/** + * 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 }; } - return { error: "Batch result still not complete after max attempts." }; -} -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + 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."); } /** - * Core caller. Mirrors Fortellis' MakeFortellisCall shape so we can reuse flow. + * Constructs and sends a SOAP call to the Reynolds & Reynolds endpoint. * - * @param {Object} args - * @param {string} args.apiName - logical name (used in logs/errors) - * @param {string} args.url - base endpoint - * @param {Object} [args.headers] - extra headers to send - * @param {Object} [args.body] - POST/PUT body - * @param {"get"|"post"|"put"|"delete"} [args.type="post"] - * @param {boolean} [args.debug=true] - * @param {string} [args.requestPathParams] - path segment to append to url - * @param {Array<[string,string]>} [args.requestSearchParams=[]] - tuples of [key, val] for query params - * @param {string|number} [args.jobid] - used for logger correlation (optional) - * @param {Object} args.redisHelpers - your redisHelpers api (for token cache) - * @param {Object} [args.socket] - pass-through so we can pull user/email if needed - * @returns {Promise} + * 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 */ async function MakeRRCall({ - apiName, - url, - headers = {}, - body = {}, - type = "post", - debug = true, - requestPathParams, - requestSearchParams = [], + action, + body, + socket, + // redisHelpers, jobid, - redisHelpers, - socket + dealerConfig, + retries = 1 }) { - const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams }); - const reqId = uuid(); - const idempotencyKey = uuid(); - const access_token = await getRRToken({ redisHelpers }); + const correlationId = uuidv4(); - if (debug) { - logger.log("rr-call", "DEBUG", socket?.user?.email, null, { - apiName, - type, - url: fullUrl, - jobid, - reqId, - body: safeLogJson(body) - }); + 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 || {}); } - try { - const baseHeaders = { - Authorization: `Bearer ${access_token}`, - "X-Request-Id": reqId, - "Idempotency-Key": idempotencyKey, - ...headers - }; + // 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 + }; - let resp; - switch ((type || "post").toLowerCase()) { - case "get": - resp = await axios.get(fullUrl, { headers: baseHeaders }); - break; - case "put": - resp = await axios.put(fullUrl, body, { headers: baseHeaders }); - break; - case "delete": - // Some APIs require body with DELETE; axios supports { data } for that - resp = await axios.delete(fullUrl, { headers: baseHeaders, data: body }); - break; - case "post": - default: - resp = await axios.post(fullUrl, body, { headers: baseHeaders }); - break; - } + // Build full SOAP envelope with proper header + const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars); - if (debug) { - logger.log("rr-response", "DEBUG", socket?.user?.email, null, { - apiName, - reqId, - data: safeLogJson(resp?.data) - }); - } + RRLogger(socket, "info", `RR → ${soapAction || "SOAP"} request`, { + jobid, + url, + correlationId + }); - // If RR returns a "check later" envelope, route through DelayedCallback - if (resp?.data?.checkStatusAfterSeconds) { - const delayed = await DelayedCallback({ - delayMeta: resp.data, - access_token, - reqId - }); - return delayed; - } + const headers = { + ...RR_SOAP_HEADERS, + SOAPAction: soapAction, + "Content-Type": "text/xml; charset=utf-8", + "X-Request-Id": correlationId + }; - return resp?.data; - } catch (error) { - // Handle 429 backoff hint (simple single-retry stub) - if (error?.response?.status === 429) { - const retryAfter = Number(error.response.headers?.["retry-after"] || 1); - await sleep(retryAfter * 1000); - return MakeRRCall({ - apiName, - url, + let attempt = 0; + while (attempt <= retries) { + attempt += 1; + try { + const response = await axios.post(url, soapEnvelope, { headers, - body, - type, - debug, - requestPathParams, - requestSearchParams, - jobid, - redisHelpers, - socket + timeout: effectiveConfig.timeout || 30000, + responseType: "text", + validateStatus: () => true }); + + 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; } - - const errPayload = { - reqId, - url: fullUrl, - apiName, - errorData: error?.response?.data, - status: error?.response?.status, - statusText: error?.response?.statusText - }; - - // Log and throw a typed error (consistent with Fortellis helpers) - logger.log("rr-call-error", "ERROR", socket?.user?.email, null, { - ...errPayload, - message: error?.message, - stack: error?.stack - }); - - throw new RrApiError(`RR API call failed for ${apiName}: ${error?.message}`, errPayload); } } -/** - * Central action registry. - * TODO: Replace ALL URLs with real RR endpoints from the Rome/RR specs. - * You can later split into domain-specific registries if it grows large. - */ -const RRActions = { - // Vehicles - GetVehicleId: { - apiName: "RR Get Vehicle Id", - url: isProduction - ? "https://rr.example.com/service-vehicle-mgmt/v1/vehicle-ids/" // append VIN - : "https://rr-uat.example.com/service-vehicle-mgmt/v1/vehicle-ids/", - type: "get" - }, - ReadVehicle: { - apiName: "RR Read Vehicle", - url: isProduction - ? "https://rr.example.com/service-vehicle-mgmt/v1/" // append vehicleId - : "https://rr-uat.example.com/service-vehicle-mgmt/v1/", - type: "get" - }, - InsertVehicle: { - apiName: "RR Insert Service Vehicle", - url: isProduction - ? "https://rr.example.com/service-vehicle-mgmt/v1/" - : "https://rr-uat.example.com/service-vehicle-mgmt/v1/", - type: "post" - }, - UpdateVehicle: { - apiName: "RR Update Service Vehicle", - url: isProduction - ? "https://rr.example.com/service-vehicle-mgmt/v1/" - : "https://rr-uat.example.com/service-vehicle-mgmt/v1/", - type: "put" - }, +/* ------------------------------------------------------------------------------------------------ + * Exports + * ----------------------------------------------------------------------------------------------*/ - // Customers - CreateCustomer: { - apiName: "RR Create Customer", - url: isProduction ? "https://rr.example.com/customer/v1/" : "https://rr-uat.example.com/customer/v1/", - type: "post" - }, - UpdateCustomer: { - apiName: "RR Update Customer", - url: isProduction - ? "https://rr.example.com/customer/v1/" // append /{id} if required by spec - : "https://rr-uat.example.com/customer/v1/", - type: "put" - }, - ReadCustomer: { - apiName: "RR Read Customer", - url: isProduction - ? "https://rr.example.com/customer/v1/" // append /{id} - : "https://rr-uat.example.com/customer/v1/", - type: "get" - }, - SearchCustomer: { - apiName: "RR Query Customer By Name", - url: isProduction ? "https://rr.example.com/customer/v1/search" : "https://rr-uat.example.com/customer/v1/search", - type: "get" - }, - - // Combined search (customer + vehicle) - CombinedSearch: { - apiName: "RR Combined Search (Customer + Vehicle)", - url: isProduction - ? "https://rr.example.com/search/v1/customer-vehicle" - : "https://rr-uat.example.com/search/v1/customer-vehicle", - type: "get" - }, - - // Advisors - GetAdvisors: { - apiName: "RR Get Advisors", - url: isProduction ? "https://rr.example.com/advisors/v1" : "https://rr-uat.example.com/advisors/v1", - type: "get" - }, - - // Parts - GetParts: { - apiName: "RR Get Parts", - url: isProduction ? "https://rr.example.com/parts/v1" : "https://rr-uat.example.com/parts/v1", - type: "get" - }, - - // GL / WIP (mirroring your existing flows; endpoints are placeholders) - StartWip: { - apiName: "RR Start WIP", - url: isProduction ? "https://rr.example.com/glpost/v1/startWIP" : "https://rr-uat.example.com/glpost/v1/startWIP", - type: "post" - }, - TranBatchWip: { - apiName: "RR Trans Batch WIP", - url: isProduction - ? "https://rr.example.com/glpost/v1/transBatchWIP" - : "https://rr-uat.example.com/glpost/v1/transBatchWIP", - type: "post" - }, - PostBatchWip: { - apiName: "RR Post Batch WIP", - url: isProduction - ? "https://rr.example.com/glpost/v1/postBatchWIP" - : "https://rr-uat.example.com/glpost/v1/postBatchWIP", - type: "post" - }, - QueryErrorWip: { - apiName: "RR Query Error WIP", - url: isProduction ? "https://rr.example.com/glpost/v1/errWIP" : "https://rr-uat.example.com/glpost/v1/errWIP", - type: "get" - }, - - // Service history (header insert) - ServiceHistoryInsert: { - apiName: "RR Service Vehicle History Insert", - url: isProduction - ? "https://rr.example.com/service-vehicle-history-mgmt/v1/" - : "https://rr-uat.example.com/service-vehicle-history-mgmt/v1/", - type: "post" - }, - - // Repair Orders - CreateRepairOrder: { - apiName: "RR Create Repair Order", - url: isProduction ? "https://rr.example.com/repair-orders/v1" : "https://rr-uat.example.com/repair-orders/v1", - type: "post" - }, - UpdateRepairOrder: { - apiName: "RR Update Repair Order", - url: isProduction - ? "https://rr.example.com/repair-orders/v1/" // append /{id} if required - : "https://rr-uat.example.com/repair-orders/v1/", - type: "put" - } -}; - -/** - * Safe JSON logger helper to avoid huge payloads/recursive structures in logs. - */ -function safeLogJson(data) { - try { - const text = JSON.stringify(data); - // cap to ~5k for logs - return text.length > 5000 ? `${text.slice(0, 5000)}… [truncated]` : text; - } catch { - return "[unserializable]"; - } -} +const RRActions = RR_ACTIONS; module.exports = { - // core helpers MakeRRCall, - RRActions, - getRRToken, - constructFullUrl, - DelayedCallback, - - // parity exports required by other RR modules - getTransactionType, - defaultRRTTL, - RRCacheEnums + getDealerConfig, + renderXmlTemplate, + resolveRRConfig, + RRActions }; diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index f35c1caf1..bbc336653 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -1,567 +1,133 @@ -// ----------------------------------------------------------------------------- -// Reynolds & Reynolds (RR) Job Export flow (scaffold). -// -// Parity with Fortellis/CDK export shape so the UI + socket flows remain -// consistent: -// -// - RRJobExport: initial VIN/customer discovery & prompt for customer select -// - RRSelectedCustomer: create/update customer, insert/read vehicle, -// post WIP batch, post history, mark success/failure, notify client -// -// What’s still missing (fill in from Rome/RR PDFs you provided): -// - Exact request/response envelopes for each RR operation -// (Customer Insert/Update, Vehicle Insert/Read, WIP APIs, Service History). -// - Final success/error conditions for assertRrOk (we currently use heuristics). -// - Precise field mappings inside CreateCustomer, InsertVehicle, -// StartWip/TransBatchWip/PostBatchWip, InsertServiceVehicleHistory. -// ----------------------------------------------------------------------------- +/** + * @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. + */ -const { GraphQLClient } = require("graphql-request"); -const moment = require("moment-timezone"); - -const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default; // reuse allocations -const CreateRRLogEvent = require("./rr-logger"); -const queries = require("../graphql-client/queries"); -const { MakeRRCall, RRActions, getTransactionType, defaultRRTTL, RRCacheEnums } = require("./rr-helpers"); - -// ----------------------------------------------------------------------------- -// Public entry points (wired in redisSocketEvents.js) -// ----------------------------------------------------------------------------- +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"); /** - * Seed export: cache txEnvelope + JobData, discover VIN->VehicleId + owner, - * search by customer name, and prompt client to select/create a customer. + * Inserts a service vehicle record for the repair order. + * Follows the "Rome Insert Service Vehicle Interface Specification" via SOAP/XML. */ -async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { - const { setSessionTransactionData } = redisHelpers; - +async function RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig }) { try { - CreateRRLogEvent(socket, "DEBUG", `[RR] Received Job export request`, { jobid }); + RRLogger(socket, "info", "RR Insert Service Vehicle started", { jobid: JobData?.id }); - // cache txEnvelope for this job session - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.txEnvelope, - txEnvelope, - defaultRRTTL - ); + // Build Mustache variables for server/rr/xml-templates/InsertServiceVehicle.xml + const variables = mapServiceVehicleInsert(JobData, dealerConfig); - const JobData = await QueryJobData({ socket, jobid }); - await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData, JobData, defaultRRTTL); + const xml = await MakeRRCall({ + action: RRActions.InsertServiceVehicle, + body: { template: "InsertServiceVehicle", data: variables }, + redisHelpers, + socket, + jobid: JobData.id, + dealerConfig + }); - CreateRRLogEvent(socket, "DEBUG", `[RR] Get Vehicle Id via VIN`, { vin: JobData.v_vin }); + const ok = assertRrOkXml(xml, { apiName: "RR Insert Service Vehicle" }); + const normalized = extractRrResponseData(ok, { action: "InsertServiceVehicle" }); - const DMSVid = await GetVehicleId({ socket, redisHelpers, JobData }); - await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid, DMSVid, defaultRRTTL); + RRLogger(socket, "debug", "RR Insert Service Vehicle success", { + jobid: JobData?.id, + vehicleId: normalized?.VehicleId || normalized?.vehicleId + }); - let DMSVehCustomer; - if (!DMSVid?.newId) { - // existing vehicle, load details - const DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId }); - await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL); - - // Try to read the CURRENT owner (shape TBD per RR) - const owner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id?.assigningPartyId === "CURRENT"); - if (owner?.id?.value) { - DMSVehCustomer = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: owner.id.value }); - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.DMSVehCustomer, - DMSVehCustomer, - defaultRRTTL - ); - } - } - - // Search customers by job owner name (param names TBD per RR) - const DMSCustList = await SearchCustomerByName({ socket, redisHelpers, JobData }); - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.DMSCustList, - DMSCustList, - defaultRRTTL - ); - - // Emit choices: (VIN owner first if present) + search results - socket.emit("rr-select-customer", [ - ...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []), - ...(Array.isArray(DMSCustList) ? DMSCustList : []) - ]); + return normalized; } catch (error) { - CreateRRLogEvent(socket, "ERROR", `[RR] RRJobExport failed: ${error.message}`, { stack: error.stack }); + RRLogger(socket, "error", `RR Insert Service Vehicle failed: ${error.message}`, { jobid: JobData?.id }); + throw error; } } /** - * After client selects a customer (or requests create): - * - Read or create the customer - * - Insert vehicle if needed (or read existing) - * - StartWip -> TransBatchWip -> PostBatchWip -> Mark exported - * - Optionally insert service history - */ -async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jobid }) { - const { setSessionTransactionData, getSessionTransactionData } = redisHelpers; - - try { - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.selectedCustomerId, - selectedCustomerId, - defaultRRTTL - ); - - const JobData = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData); - const txEnvelope = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.txEnvelope); - const DMSVid = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid); - - // Ensure we have a customer to use - let DMSCust; - if (selectedCustomerId) { - DMSCust = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: selectedCustomerId }); - } else { - const createRes = await CreateCustomer({ socket, redisHelpers, JobData }); - DMSCust = { customerId: createRes?.data || createRes?.customerId || createRes?.id }; - } - await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSCust, DMSCust, defaultRRTTL); - - // Ensure the vehicle exists (ownership model TBD per RR) - let DMSVeh; - if (DMSVid?.newId) { - DMSVeh = await InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust }); - } else { - DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId }); - // TODO: If RR supports “UpdateVehicle” to change ownership, add it here. - } - await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL); - - // Start WIP header - const DMSTransHeader = await StartWip({ socket, redisHelpers, JobData, txEnvelope }); - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.DMSTransHeader, - DMSTransHeader, - defaultRRTTL - ); - - // Post lines - const DMSBatchTxn = await TransBatchWip({ socket, redisHelpers, JobData }); - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.DMSBatchTxn, - DMSBatchTxn, - defaultRRTTL - ); - - // Decide success from envelope (heuristic until exact spec confirmed) - if (String(DMSBatchTxn?.rtnCode || "0") === "0") { - const DmsBatchTxnPost = await PostBatchWip({ socket, redisHelpers, JobData }); - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.DmsBatchTxnPost, - DmsBatchTxnPost, - defaultRRTTL - ); - - if (String(DmsBatchTxnPost?.rtnCode || "0") === "0") { - await MarkJobExported({ socket, jobid: JobData.id, redisHelpers }); - - // Optional service history write (non-blocking) - try { - const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData }); - await setSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.DMSVehHistory, - DMSVehHistory, - defaultRRTTL - ); - } catch (e) { - CreateRRLogEvent(socket, "WARN", `[RR] ServiceVehicleHistory optional step failed: ${e.message}`); - } - - socket.emit("export-success", JobData.id); - } else { - await HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeader }); - } - } else { - await InsertFailedExportLog({ - socket, - JobData, - error: `RR DMSBatchTxn not successful: ${JSON.stringify(DMSBatchTxn)}` - }); - } - } catch (error) { - CreateRRLogEvent(socket, "ERROR", `[RR] RRSelectedCustomer failed: ${error.message}`, { stack: error.stack }); - const JobData = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.JobData - ); - if (JobData) await InsertFailedExportLog({ socket, JobData, error }); - } -} - -// ----------------------------------------------------------------------------- -// GraphQL job fetch -// ----------------------------------------------------------------------------- - -async function QueryJobData({ socket, jobid }) { - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); - const currentToken = - (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); - - const result = await client - .setHeaders({ Authorization: `Bearer ${currentToken}` }) - .request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobid }); - - return result.jobs_by_pk; -} - -// ----------------------------------------------------------------------------- -// RR API step stubs (wire to MakeRRCall). Replace request payloads once the -// exact RR/Rome schemas are confirmed from the PDFs. -// ----------------------------------------------------------------------------- - -async function GetVehicleId({ socket, redisHelpers, JobData }) { - return await MakeRRCall({ - ...RRActions.GetVehicleId, - requestPathParams: JobData.v_vin, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function ReadVehicleById({ socket, redisHelpers, JobData, vehicleId }) { - return await MakeRRCall({ - ...RRActions.ReadVehicle, - requestPathParams: vehicleId, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function ReadCustomerById({ socket, redisHelpers, JobData, customerId }) { - return await MakeRRCall({ - ...RRActions.ReadCustomer, - requestPathParams: customerId, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function SearchCustomerByName({ socket, redisHelpers, JobData }) { - // TODO: Confirm exact query param names from the RR search spec - const ownerNameParams = - JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== "" - ? [["lastName", JobData.ownr_co_nm]] // placeholder: business search - : [ - ["firstName", JobData.ownr_fn], - ["lastName", JobData.ownr_ln] - ]; - - return await MakeRRCall({ - ...RRActions.QueryCustomerByName, // ✅ use action defined in rr-helpers - requestSearchParams: ownerNameParams, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function CreateCustomer({ socket, redisHelpers, JobData }) { - // TODO: Replace with exact RR Customer Insert envelope & fields - const body = { - customerType: JobData.ownr_co_nm ? "BUSINESS" : "INDIVIDUAL" - }; - return await MakeRRCall({ - ...RRActions.CreateCustomer, - body, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function InsertVehicle({ socket, redisHelpers, JobData /*, txEnvelope, DMSVid, DMSCust*/ }) { - // TODO: Replace with exact RR Service Vehicle Insert mapping - const body = { - vin: JobData.v_vin - // owners, make/model, odometer, etc… - }; - return await MakeRRCall({ - ...RRActions.InsertVehicle, - body, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function StartWip({ socket, redisHelpers, JobData, txEnvelope }) { - // TODO: Replace body fields with RR WIP header schema - const body = { - acctgDate: moment().tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"), - desc: txEnvelope?.story || "", - docType: "10", - m13Flag: "0", - refer: JobData.ro_number, - srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder from CDK config; RR equivalent TBD - srcJrnl: txEnvelope?.journal, - userID: "BSMS" - }; - return await MakeRRCall({ - ...RRActions.StartWip, - body, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function TransBatchWip({ socket, redisHelpers, JobData }) { - const wips = await GenerateTransWips({ socket, redisHelpers, JobData }); - - // TODO: Ensure this body shape matches RR batch transaction schema - return await MakeRRCall({ - ...RRActions.TranBatchWip, - body: wips, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function PostBatchWip({ socket, redisHelpers, JobData }) { - const DMSTransHeader = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.DMSTransHeader - ); - - // TODO: Confirm final field names for “post” operation in RR - const body = { - opCode: "P", - transID: DMSTransHeader?.transID - }; - - return await MakeRRCall({ - ...RRActions.PostBatchWip, - body, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function QueryErrWip({ socket, redisHelpers, JobData }) { - const DMSTransHeader = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.DMSTransHeader - ); - return await MakeRRCall({ - ...RRActions.QueryErrorWip, - requestPathParams: DMSTransHeader?.transID, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function DeleteWip({ socket, redisHelpers, JobData }) { - const DMSTransHeader = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.DMSTransHeader - ); - - // TODO: Confirm if RR uses the same endpoint with opCode=D to delete/void - const body = { opCode: "D", transID: DMSTransHeader?.transID }; - - return await MakeRRCall({ - ...RRActions.PostBatchWip, - body, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) { - const txEnvelope = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.txEnvelope - ); - - // TODO: Replace with RR Service Vehicle History schema - const body = { - comments: txEnvelope?.story || "" - }; - return await MakeRRCall({ - ...RRActions.ServiceHistoryInsert, - body, - redisHelpers, - socket, - jobid: JobData.id - }); -} - -async function HandlePostingError({ socket, redisHelpers, JobData /*, DMSTransHeader*/ }) { - const DmsError = await QueryErrWip({ socket, redisHelpers, JobData }); - await DeleteWip({ socket, redisHelpers, JobData }); - - const errString = DmsError?.errMsg || JSON.stringify(DmsError); - errString?.split("|")?.forEach((e) => e && CreateRRLogEvent(socket, "ERROR", `[RR] Post error: ${e}`)); - await InsertFailedExportLog({ socket, JobData, error: errString }); -} - -/** - * Convert app allocations to RR WIP lines. - * Re-uses existing CalculateAllocations to keep parity with CDK/Fortellis. + * Full DMS export sequence for Reynolds & Reynolds. * - * TODO: Confirm exact RR posting model (accounts, control numbers, company ids, - * and whether amounts are signed or need separate debit/credit flags). + * 1. Ensure customer exists (insert or update) + * 2. Ensure vehicle exists/linked + * 3. Create or update repair order */ -async function GenerateTransWips({ socket, redisHelpers, JobData }) { - const allocations = await CalculateAllocations(socket, JobData.id, true); // true==verbose logging - const DMSTransHeader = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.DMSTransHeader - ); +async function ExportJobToRR({ socket, redisHelpers, JobData }) { + const jobid = JobData?.id; + const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; - const wips = []; - allocations.forEach((alloc) => { - if (alloc.sale.getAmount() > 0 && !alloc.tax) { - wips.push({ - acct: alloc.profitCenter.dms_acctnumber, - cntl: alloc.profitCenter.dms_control_override || JobData.ro_number, - postAmt: alloc.sale.multiply(-1).getAmount(), // sale is a credit in many GLs; confirm RR sign - transID: DMSTransHeader?.transID, - trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco // RR equivalent TBD - }); - } - if (alloc.cost.getAmount() > 0 && !alloc.tax) { - wips.push({ - acct: alloc.costCenter.dms_acctnumber, - cntl: alloc.costCenter.dms_control_override || JobData.ro_number, - postAmt: alloc.cost.getAmount(), - transID: DMSTransHeader?.transID, - trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco - }); - wips.push({ - acct: alloc.costCenter.dms_wip_acctnumber, - cntl: alloc.costCenter.dms_control_override || JobData.ro_number, - postAmt: alloc.cost.multiply(-1).getAmount(), - transID: DMSTransHeader?.transID, - trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco - }); - } - if (alloc.tax && alloc.sale.getAmount() > 0) { - wips.push({ - acct: alloc.profitCenter.dms_acctnumber, - cntl: alloc.profitCenter.dms_control_override || JobData.ro_number, - postAmt: alloc.sale.multiply(-1).getAmount(), - transID: DMSTransHeader?.transID, - trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco - }); - } - }); + RRLogger(socket, "info", "Starting RR job export", { jobid, bodyshopId }); - const txEnvelope = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.txEnvelope - ); - - txEnvelope?.payers?.forEach((payer) => { - wips.push({ - acct: payer.dms_acctnumber, - cntl: payer.controlnumber, - postAmt: Math.round(payer.amount * 100), // assuming cents (confirm RR units) - transID: DMSTransHeader?.transID, - trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco - }); - }); - - await redisHelpers.setSessionTransactionData( - socket.id, - getTransactionType(JobData.id), - RRCacheEnums.transWips, - wips, - defaultRRTTL - ); - return wips; -} - -// ----------------------------------------------------------------------------- -// DB logging mirrors Fortellis (status + export log) -// ----------------------------------------------------------------------------- - -async function MarkJobExported({ socket, jobid, redisHelpers }) { - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); - const currentToken = - (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); - - // Pull JobData from the session to get bodyshop info + default statuses - const JobData = - (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData)) || {}; - - const transWips = await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.transWips - ); - - return client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.MARK_JOB_EXPORTED, { - jobId: jobid, - job: { - status: JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*", - date_exported: new Date() - }, - log: { - bodyshopid: JobData?.bodyshop?.id, - jobid, - successful: true, - useremail: socket.user?.email, - metadata: transWips - }, - bill: { exported: true, exported_at: new Date() } - }); -} - -async function InsertFailedExportLog({ socket, JobData, error }) { try { - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); - const currentToken = - (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); + // Pull dealer-level overrides once (DB), env/platform secrets come from rr-helpers internally. + const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {}; - return await client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.INSERT_EXPORT_LOG, { - log: { - bodyshopid: JobData.bodyshop.id, - jobid: JobData.id, - successful: false, - message: typeof error === "string" ? error : JSON.stringify(error), - useremail: socket.user?.email - } + // + // STEP 1: CUSTOMER + // + RRLogger(socket, "info", "RR Step 1: Customer check/insert", { jobid }); + let rrCustomerResult; + + if (JobData?.rr_customer_id) { + rrCustomerResult = await RrCustomerUpdate({ + socket, + redisHelpers, + JobData, + existingCustomer: { CustomerId: JobData.rr_customer_id }, + patch: JobData.customer_patch + }); + } else { + rrCustomerResult = await RrCustomerInsert({ socket, redisHelpers, JobData }); + } + + // + // 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 }); - } catch (error2) { - CreateRRLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error2.message}`, { stack: error2.stack }); + + 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 + }; } } module.exports = { - RRJobExport, - RRSelectedCustomer + ExportJobToRR, + RrServiceVehicleInsert }; diff --git a/server/rr/rr-logger.js b/server/rr/rr-logger.js index 2d6a827be..a6a57e389 100644 --- a/server/rr/rr-logger.js +++ b/server/rr/rr-logger.js @@ -1,49 +1,50 @@ -// ----------------------------------------------------------------------------- -// Thin wrapper around the shared logger + a socket emitter, -// mirroring the Fortellis logger shape for parity. -// -// Emits: "rr-log-event" { level, message, txnDetails } -// -// NOTE: Keep this lightweight and side-effect free—downstream flows rely on it -// for both developer troubleshooting and UI progress messages. -// ----------------------------------------------------------------------------- +/** + * @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. + */ const logger = require("../utils/logger"); /** - * Emit a structured RR log event to both the central logger and (if present) - * over the current socket connection for real-time UI visibility. + * Create a structured RR log event. * - * @param {Object} socket - A Socket.IO socket OR an Express req (when reused in REST). - * Expected fields if present: - * - socket.user?.email (used as the "actor" for audit trails) - * - socket.emit(...) method in live socket contexts - * @param {"SILLY"|"DEBUG"|"INFO"|"WARN"|"ERROR"} level - Log severity - * @param {string} message - Human readable message - * @param {Object} [txnDetails] - Optional structured metadata (ids, payload snippets, timings) + * @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.) */ -function CreateRRLogEvent(socket, level, message, txnDetails) { - const userEmail = socket?.user?.email || "unknown"; - - // 1) Centralized app logger (goes to your sinks: console, Datadog, etc.) - // Namespace: "rr-log-event" to keep provider logs grouped. +const RRLogger = (socket, level = "info", message, txnDetails = {}) => { try { - logger.log("rr-log-event", level, userEmail, null, { + // 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"; + + 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 }); - } catch { - // Best-effort: never throw from logging - } - // 2) Realtime push to the UI if we're in a socket context - try { - if (typeof socket?.emit === "function") { - socket.emit("rr-log-event", { level, 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 { - // Best-effort: never throw from logging + } catch (err) { + // As a fallback, log directly to console + console.error("RRLogger internal error:", err); + console.error("Original message:", message, txnDetails); } -} +}; -module.exports = CreateRRLogEvent; +module.exports = RRLogger; diff --git a/server/rr/rr-lookup.js b/server/rr/rr-lookup.js index 3b4734aa5..4070867ae 100644 --- a/server/rr/rr-lookup.js +++ b/server/rr/rr-lookup.js @@ -1,88 +1,143 @@ -// ----------------------------------------------------------------------------- -// Reynolds & Reynolds (RR) lookup helpers. -// Uses MakeRRCall + RRActions from rr-helpers, and shared response validation -// from rr-error. -// -// What’s still missing / to confirm against the Rome/RR PDFs: -// - Final query param names and value formats for the “combined search” -// (customer + vehicle), advisors directory, and parts lookup. -// - Any RR-required headers (dealer/site/location ids) — add in rr-helpers -// via the MakeRRCall default headers if needed. -// - Final success envelope checks in assertRrOk. -// ----------------------------------------------------------------------------- +/** + * @file rr-lookup.js + * @description Reynolds & Reynolds lookup operations + * (Combined Search, Get Advisors, Get Parts) via SOAP/XML templates. + */ -const { MakeRRCall, RRActions } = require("./rr-helpers"); -const { assertRrOk } = require("./rr-error"); +const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers"); +const { assertRrOkXml, extractRrResponseData } = require("./rr-error"); +const { mapCombinedSearchVars, mapGetAdvisorsVars, mapGetPartsVars } = require("./rr-mappers"); +const RRLogger = require("./rr-logger"); /** - * RR Combined Search (Customer + Vehicle). + * Combined Search + * Maps to "Search Customer Service Vehicle Combined" spec (Rome) * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - * @param {string|number} deps.jobid - for correlation/logging only - * @param {Array<[string,string]>} [deps.params=[]] - * Example: [["vin", "1HGBH41JXMN109186"], ["lastName","DOE"]] - * @returns {Promise} RR response (envelope TBD) + * @param {object} options + * @param {object} options.socket - Socket or Express req (used for auth + bodyshopId) + * @param {object} options.redisHelpers - (unused, kept for parity) + * @param {string} options.jobid - Job reference for correlation + * @param {Array<[string, string]>} [options.params] - e.g. [["VIN","1HG..."],["LastName","DOE"]] */ async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) { - const data = await MakeRRCall({ - ...RRActions.CombinedSearch, // GET /search/v1/customer-vehicle - requestSearchParams: params, - type: "get", - redisHelpers, - socket, - jobid - }); + try { + RRLogger(socket, "info", "Starting RR Combined Search", { jobid, params }); - // allowEmpty=true because searches may legitimately return 0 rows - return assertRrOk(data, { apiName: "RR Combined Search", allowEmpty: true }); + 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, + socket, + jobid + }); + + // Validate + normalize + const ok = assertRrOkXml(xml, { apiName: "RR Combined Search", allowEmpty: true }); + const normalized = extractRrResponseData(ok, { action: "CombinedSearch" }); + + RRLogger(socket, "debug", "RR Combined Search complete", { + jobid, + count: Array.isArray(normalized) ? normalized.length : 0 + }); + return normalized; + } catch (error) { + RRLogger(socket, "error", `RR Combined Search failed: ${error.message}`, { jobid }); + throw error; + } } /** - * RR Get Advisors. + * Get Advisors + * Maps to "Get Advisors Specification" (Rome) * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - * @param {string|number} deps.jobid - * @param {Array<[string,string]>} [deps.params=[]] - * Example: [["active","true"]] - * @returns {Promise} RR response (envelope TBD) + * @param {object} options + * @param {object} options.socket + * @param {object} options.redisHelpers + * @param {string} options.jobid + * @param {Array<[string, string]>} [options.params] */ async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) { - const data = await MakeRRCall({ - ...RRActions.GetAdvisors, // GET /advisors/v1 - requestSearchParams: params, - type: "get", - redisHelpers, - socket, - jobid - }); - return assertRrOk(data, { apiName: "RR Get Advisors", allowEmpty: true }); + try { + RRLogger(socket, "info", "Starting RR Get Advisors", { jobid, params }); + + 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, + socket, + jobid + }); + + const ok = assertRrOkXml(xml, { apiName: "RR Get Advisors", allowEmpty: true }); + const normalized = extractRrResponseData(ok, { action: "GetAdvisors" }); + + RRLogger(socket, "debug", "RR Get Advisors complete", { + jobid, + count: Array.isArray(normalized) ? normalized.length : 0 + }); + return normalized; + } catch (error) { + RRLogger(socket, "error", `RR Get Advisors failed: ${error.message}`, { jobid }); + throw error; + } } /** - * RR Get Parts. + * Get Parts + * Maps to "Get Part Specification" (Rome) * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - * @param {string|number} deps.jobid - * @param {Array<[string,string]>} [deps.params=[]] - * Example: [["sku","ABC123"], ["page","1"], ["pageSize","50"]] - * @returns {Promise} RR response (envelope TBD) + * @param {object} options + * @param {object} options.socket + * @param {object} options.redisHelpers + * @param {string} options.jobid + * @param {Array<[string, string]>} [options.params] */ async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) { - const data = await MakeRRCall({ - ...RRActions.GetParts, // GET /parts/v1 - requestSearchParams: params, - type: "get", - redisHelpers, - socket, - jobid - }); - return assertRrOk(data, { apiName: "RR Get Parts", allowEmpty: true }); + try { + RRLogger(socket, "info", "Starting RR Get Parts", { jobid, params }); + + 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, + socket, + jobid + }); + + const ok = assertRrOkXml(xml, { apiName: "RR Get Parts", allowEmpty: true }); + const normalized = extractRrResponseData(ok, { action: "GetParts" }); + + RRLogger(socket, "debug", "RR Get Parts complete", { + jobid, + count: Array.isArray(normalized) ? normalized.length : 0 + }); + return normalized; + } catch (error) { + RRLogger(socket, "error", `RR Get Parts failed: ${error.message}`, { jobid }); + throw error; + } } -module.exports = { RrCombinedSearch, RrGetAdvisors, RrGetParts }; +module.exports = { + RrCombinedSearch, + RrGetAdvisors, + RrGetParts +}; diff --git a/server/rr/rr-mappers.js b/server/rr/rr-mappers.js index 843186fae..7bde0fb96 100644 --- a/server/rr/rr-mappers.js +++ b/server/rr/rr-mappers.js @@ -1,333 +1,424 @@ +// server/rr/rr-mappers.js // ----------------------------------------------------------------------------- -// Centralized mapping & normalization for Reynolds & Reynolds (RR) +// 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. // -// This is scaffolding aligned to the Rome RR PDFs you provided: +// 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. // -// - Rome Customer Insert Specification 1.2.pdf -// - Rome Customer Update Specification 1.2.pdf -// - Rome Insert Service Vehicle Interface Specification.pdf -// - Rome Create Body Shop Management Repair Order Interface Specification.pdf -// - Rome Update Body Shop Management Repair Order Interface Specification.pdf -// - Rome Get Advisors Specification.pdf -// - Rome Get Part Specification.pdf -// - Rome Search Customer Service Vehicle Combined Specification.pdf +// Templates these map into (variable names must match): +// - InsertCustomer.xml: +// - UpdateCustomer.xml: +// - InsertServiceVehicle.xml: +// - CreateRepairOrder.xml: +// - UpdateRepairOrder.xml: // -// Replace all TODO:RR with exact element/attribute names and enumerations from -// the PDFs above. The shapes here are intentionally close to other providers -// so you can reuse upstream plumbing without surprises. +// All map* functions below return a plain object shaped for Mustache rendering. // ----------------------------------------------------------------------------- const _ = require("lodash"); -// Keep this consistent with other providers -const replaceSpecialRegex = /[^a-zA-Z0-9 .,\n #]+/g; +// Keep this consistent with other providers (sanitize strings for XML) +const REPLACE_SPECIAL = /[^a-zA-Z0-9 .,\n#\-()/]+/g; -// ---------- Generic helpers -------------------------------------------------- - -function sanitize(value) { - if (value === null || value === undefined) return value; - return String(value).replace(replaceSpecialRegex, "").trim(); +function sanitize(v) { + if (v === null || v === undefined) return null; + return String(v).replace(REPLACE_SPECIAL, "").trim(); } -function asStringOrNull(value) { - const s = sanitize(value); - return s && s.length > 0 ? s : null; -} - -function toUpperOrNull(value) { - const s = asStringOrNull(value); +function upper(v) { + const s = sanitize(v); return s ? s.toUpperCase() : null; } -/** - * Normalize postal/zip minimally; keep simple and provider-agnostic for now. - * TODO:RR — If RR enforces specific postal formatting by country, implement it here. - */ -function normalizePostal(raw) { - if (!raw) return null; - return String(raw).trim(); +function asNumberOrNull(v) { + if (v === null || v === undefined || v === "") return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; } +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); +} + +/** + * Compose the dealer section used by every template. + * We prefer dealer-level rr_configuration first; fallback to env. + */ +function buildDealerVars(dealerCfg = {}) { + 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 + }; +} + +/* ------------------------------- Phones/Emails ------------------------------- */ + function mapPhones({ ph1, ph2, mobile }) { - // TODO:RR — Replace "HOME|WORK|MOBILE" with RR's phone type codes + any flags (preferred, sms ok). + // TODO (spec): adjust PhoneType enumerations if RR requires strict codes. const out = []; - if (ph1) out.push({ number: sanitize(ph1), type: "HOME" }); - if (ph2) out.push({ number: sanitize(ph2), type: "WORK" }); - if (mobile) out.push({ number: sanitize(mobile), type: "MOBILE" }); + 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 }) { - // TODO:RR — If RR supports multiple with flags, expand (preferred, statement, etc.). if (!email) return []; - return [{ address: sanitize(email), type: "PERSONAL" }]; + // TODO (spec): include EmailType (e.g., PERSONAL/WORK) if RR mandates it. + return [{ EmailAddress: sanitize(email), EmailType: "PERSONAL" }]; } -// ---------- Address/Contact from Rome JobData -------------------------------- +/* -------------------------------- Addresses -------------------------------- */ function mapPostalAddressFromJob(job) { - // Rome job-level owner fields (aligned with other providers) - // TODO:RR — Confirm exact element names (e.g., AddressLine vs Street1, State vs Province). - return { - addressLine1: asStringOrNull(job.ownr_addr1), - addressLine2: asStringOrNull(job.ownr_addr2), - city: asStringOrNull(job.ownr_city), - state: asStringOrNull(job.ownr_st || job.ownr_state), - province: asStringOrNull(job.ownr_province), // keep both for CA use-cases if distinct in RR - postalCode: normalizePostal(job.ownr_zip), - country: asStringOrNull(job.ownr_ctry) || "USA" - }; -} - -function mapPhonesFromJob(job) { - return mapPhones({ - ph1: job.ownr_ph1, - ph2: job.ownr_ph2, - mobile: job.ownr_mobile - }); -} - -function mapEmailsFromJob(job) { - return mapEmails({ email: job.ownr_ea }); -} - -// ---------- Customer mappers -------------------------------------------------- - -/** - * Customer Insert - * Matches call-site: const body = mapCustomerInsert(JobData); - * - * TODO:RR — Replace envelope and field names with exact RR schema: - * e.g., CustomerInsertRq.Customer (Organization vs Person), name blocks, ids, codes, etc. - */ -function mapCustomerInsert(job) { - const isCompany = Boolean(job?.ownr_co_nm && job.ownr_co_nm.trim() !== ""); - - return { - // Example envelope — rename to match the PDF (e.g., "CustomerInsertRq") - CustomerInsertRq: { - // High-level type — confirm the exact enum RR expects. - customerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL", - - // Name block — ensure RR's exact element names and casing. - customerName: { - companyName: isCompany ? toUpperOrNull(job.ownr_co_nm) : null, - firstName: isCompany ? null : toUpperOrNull(job.ownr_fn), - lastName: isCompany ? null : toUpperOrNull(job.ownr_ln) - }, - - // Mailing address - postalAddress: mapPostalAddressFromJob(job), - - // Contacts - contactMethods: { - phones: mapPhonesFromJob(job), - emailAddresses: mapEmailsFromJob(job) - } - - // TODO:RR — Common optional fields: tax/resale codes, pricing flags, AR terms, source codes, etc. - // taxCode: null, - // termsCode: null, - // marketingOptIn: null, - // dealerSpecificFields: [] + return [ + { + AddressLine1: sanitize(job.ownr_addr1), + AddressLine2: sanitize(job.ownr_addr2), + City: upper(job.ownr_city), + State: upper(job.ownr_st || job.ownr_state), + PostalCode: normalizePostal(job.ownr_zip), + Country: upper(job.ownr_ctry) || "USA" } + ].filter((addr) => Object.values(addr).some(Boolean)); +} + +/* --------------------------------- Customer -------------------------------- */ + +function mapCustomerInsert(job, dealerCfg = {}) { + const dealer = buildDealerVars(dealerCfg); + const isCompany = Boolean(job?.ownr_co_nm && String(job.ownr_co_nm).trim() !== ""); + + return { + ...dealer, + // Envelope metadata (optional) + RequestId: job?.id || null, + Environment: process.env.NODE_ENV || "development", + + // 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", + + Addresses: mapPostalAddressFromJob(job), + Phones: mapPhones({ ph1: job.ownr_ph1, ph2: job.ownr_ph2, mobile: job.ownr_mobile }), + Emails: mapEmails({ email: job.ownr_ea }), + + // Optional blocks (keep null unless you truly have values) + DriverLicense: null, // { LicenseNumber, LicenseState, ExpirationDate } + Insurance: null, // { CompanyName, PolicyNumber, ExpirationDate } + Notes: null // { Note } }; } -/** - * Customer Update - * Matches call-site: const body = mapCustomerUpdate(existingCustomer, patch); - * - * - existingCustomer: RR's current representation (from Read/Query) - * - patch: a thin delta from UI/Job selection - * - * TODO:RR — Swap envelope/fields for RR's specific Update schema. - */ -function mapCustomerUpdate(existingCustomer, patch = {}) { +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?.id || merged?.CustomerId || merged?.customer?.id || null; + const id = + merged?.CustomerId || + merged?.customerId || + merged?.id || + merged?.customer?.id || + patch?.CustomerId || + patch?.customerId || + null; - const isCompany = Boolean(merged?.customerName?.companyName) || Boolean(merged?.companyName); + // Derive company vs individual + const isCompany = Boolean(merged?.CompanyName || merged?.customerName?.companyName); - const normalizedName = { - companyName: asStringOrNull(merged?.customerName?.companyName) || asStringOrNull(merged?.companyName) || null, - firstName: isCompany - ? null - : asStringOrNull(merged?.customerName?.firstName) || asStringOrNull(merged?.firstName) || null, - lastName: isCompany - ? null - : asStringOrNull(merged?.customerName?.lastName) || asStringOrNull(merged?.lastName) || null + 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 normalizedAddress = { - addressLine1: asStringOrNull(merged?.postalAddress?.addressLine1) || asStringOrNull(merged?.addressLine1) || null, - addressLine2: asStringOrNull(merged?.postalAddress?.addressLine2) || asStringOrNull(merged?.addressLine2) || null, - city: asStringOrNull(merged?.postalAddress?.city) || asStringOrNull(merged?.city) || null, - state: - asStringOrNull(merged?.postalAddress?.state) || - asStringOrNull(merged?.state) || - asStringOrNull(merged?.stateOrProvince) || - asStringOrNull(merged?.province) || - null, - province: asStringOrNull(merged?.postalAddress?.province) || asStringOrNull(merged?.province) || null, - postalCode: normalizePostal(merged?.postalAddress?.postalCode || merged?.postalCode), - country: asStringOrNull(merged?.postalAddress?.country) || asStringOrNull(merged?.country) || "USA" - }; + // 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" + } + ] + : null); - // Contacts (reuse existing unless patch supplied a new structure upstream) - const normalizedPhones = merged?.contactMethods?.phones || merged?.phones || []; - - const normalizedEmails = merged?.contactMethods?.emailAddresses || merged?.emailAddresses || []; + // Phones & Emails + const phones = merged?.Phones || merged?.contactMethods?.phones || []; + const emails = merged?.Emails || merged?.contactMethods?.emailAddresses || []; return { - // Example envelope — rename to match the PDF (e.g., "CustomerUpdateRq") - CustomerUpdateRq: { - customerId: id, - customerType: normalizedName.companyName ? "ORGANIZATION" : "INDIVIDUAL", - customerName: normalizedName, - postalAddress: normalizedAddress, - contactMethods: { - phones: normalizedPhones, - emailAddresses: normalizedEmails - } - // TODO:RR — include fields that RR requires for update (version, hash, lastUpdatedTs, etc.) - } + ...dealer, + RequestId: merged?.RequestId || null, + Environment: process.env.NODE_ENV || "development", + + CustomerId: id, + CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL", + ...nameBlock, + ActiveFlag: merged?.ActiveFlag || "Y", + + Addresses: addr, + Phones: phones.map((p) => ({ PhoneNumber: sanitize(p.PhoneNumber || p.number), PhoneType: p.PhoneType || p.type })), + Emails: emails.map((e) => ({ + EmailAddress: sanitize(e.EmailAddress || e.address), + EmailType: e.EmailType || e.type || "PERSONAL" + })), + + // Optional + DriverLicense: merged?.DriverLicense || null, + Insurance: merged?.Insurance || null, + Notes: merged?.Notes || null }; } -// ---------- Vehicle mappers --------------------------------------------------- +/* --------------------------------- Vehicle --------------------------------- */ + +function mapVehicleInsertFromJob(job, dealerCfg = {}, opts = {}) { + // opts: { customerId } + const dealer = buildDealerVars(dealerCfg); -/** - * Vehicle Insert from JobData - * Called (or call-able) by InsertVehicle. - * - * TODO:RR — Replace envelope/field names with the exact RR vehicle schema. - */ -function mapVehicleInsertFromJob(job, txEnvelope = {}) { return { - ServiceVehicleInsertRq: { - vin: asStringOrNull(job.v_vin), - // Year/make/model — validate source fields vs RR required fields - year: job.v_model_yr || null, - make: toUpperOrNull(txEnvelope.dms_make || job.v_make), - model: toUpperOrNull(txEnvelope.dms_model || job.v_model), - // Mileage/odometer — confirm units/element names - odometer: txEnvelope.kmout || txEnvelope.miout || null, - // Plate — uppercase and sanitize - licensePlate: job.plate_no ? toUpperOrNull(job.plate_no) : null - // TODO:RR — owner/customer link, color, trim, fuel, DRIVETRAIN, etc. - } + ...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 Order mappers --------------------------------------------- +/* ------------------------------- 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) + }; -/** - * Create Repair Order - * Matches call-site: mapRepairOrderCreate({ JobData, txEnvelope }) - * - * TODO:RR — Use the exact request envelope/fields for Create RO from the PDF: - * Header (customer/vehicle/ro-no/dates), lines/labors/parts/taxes, totals. - */ -function mapRepairOrderCreate({ JobData, txEnvelope }) { return { - RepairOrderCreateRq: { - // Header - referenceNumber: asStringOrNull(JobData.ro_number), - customerId: JobData?.customer?.id || null, // supply from previous step or selection - vehicleId: JobData?.vehicle?.id || null, // supply from previous step - openedAt: JobData?.actual_in || null, // confirm expected datetime format - promisedAt: JobData?.promise_date || null, - advisorId: txEnvelope?.advisorId || null, + ...dealer, + RequestId: job?.id || null, + Environment: process.env.NODE_ENV || "development", - // Lines (placeholder) - lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [], + 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", - // Taxes (placeholder) - taxes: mapTaxes(JobData), + CustomerId: customerVars.CustomerId, + CustomerName: customerVars.CustomerName, + PhoneNumber: customerVars.PhoneNumber, + EmailAddress: customerVars.EmailAddress, - // Payments (placeholder) - payments: mapPayments(txEnvelope) + VIN: vehicleVars.VIN, + LicensePlate: vehicleVars.LicensePlate, + Year: vehicleVars.Year, + Make: vehicleVars.Make, + Model: vehicleVars.Model, + Odometer: vehicleVars.Odometer, + Color: vehicleVars.Color, - // TODO:RR — add required flags, shop supplies, labor matrix, discounts, etc. - } + JobLines: (job?.joblines || txEnvelope?.lines || []).map((ln, idx) => mapJobLineToRRLine(ln, idx + 1)), + + Totals: txEnvelope?.totals + ? { + 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) + } + : null, + + Insurance: txEnvelope?.insurance + ? { + CompanyName: upper(txEnvelope.insurance.company), + ClaimNumber: sanitize(txEnvelope.insurance.claim), + AdjusterName: upper(txEnvelope.insurance.adjuster), + AdjusterPhone: sanitize(txEnvelope.insurance.phone) + } + : null, + + Notes: txEnvelope?.story ? { Note: sanitize(txEnvelope.story) } : null }; } -/** - * Update Repair Order - * Matches call-site: mapRepairOrderUpdate({ JobData, txEnvelope }) - * - * TODO:RR — RR may want delta format (change set) vs full replace. - * Add versioning/concurrency tokens if specified in the PDF. - */ -function mapRepairOrderUpdate({ JobData, txEnvelope }) { - return { - RepairOrderUpdateRq: { - repairOrderId: JobData?.id || txEnvelope?.repairOrderId || null, - referenceNumber: asStringOrNull(JobData?.ro_number), +function mapRepairOrderChangeFromJob(current, delta = {}, dealerCfg = {}) { + // current: existing RO (our cached shape) + // delta: patch object describing header fields and line changes + const dealer = buildDealerVars(dealerCfg); - // Example: only pass changed lines (you may need your diff before mapping) - // For scaffolding, we pass what we have; replace with proper deltas later. - lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [], - - taxes: mapTaxes(JobData), - payments: mapPayments(txEnvelope) - - // TODO:RR — include RO status transitions, close/invoice flags, etc. - } - }; -} - -/* ----- Line/Tax/Payment helpers (placeholders) ----------------------------- */ - -function mapJobLineToRRLine(line) { - // TODO:RR — Replace with RR RO line schema (labor/part/misc line types, op-code, flags). - return { - lineType: line?.type || "LABOR", // e.g., LABOR | PART | MISC - sequence: line?.sequence || null, - opCode: line?.opCode || line?.opcode || null, - description: asStringOrNull(line?.description || line?.descr), - quantity: line?.part_qty || line?.qty || 1, - unitPrice: line?.price || line?.unitPrice || null, - extendedAmount: line?.ext || null - }; -} - -function mapTaxes(job) { - // TODO:RR — Implement per RR tax structure (rates by jurisdiction, taxable flags, rounding rules). - // Return empty array as scaffolding. - return []; -} - -function mapPayments(txEnvelope = {}) { - // TODO:RR — Implement per RR payment shape (payer types, amounts, reference ids) - // For Fortellis/CDK parity, txEnvelope.payers often exists; adapt to RR fields. - if (!Array.isArray(txEnvelope?.payers)) return []; - return txEnvelope.payers.map((p) => ({ - payerType: p.type || "INSURER", // e.g., CUSTOMER | INSURER | WARRANTY - reference: asStringOrNull(p.controlnumber || p.ref), - amount: p.amount != null ? Number(p.amount) : null + 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 + })); + + 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; + + 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; + + const notes = + Array.isArray(delta?.notes) && delta.notes.length + ? { Items: delta.notes.map((n) => sanitize(n)).filter(Boolean) } + : null; + + return { + ...dealer, + RequestId: delta?.RequestId || current?.RequestId || null, + Environment: process.env.NODE_ENV || "development", + + 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, + + // Optional customer/vehicle patches + Customer: delta?.Customer || null, + Vehicle: delta?.Vehicle || null, + + // Line changes + AddedJobLines: added.length ? added : null, + UpdatedJobLines: updated.length ? updated : null, + RemovedJobLines: removed.length ? removed : null, + + Totals: totals, + Insurance: insurance, + Notes: notes + }; } -// ---------- Exports ----------------------------------------------------------- +/* ------------------------------- 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 = { - // Used by current call-sites: + // Customer mapCustomerInsert, mapCustomerUpdate, - mapRepairOrderCreate, - mapRepairOrderUpdate, - // Extra scaffolds you’ll likely use soon: + // Vehicle mapVehicleInsertFromJob, - mapJobLineToRRLine, - mapTaxes, - mapPayments, - // Low-level utils (handy in tests) + // Repair orders + mapRepairOrderAddFromJob, + mapRepairOrderChangeFromJob, + mapJobLineToRRLine, + + // shared utils (handy in tests) + buildDealerVars, _sanitize: sanitize, - _normalizePostal: normalizePostal, - _toUpperOrNull: toUpperOrNull + _upper: upper, + _normalizePostal: normalizePostal }; diff --git a/server/rr/rr-repair-orders.js b/server/rr/rr-repair-orders.js index 273dc0784..2d33f53eb 100644 --- a/server/rr/rr-repair-orders.js +++ b/server/rr/rr-repair-orders.js @@ -1,76 +1,144 @@ -// server/rr/rr-repair-orders.js -// ----------------------------------------------------------------------------- -// RR Repair Order create/update wired through MakeRRCall. -// Mapping comes from rr-mappers.js and response validation via rr-error.js. -// -// What’s still missing (complete when you wire to the PDFs): -// - Final RR request envelopes & field names in rr-mappers.js -// (Create: “RepairOrderAddRq”, Update: “RepairOrderChgRq”, etc.) -// - Definitive success/error envelope checks in rr-error.js (assertRrOk) -// - Any RR-required headers (dealer/tenant/site/location ids) in rr-helpers -// - If RR requires path params for update (e.g., /repair-orders/{id}), -// either add requestPathParams here or move id into RRActions.UpdateRepairOrder -// ----------------------------------------------------------------------------- +/** + * @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. + */ const { MakeRRCall, RRActions } = require("./rr-helpers"); const { assertRrOk } = require("./rr-error"); -const { mapRepairOrderAddFromJob, mapRepairOrderChangeFromJob } = require("./rr-mappers"); +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"); /** - * Create a Repair Order in RR. + * Fetch rr_configuration for the current bodyshop directly from DB. + * Dealer-specific configuration is mandatory for RR operations. + */ +async function getDealerConfigFromDB(bodyshopId, logger) { + try { + const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId }); + const config = result?.bodyshops_by_pk?.rr_configuration || null; + + if (!config) { + throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`); + } + + logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config); + return config; + } catch (error) { + logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, { + bodyshopId, + message: error.message, + stack: error.stack + }); + throw error; + } +} + +/** + * CREATE REPAIR ORDER + * Based on "Rome Create Body Shop Management Repair Order Specification" * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - * @param {Object} deps.JobData - Rome job (used for mapping) - * @param {Object} deps.txEnvelope - Posting/GL context if needed in mapping - * @returns {Promise} - RR response (envelope TBD) + * @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.) */ async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { - // Map JobData (+ optional txEnvelope) -> RR "Repair Order Add" request body - const body = mapRepairOrderAddFromJob({ ...JobData, txEnvelope }); + const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; + const logger = socket?.logger || console; - const data = await MakeRRCall({ - ...RRActions.CreateRepairOrder, // POST /repair-orders/v1 - body, - redisHelpers, - socket, - jobid: JobData?.id - }); + try { + RRLogger(socket, "info", "RR Create Repair Order started", { + jobid: JobData?.id, + bodyshopId + }); - // TODO: Update assertRrOk once RR’s success envelope is finalized - return assertRrOk(data, { apiName: "RR Create Repair Order" }); + const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger); + + // 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, + socket, + jobid: JobData.id + }); + + const response = assertRrOk(data, { apiName: "RR Create Repair Order" }); + + RRLogger(socket, "debug", "RR Create Repair Order success", { + jobid: JobData?.id, + dealer: dealerConfig?.dealer_code || dealerConfig?.dealerCode + }); + + return response; + } catch (error) { + RRLogger(socket, "error", `RR Create Repair Order failed: ${error.message}`, { + jobid: JobData?.id + }); + throw error; + } } /** - * Update a Repair Order in RR. + * UPDATE REPAIR ORDER + * Based on "Rome Update Body Shop Management Repair Order Specification" * - * NOTE: If RR requires the repair order id in the URL (PUT /repair-orders/{id}), - * pass it via requestPathParams here once you have it: - * requestPathParams: repairOrderId - * and ensure RRActions.UpdateRepairOrder.url ends with a trailing slash. - * - * @param {Object} deps - * @param {Socket|ExpressRequest} deps.socket - * @param {Object} deps.redisHelpers - * @param {Object} deps.JobData - Rome job (used for mapping) - * @param {Object} deps.txEnvelope - Posting/GL context if needed in mapping - * @param {string|number} [deps.repairOrderId] - If RR expects a path param - * @returns {Promise} - RR response (envelope TBD) + * @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, repairOrderId }) { - const body = mapRepairOrderChangeFromJob({ ...JobData, txEnvelope }); +async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { + const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid; + const logger = socket?.logger || console; - const data = await MakeRRCall({ - ...RRActions.UpdateRepairOrder, // PUT /repair-orders/v1 (or /v1/{id}) - ...(repairOrderId ? { requestPathParams: String(repairOrderId) } : {}), - body, - redisHelpers, - socket, - jobid: JobData?.id - }); + try { + RRLogger(socket, "info", "RR Update Repair Order started", { + jobid: JobData?.id, + bodyshopId, + rr_ro_id: JobData?.rr_ro_id + }); - return assertRrOk(data, { apiName: "RR Update Repair Order" }); + const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger); + + // Build Mustache variables for server/rr/xml-templates/UpdateRepairOrder.xml + const vars = mapRepairOrderUpdate({ JobData, txEnvelope, dealerConfig }); + + const data = await MakeRRCall({ + action: RRActions.UpdateRepairOrder, // resolves SOAPAction+URL + body: { template: "UpdateRepairOrder", data: vars }, // render XML template + redisHelpers, + socket, + jobid: JobData.id + }); + + const response = assertRrOk(data, { apiName: "RR Update Repair Order" }); + + RRLogger(socket, "debug", "RR Update Repair Order success", { + jobid: JobData?.id, + rr_ro_id: JobData?.rr_ro_id + }); + + return response; + } catch (error) { + RRLogger(socket, "error", `RR Update Repair Order failed: ${error.message}`, { + jobid: JobData?.id, + rr_ro_id: JobData?.rr_ro_id + }); + throw error; + } } -module.exports = { CreateRepairOrder, UpdateRepairOrder }; +module.exports = { + CreateRepairOrder, + UpdateRepairOrder, + getDealerConfigFromDB +}; diff --git a/server/rr/rr-test.js b/server/rr/rr-test.js new file mode 100644 index 000000000..8839389b6 --- /dev/null +++ b/server/rr/rr-test.js @@ -0,0 +1,127 @@ +// node server/rr/rr-test.js + +/** + * @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 + */ + +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 { 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 +}; + +const redisHelpers = { + setSessionData: async () => {}, + getSessionData: async () => {}, + setSessionTransactionData: async () => {}, + getSessionTransactionData: async () => {}, + clearSessionTransactionData: async () => {} +}; + +(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" + } + }; + + // 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" + }; + + // 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); + + // 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; + } + + console.log(`--- Sending SOAP Request: ${RRActions.GetAdvisors.action} ---\n`); + + 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 + }); + + 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); + } +})(); diff --git a/server/rr/rr-wsdl.js b/server/rr/rr-wsdl.js new file mode 100644 index 000000000..8be45d512 --- /dev/null +++ b/server/rr/rr-wsdl.js @@ -0,0 +1,97 @@ +/** + * 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. + */ + +const { XMLParser } = require("fast-xml-parser"); +const logger = require("../utils/logger"); +const { MakeRRCall, resolveRRConfig, renderXmlTemplate } = require("./rr-helpers"); + +// 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" } +}; + +/** + * Optionally render just the body XML for a given action (no SOAP envelope). + * Mostly useful for diagnostics/tests. + */ +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); +} + +/** + * 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 + */ +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}`); + + const templateName = entry.file.replace(/\.xml$/i, ""); + const dealerConfig = await resolveRRConfig(socket); + + // Let MakeRRCall render + envelope + post + const xml = await MakeRRCall({ + action, + body: { template: templateName, data: variables }, + socket, + dealerConfig, + retries + }); + + if (raw) return xml; + + try { + const parser = new XMLParser({ ignoreAttributes: false }); + const parsed = parser.parse(xml); + + // 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; + } +} + +module.exports = { + sendRRRequest, + buildRRXml, + RR_ACTION_MAP +}; diff --git a/server/rr/xml-templates/CombinedSearch.xml b/server/rr/xml-templates/CombinedSearch.xml new file mode 100644 index 000000000..60bf76df8 --- /dev/null +++ b/server/rr/xml-templates/CombinedSearch.xml @@ -0,0 +1,73 @@ + + + {{#RequestId}} + {{RequestId}} + {{/RequestId}} + {{#Environment}} + {{Environment}} + {{/Environment}} + + + {{DealerCode}} + {{#DealerName}} + {{DealerName}} + {{/DealerName}} + {{#DealerNumber}} + {{DealerNumber}} + {{/DealerNumber}} + {{#StoreNumber}} + {{StoreNumber}} + {{/StoreNumber}} + {{#BranchNumber}} + {{BranchNumber}} + {{/BranchNumber}} + + + + {{#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}} + + diff --git a/server/rr/xml-templates/CreateRepairOrder.xml b/server/rr/xml-templates/CreateRepairOrder.xml new file mode 100644 index 000000000..b0c2b9a7b --- /dev/null +++ b/server/rr/xml-templates/CreateRepairOrder.xml @@ -0,0 +1,158 @@ + + + {{#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}} + + {{#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}} + + {{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}} + + diff --git a/server/rr/xml-templates/GetAdvisors.xml b/server/rr/xml-templates/GetAdvisors.xml new file mode 100644 index 000000000..c6cfa61da --- /dev/null +++ b/server/rr/xml-templates/GetAdvisors.xml @@ -0,0 +1,34 @@ + + + {{#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}} + diff --git a/server/rr/xml-templates/GetParts.xml b/server/rr/xml-templates/GetParts.xml new file mode 100644 index 000000000..eba4f6a56 --- /dev/null +++ b/server/rr/xml-templates/GetParts.xml @@ -0,0 +1,50 @@ + + + {{#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}} + + diff --git a/server/rr/xml-templates/InsertCustomer.xml b/server/rr/xml-templates/InsertCustomer.xml new file mode 100644 index 000000000..ce36139ba --- /dev/null +++ b/server/rr/xml-templates/InsertCustomer.xml @@ -0,0 +1,102 @@ + + + {{#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}} + + diff --git a/server/rr/xml-templates/InsertServiceVehicle.xml b/server/rr/xml-templates/InsertServiceVehicle.xml new file mode 100644 index 000000000..0487fbd2a --- /dev/null +++ b/server/rr/xml-templates/InsertServiceVehicle.xml @@ -0,0 +1,83 @@ + + + {{#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}} + + diff --git a/server/rr/xml-templates/UpdateCustomer.xml b/server/rr/xml-templates/UpdateCustomer.xml new file mode 100644 index 000000000..86f868e59 --- /dev/null +++ b/server/rr/xml-templates/UpdateCustomer.xml @@ -0,0 +1,107 @@ + + + {{#RequestId}}{{RequestId}}{{/RequestId}} + {{#Environment}}{{Environment}}{{/Environment}} + + + {{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}} + + diff --git a/server/rr/xml-templates/UpdateRepairOrder.xml b/server/rr/xml-templates/UpdateRepairOrder.xml new file mode 100644 index 000000000..e8d966bd2 --- /dev/null +++ b/server/rr/xml-templates/UpdateRepairOrder.xml @@ -0,0 +1,135 @@ + + + {{#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}} + + diff --git a/server/rr/xml-templates/_EnvelopeFooter.xml b/server/rr/xml-templates/_EnvelopeFooter.xml new file mode 100644 index 000000000..409a87e7d --- /dev/null +++ b/server/rr/xml-templates/_EnvelopeFooter.xml @@ -0,0 +1,17 @@ + + + + {{#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 new file mode 100644 index 000000000..361e7236e --- /dev/null +++ b/server/rr/xml-templates/_EnvelopeHeader.xml @@ -0,0 +1,29 @@ + + + + {{#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 2e623d015..a542e282c 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -416,14 +416,6 @@ const redisSocketEvents = ({ } }); - socket.on("rr-get-advisors", async ({ jobid, params }, cb) => { - // similar pattern using RrGetAdvisors - }); - - socket.on("rr-get-parts", async ({ jobid, params }, cb) => { - // similar pattern using RrGetParts - }); - socket.on("rr-get-advisors", async ({ jobid, params }, cb) => { try { const { RrGetAdvisors } = require("../rr/rr-lookup");