feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -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;
|
||||
|
||||
79
server/rr/rr-constants.js
Normal file
79
server/rr/rr-constants.js
Normal file
@@ -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 = "") => `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="${RR_NAMESPACE_URI}">
|
||||
<soapenv:Header>
|
||||
${headerXml}
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
${xmlBody}
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Loads base configuration for R&R requests from environment variables.
|
||||
* Dealer-specific overrides come from bodyshop.rr_configuration in the DB.
|
||||
*/
|
||||
const getBaseRRConfig = () => ({
|
||||
// IMPORTANT: RCI Receive endpoint ends with .ashx
|
||||
baseUrl: process.env.RR_API_BASE_URL || "https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx",
|
||||
username: process.env.RR_API_USER || "",
|
||||
password: process.env.RR_API_PASS || "",
|
||||
ppsysId: process.env.RR_PPSYS_ID || "",
|
||||
|
||||
// Welcome Kit often provides these (used in SOAP header)
|
||||
dealerNumber: process.env.RR_DEALER_NUMBER || "",
|
||||
storeNumber: process.env.RR_STORE_NUMBER || "",
|
||||
branchNumber: process.env.RR_BRANCH_NUMBER || "",
|
||||
|
||||
dealerDefault: process.env.RR_DEFAULT_DEALER || "ROME",
|
||||
timeout: RR_TIMEOUT_MS
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
RR_TIMEOUT_MS,
|
||||
RR_NAMESPACE_URI,
|
||||
RR_DEFAULT_MAX_RESULTS,
|
||||
RR_ACTIONS,
|
||||
RR_SOAP_HEADERS,
|
||||
buildSoapEnvelope,
|
||||
getBaseRRConfig
|
||||
};
|
||||
@@ -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<any>} 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<any>} 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
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
// - <Status code="0" severity="INFO">Success</Status>
|
||||
// - <Status code="123" severity="ERROR">Some message</Status>
|
||||
// - 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
|
||||
};
|
||||
|
||||
@@ -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<object>} 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<string>} 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<object>} 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<string>} Rendered XML string
|
||||
*/
|
||||
async function renderXmlTemplate(templateName, data) {
|
||||
const templatePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||
const xmlTemplate = await fs.readFile(templatePath, "utf8");
|
||||
return mustache.render(xmlTemplate, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any>}
|
||||
* Build a SOAP envelope with a rendered header + body.
|
||||
* Header comes from xml-templates/_EnvelopeHeader.xml.
|
||||
* @param {string} renderedBodyXml
|
||||
* @param {object} headerVars - values for header mustache
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function 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 `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||
<soapenv:Header>
|
||||
${headerXml}
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
${renderedBodyXml}
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
`.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<any>}
|
||||
* Body can be one of:
|
||||
* - string (already-rendered XML body)
|
||||
* - { template: "TemplateName", data: {...} } to render server/rr/xml-templates/TemplateName.xml
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string|object} params.action - RR action key (RR_ACTIONS) or a raw URL/spec
|
||||
* @param {string|{template:string,data:object}} params.body - Rendered XML or template descriptor
|
||||
* @param {object} params.socket - The socket or req object for context (used to resolve config & logging)
|
||||
* @param {object} [params.redisHelpers]
|
||||
* @param {string|number} [params.jobid]
|
||||
* @param {object} [params.dealerConfig]
|
||||
* @param {number} [params.retries=1]
|
||||
* @returns {Promise<string>} Raw SOAP response text
|
||||
*/
|
||||
async function MakeRRCall({
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<any>} 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<any>} 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<any>} 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
|
||||
};
|
||||
|
||||
@@ -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: <rr:CustomerInsertRq/>
|
||||
// - UpdateCustomer.xml: <rr:CustomerUpdateRq/>
|
||||
// - InsertServiceVehicle.xml: <rr:ServiceVehicleAddRq/>
|
||||
// - CreateRepairOrder.xml: <rr:RepairOrderInsertRq/>
|
||||
// - UpdateRepairOrder.xml: <rr:RepairOrderChgRq/>
|
||||
//
|
||||
// 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
|
||||
};
|
||||
|
||||
@@ -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<any>} - 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<any>} - 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
|
||||
};
|
||||
|
||||
127
server/rr/rr-test.js
Normal file
127
server/rr/rr-test.js
Normal file
@@ -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 = `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||
<soapenv:Header>
|
||||
${headerPreview}
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
${renderedBody}
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>`.trim();
|
||||
|
||||
console.log("\n--- Rendered SOAP Envelope (first 600 chars) ---\n");
|
||||
console.log(previewEnvelope.slice(0, 600));
|
||||
console.log("... [truncated]\n");
|
||||
|
||||
// If we don't have a base URL, skip the live call
|
||||
if (!baseCfg.baseUrl) {
|
||||
console.warn("\n⚠️ No RR baseUrl defined. Skipping live call.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
97
server/rr/rr-wsdl.js
Normal file
97
server/rr/rr-wsdl.js
Normal file
@@ -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<string|object>} 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
|
||||
};
|
||||
73
server/rr/xml-templates/CombinedSearch.xml
Normal file
73
server/rr/xml-templates/CombinedSearch.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<rr:CombinedSearchRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}
|
||||
<rr:RequestId>{{RequestId}}</rr:RequestId>
|
||||
{{/RequestId}}
|
||||
{{#Environment}}
|
||||
<rr:Environment>{{Environment}}</rr:Environment>
|
||||
{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}
|
||||
<rr:DealerName>{{DealerName}}</rr:DealerName>
|
||||
{{/DealerName}}
|
||||
{{#DealerNumber}}
|
||||
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
|
||||
{{/DealerNumber}}
|
||||
{{#StoreNumber}}
|
||||
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
|
||||
{{/StoreNumber}}
|
||||
{{#BranchNumber}}
|
||||
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
|
||||
{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:SearchCriteria>
|
||||
{{#Customer}}
|
||||
<rr:Customer>
|
||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
</rr:Customer>
|
||||
{{/Customer}}
|
||||
|
||||
{{#Vehicle}}
|
||||
<rr:ServiceVehicle>
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
||||
</rr:ServiceVehicle>
|
||||
{{/Vehicle}}
|
||||
|
||||
{{#Company}}
|
||||
<rr:Company>
|
||||
{{#Name}}<rr:Name>{{Name}}</rr:Name>{{/Name}}
|
||||
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
|
||||
</rr:Company>
|
||||
{{/Company}}
|
||||
|
||||
<!-- Search behavior flags (all optional) -->
|
||||
{{#SearchMode}}<rr:SearchMode>{{SearchMode}}</rr:SearchMode>{{/SearchMode}}
|
||||
{{#ExactMatch}}<rr:ExactMatch>{{ExactMatch}}</rr:ExactMatch>{{/ExactMatch}}
|
||||
{{#PartialMatch}}<rr:PartialMatch>{{PartialMatch}}</rr:PartialMatch>{{/PartialMatch}}
|
||||
{{#CaseInsensitive}}<rr:CaseInsensitive>{{CaseInsensitive}}</rr:CaseInsensitive>{{/CaseInsensitive}}
|
||||
|
||||
<!-- Result shaping (all optional) -->
|
||||
{{#ReturnCustomers}}<rr:ReturnCustomers>{{ReturnCustomers}}</rr:ReturnCustomers>{{/ReturnCustomers}}
|
||||
{{#ReturnVehicles}}<rr:ReturnVehicles>{{ReturnVehicles}}</rr:ReturnVehicles>{{/ReturnVehicles}}
|
||||
{{#ReturnCompanies}}<rr:ReturnCompanies>{{ReturnCompanies}}</rr:ReturnCompanies>{{/ReturnCompanies}}
|
||||
|
||||
<!-- Paging/sorting (all optional) -->
|
||||
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
||||
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
||||
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
|
||||
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
|
||||
</rr:SearchCriteria>
|
||||
</rr:CombinedSearchRq>
|
||||
158
server/rr/xml-templates/CreateRepairOrder.xml
Normal file
158
server/rr/xml-templates/CreateRepairOrder.xml
Normal file
@@ -0,0 +1,158 @@
|
||||
<rr:RepairOrderInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:RepairOrder>
|
||||
<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>
|
||||
{{#DmsRepairOrderId}}<rr:DmsRepairOrderId>{{DmsRepairOrderId}}</rr:DmsRepairOrderId>{{/DmsRepairOrderId}}
|
||||
|
||||
<!-- Core dates -->
|
||||
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
||||
|
||||
<!-- People & routing -->
|
||||
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||
{{#ProfitCenter}}<rr:ProfitCenter>{{ProfitCenter}}</rr:ProfitCenter>{{/ProfitCenter}}
|
||||
|
||||
<!-- Type & status -->
|
||||
{{#ROType}}<rr:ROType>{{ROType}}</rr:ROType>{{/ROType}}
|
||||
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}}
|
||||
{{#IsBodyShop}}<rr:IsBodyShop>{{IsBodyShop}}</rr:IsBodyShop>{{/IsBodyShop}}
|
||||
{{#DRPFlag}}<rr:DRPFlag>{{DRPFlag}}</rr:DRPFlag>{{/DRPFlag}}
|
||||
|
||||
<!-- Customer -->
|
||||
<rr:Customer>
|
||||
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
||||
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||
|
||||
<!-- Optional address if you have it -->
|
||||
{{#Address}}
|
||||
<rr:Address>
|
||||
{{#Line1}}<rr:Line1>{{Line1}}</rr:Line1>{{/Line1}}
|
||||
{{#Line2}}<rr:Line2>{{Line2}}</rr:Line2>{{/Line2}}
|
||||
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
||||
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
||||
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
||||
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
||||
</rr:Address>
|
||||
{{/Address}}
|
||||
</rr:Customer>
|
||||
|
||||
<!-- Vehicle -->
|
||||
<rr:Vehicle>
|
||||
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||
</rr:Vehicle>
|
||||
|
||||
<!-- Job lines -->
|
||||
{{#JobLines}}
|
||||
<rr:JobLine>
|
||||
<rr:Sequence>{{Sequence}}</rr:Sequence>
|
||||
{{#ParentSequence}}<rr:ParentSequence>{{ParentSequence}}</rr:ParentSequence>{{/ParentSequence}}
|
||||
|
||||
{{#LineType}}<rr:LineType>
|
||||
{{LineType}}</rr:LineType>{{/LineType}} <!-- LABOR | PART | MISC | FEE | DISCOUNT -->
|
||||
{{#Category}}<rr:Category>
|
||||
{{Category}}</rr:Category>{{/Category}} <!-- e.g., BODY, PAINT, GLASS -->
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
|
||||
<!-- Labor fields -->
|
||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||
|
||||
<!-- Part fields -->
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||
|
||||
<!-- Amounts -->
|
||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#DiscountAmount}}<rr:DiscountAmount>{{DiscountAmount}}</rr:DiscountAmount>{{/DiscountAmount}}
|
||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||
{{#GLAccount}}<rr:GLAccount>{{GLAccount}}</rr:GLAccount>{{/GLAccount}}
|
||||
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
|
||||
|
||||
<!-- Tax details (optional) -->
|
||||
{{#Taxes}}
|
||||
<rr:Taxes>
|
||||
{{#Items}}
|
||||
<rr:Tax>
|
||||
<rr:Code>{{Code}}</rr:Code>
|
||||
<rr:Amount>{{Amount}}</rr:Amount>
|
||||
{{#Rate}}<rr:Rate>{{Rate}}</rr:Rate>{{/Rate}}
|
||||
</rr:Tax>
|
||||
{{/Items}}
|
||||
</rr:Taxes>
|
||||
{{/Taxes}}
|
||||
</rr:JobLine>
|
||||
{{/JobLines}}
|
||||
|
||||
<!-- Totals -->
|
||||
{{#Totals}}
|
||||
<rr:Totals>
|
||||
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
||||
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
||||
{{#DiscountTotal}}<rr:DiscountTotal>{{DiscountTotal}}</rr:DiscountTotal>{{/DiscountTotal}}
|
||||
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
||||
<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>
|
||||
</rr:Totals>
|
||||
{{/Totals}}
|
||||
|
||||
<!-- Payers/Payments (optional) -->
|
||||
{{#Payments}}
|
||||
<rr:Payments>
|
||||
{{#Items}}
|
||||
<rr:Payment>
|
||||
<rr:PayerType>{{PayerType}}</rr:PayerType> <!-- CUSTOMER | INSURANCE | WARRANTY | FLEET -->
|
||||
{{#PayerName}}<rr:PayerName>{{PayerName}}</rr:PayerName>{{/PayerName}}
|
||||
<rr:Amount>{{Amount}}</rr:Amount>
|
||||
{{#Method}}<rr:Method>{{Method}}</rr:Method>{{/Method}}
|
||||
{{#Reference}}<rr:Reference>{{Reference}}</rr:Reference>{{/Reference}}
|
||||
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
|
||||
</rr:Payment>
|
||||
{{/Items}}
|
||||
</rr:Payments>
|
||||
{{/Payments}}
|
||||
|
||||
<!-- Insurance block (optional) -->
|
||||
{{#Insurance}}
|
||||
<rr:Insurance>
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
|
||||
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
|
||||
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
|
||||
</rr:Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
<!-- Notes -->
|
||||
{{#Notes}}
|
||||
<rr:Notes>
|
||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||
</rr:Notes>
|
||||
{{/Notes}}
|
||||
</rr:RepairOrder>
|
||||
</rr:RepairOrderInsertRq>
|
||||
34
server/rr/xml-templates/GetAdvisors.xml
Normal file
34
server/rr/xml-templates/GetAdvisors.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<rr:GetAdvisorsRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
{{#SearchCriteria}}
|
||||
<rr:SearchCriteria>
|
||||
{{#AdvisorId}}<rr:AdvisorId>{{AdvisorId}}</rr:AdvisorId>{{/AdvisorId}}
|
||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}} <!-- ACTIVE | INACTIVE -->
|
||||
{{#SearchMode}}<rr:SearchMode>
|
||||
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
|
||||
{{#Email}}<rr:Email>{{Email}}</rr:Email>{{/Email}}
|
||||
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
|
||||
{{#IncludeInactive}}<rr:IncludeInactive>{{IncludeInactive}}</rr:IncludeInactive>{{/IncludeInactive}}
|
||||
|
||||
<!-- Optional paging/sorting -->
|
||||
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
||||
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
||||
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
|
||||
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
|
||||
</rr:SearchCriteria>
|
||||
{{/SearchCriteria}}
|
||||
</rr:GetAdvisorsRq>
|
||||
50
server/rr/xml-templates/GetParts.xml
Normal file
50
server/rr/xml-templates/GetParts.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<rr:GetPartRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:SearchCriteria>
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Vendor}}<rr:Vendor>{{Vendor}}</rr:Vendor>{{/Vendor}}
|
||||
{{#Category}}<rr:Category>{{Category}}</rr:Category>{{/Category}}
|
||||
|
||||
<!-- Optional classification flags -->
|
||||
{{#Brand}}<rr:Brand>{{Brand}}</rr:Brand>{{/Brand}}
|
||||
{{#IsOEM}}<rr:IsOEM>{{IsOEM}}</rr:IsOEM>{{/IsOEM}} <!-- true | false -->
|
||||
{{#IsAftermarket}}<rr:IsAftermarket>{{IsAftermarket}}</rr:IsAftermarket>{{/IsAftermarket}}
|
||||
|
||||
<!-- Availability / inventory -->
|
||||
{{#InStock}}<rr:InStock>{{InStock}}</rr:InStock>{{/InStock}} <!-- true | false -->
|
||||
{{#Warehouse}}<rr:Warehouse>{{Warehouse}}</rr:Warehouse>{{/Warehouse}}
|
||||
{{#Location}}<rr:Location>{{Location}}</rr:Location>{{/Location}}
|
||||
|
||||
<!-- Pricing filters -->
|
||||
{{#MinPrice}}<rr:MinPrice>{{MinPrice}}</rr:MinPrice>{{/MinPrice}}
|
||||
{{#MaxPrice}}<rr:MaxPrice>{{MaxPrice}}</rr:MaxPrice>{{/MaxPrice}}
|
||||
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
||||
|
||||
<!-- Search behavior -->
|
||||
{{#SearchMode}}<rr:SearchMode>
|
||||
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
|
||||
|
||||
<!-- Paging / sorting -->
|
||||
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
||||
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
||||
{{#SortBy}}<rr:SortBy>
|
||||
{{SortBy}}</rr:SortBy>{{/SortBy}} <!-- e.g., PARTNUMBER, DESCRIPTION, PRICE -->
|
||||
{{#SortDirection}}<rr:SortDirection>
|
||||
{{SortDirection}}</rr:SortDirection>{{/SortDirection}} <!-- ASC | DESC -->
|
||||
</rr:SearchCriteria>
|
||||
</rr:GetPartRq>
|
||||
102
server/rr/xml-templates/InsertCustomer.xml
Normal file
102
server/rr/xml-templates/InsertCustomer.xml
Normal file
@@ -0,0 +1,102 @@
|
||||
<rr:CustomerInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:Customer>
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
{{#CustomerType}}<rr:CustomerType>
|
||||
{{CustomerType}}</rr:CustomerType>{{/CustomerType}} <!-- RETAIL | FLEET | INTERNAL -->
|
||||
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
|
||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
|
||||
|
||||
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
|
||||
|
||||
<!-- Optional customer classification -->
|
||||
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
|
||||
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
|
||||
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
|
||||
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
|
||||
|
||||
<!-- Addresses -->
|
||||
{{#Addresses}}
|
||||
<rr:Address>
|
||||
{{#AddressType}}<rr:AddressType>
|
||||
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
|
||||
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
|
||||
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
|
||||
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
||||
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
||||
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
||||
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
||||
</rr:Address>
|
||||
{{/Addresses}}
|
||||
|
||||
<!-- Phones -->
|
||||
{{#Phones}}
|
||||
<rr:Phone>
|
||||
<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>
|
||||
{{#PhoneType}}<rr:PhoneType>
|
||||
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||
</rr:Phone>
|
||||
{{/Phones}}
|
||||
|
||||
<!-- Emails -->
|
||||
{{#Emails}}
|
||||
<rr:Email>
|
||||
<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>
|
||||
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||
</rr:Email>
|
||||
{{/Emails}}
|
||||
|
||||
<!-- Driver's License -->
|
||||
{{#DriverLicense}}
|
||||
<rr:DriverLicense>
|
||||
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
|
||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||
</rr:DriverLicense>
|
||||
{{/DriverLicense}}
|
||||
|
||||
<!-- Insurance -->
|
||||
{{#Insurance}}
|
||||
<rr:Insurance>
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
||||
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
||||
</rr:Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
<!-- Optional linked accounts -->
|
||||
{{#LinkedAccounts}}
|
||||
<rr:LinkedAccount>
|
||||
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
|
||||
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
|
||||
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
|
||||
</rr:LinkedAccount>
|
||||
{{/LinkedAccounts}}
|
||||
|
||||
<!-- Notes -->
|
||||
{{#Notes}}
|
||||
<rr:Notes>
|
||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||
</rr:Notes>
|
||||
{{/Notes}}
|
||||
</rr:Customer>
|
||||
</rr:CustomerInsertRq>
|
||||
83
server/rr/xml-templates/InsertServiceVehicle.xml
Normal file
83
server/rr/xml-templates/InsertServiceVehicle.xml
Normal file
@@ -0,0 +1,83 @@
|
||||
<rr:ServiceVehicleAddRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:ServiceVehicle>
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
|
||||
<!-- Identity -->
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#UnitNumber}}<rr:UnitNumber>{{UnitNumber}}</rr:UnitNumber>{{/UnitNumber}}
|
||||
{{#StockNumber}}<rr:StockNumber>{{StockNumber}}</rr:StockNumber>{{/StockNumber}}
|
||||
|
||||
<!-- Descriptive -->
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Trim}}<rr:Trim>{{Trim}}</rr:Trim>{{/Trim}}
|
||||
{{#BodyStyle}}<rr:BodyStyle>{{BodyStyle}}</rr:BodyStyle>{{/BodyStyle}}
|
||||
{{#Transmission}}<rr:Transmission>{{Transmission}}</rr:Transmission>{{/Transmission}}
|
||||
{{#Engine}}<rr:Engine>{{Engine}}</rr:Engine>{{/Engine}}
|
||||
{{#FuelType}}<rr:FuelType>{{FuelType}}</rr:FuelType>{{/FuelType}}
|
||||
{{#DriveType}}<rr:DriveType>{{DriveType}}</rr:DriveType>{{/DriveType}}
|
||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||
|
||||
<!-- Registration -->
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||
{{#RegistrationExpiry}}<rr:RegistrationExpiry>{{RegistrationExpiry}}</rr:RegistrationExpiry>{{/RegistrationExpiry}}
|
||||
|
||||
<!-- Odometer -->
|
||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||
{{#OdometerUnits}}<rr:OdometerUnits>
|
||||
{{OdometerUnits}}</rr:OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
|
||||
{{#InServiceDate}}<rr:InServiceDate>{{InServiceDate}}</rr:InServiceDate>{{/InServiceDate}}
|
||||
|
||||
<!-- Ownership -->
|
||||
{{#Ownership}}
|
||||
<rr:Ownership>
|
||||
{{#OwnerId}}<rr:OwnerId>{{OwnerId}}</rr:OwnerId>{{/OwnerId}}
|
||||
{{#OwnerName}}<rr:OwnerName>{{OwnerName}}</rr:OwnerName>{{/OwnerName}}
|
||||
{{#OwnershipType}}<rr:OwnershipType>
|
||||
{{OwnershipType}}</rr:OwnershipType>{{/OwnershipType}} <!-- OWNER | LEASED | FLEET -->
|
||||
</rr:Ownership>
|
||||
{{/Ownership}}
|
||||
|
||||
<!-- Insurance -->
|
||||
{{#Insurance}}
|
||||
<rr:Insurance>
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
||||
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
||||
</rr:Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
<!-- Warranty -->
|
||||
{{#Warranty}}
|
||||
<rr:Warranty>
|
||||
{{#WarrantyCompany}}<rr:WarrantyCompany>{{WarrantyCompany}}</rr:WarrantyCompany>{{/WarrantyCompany}}
|
||||
{{#WarrantyNumber}}<rr:WarrantyNumber>{{WarrantyNumber}}</rr:WarrantyNumber>{{/WarrantyNumber}}
|
||||
{{#WarrantyType}}<rr:WarrantyType>{{WarrantyType}}</rr:WarrantyType>{{/WarrantyType}}
|
||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||
</rr:Warranty>
|
||||
{{/Warranty}}
|
||||
|
||||
<!-- Notes -->
|
||||
{{#VehicleNotes}}
|
||||
<rr:Notes>
|
||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||
</rr:Notes>
|
||||
{{/VehicleNotes}}
|
||||
</rr:ServiceVehicle>
|
||||
</rr:ServiceVehicleAddRq>
|
||||
107
server/rr/xml-templates/UpdateCustomer.xml
Normal file
107
server/rr/xml-templates/UpdateCustomer.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<rr:CustomerUpdateRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:Customer>
|
||||
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
||||
{{#CustomerType}}<rr:CustomerType>{{CustomerType}}</rr:CustomerType>{{/CustomerType}}
|
||||
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
|
||||
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
|
||||
|
||||
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
|
||||
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
|
||||
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
|
||||
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
|
||||
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
|
||||
|
||||
<!-- Addresses -->
|
||||
{{#Addresses}}
|
||||
<rr:Address>
|
||||
{{#AddressId}}<rr:AddressId>{{AddressId}}</rr:AddressId>{{/AddressId}}
|
||||
{{#AddressType}}<rr:AddressType>
|
||||
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
|
||||
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
|
||||
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
|
||||
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
||||
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
||||
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
||||
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
||||
{{#IsPrimary}}<rr:IsPrimary>{{IsPrimary}}</rr:IsPrimary>{{/IsPrimary}}
|
||||
</rr:Address>
|
||||
{{/Addresses}}
|
||||
|
||||
<!-- Phones -->
|
||||
{{#Phones}}
|
||||
<rr:Phone>
|
||||
{{#PhoneId}}<rr:PhoneId>{{PhoneId}}</rr:PhoneId>{{/PhoneId}}
|
||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||
{{#PhoneType}}<rr:PhoneType>
|
||||
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||
{{#IsDeleted}}<rr:IsDeleted>
|
||||
{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}} <!-- Mark for deletion -->
|
||||
</rr:Phone>
|
||||
{{/Phones}}
|
||||
|
||||
<!-- Emails -->
|
||||
{{#Emails}}
|
||||
<rr:Email>
|
||||
{{#EmailId}}<rr:EmailId>{{EmailId}}</rr:EmailId>{{/EmailId}}
|
||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
||||
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
|
||||
</rr:Email>
|
||||
{{/Emails}}
|
||||
|
||||
<!-- Driver's License -->
|
||||
{{#DriverLicense}}
|
||||
<rr:DriverLicense>
|
||||
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
|
||||
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||
</rr:DriverLicense>
|
||||
{{/DriverLicense}}
|
||||
|
||||
<!-- Insurance -->
|
||||
{{#Insurance}}
|
||||
<rr:Insurance>
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
||||
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
||||
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
||||
</rr:Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
<!-- Linked Accounts -->
|
||||
{{#LinkedAccounts}}
|
||||
<rr:LinkedAccount>
|
||||
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
|
||||
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
|
||||
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
|
||||
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
|
||||
</rr:LinkedAccount>
|
||||
{{/LinkedAccounts}}
|
||||
|
||||
<!-- Notes -->
|
||||
{{#Notes}}
|
||||
<rr:Notes>
|
||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||
</rr:Notes>
|
||||
{{/Notes}}
|
||||
</rr:Customer>
|
||||
</rr:CustomerUpdateRq>
|
||||
135
server/rr/xml-templates/UpdateRepairOrder.xml
Normal file
135
server/rr/xml-templates/UpdateRepairOrder.xml
Normal file
@@ -0,0 +1,135 @@
|
||||
<rr:RepairOrderChgRq xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional request metadata -->
|
||||
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||
|
||||
<rr:Dealer>
|
||||
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||
</rr:Dealer>
|
||||
|
||||
<rr:RepairOrder>
|
||||
<!-- Identity -->
|
||||
{{#RepairOrderId}}<rr:RepairOrderId>{{RepairOrderId}}</rr:RepairOrderId>{{/RepairOrderId}}
|
||||
{{#RepairOrderNumber}}<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>{{/RepairOrderNumber}}
|
||||
|
||||
<!-- Header fields that may be patched -->
|
||||
{{#Status}}<rr:Status>
|
||||
{{Status}}</rr:Status>{{/Status}} <!-- e.g., OPEN|IN_PROGRESS|CLOSED -->
|
||||
{{#ROType}}<rr:ROType>
|
||||
{{ROType}}</rr:ROType>{{/ROType}} <!-- e.g., INSURANCE|CUSTOMER_PAY -->
|
||||
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
||||
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
||||
{{#LocationCode}}<rr:LocationCode>{{LocationCode}}</rr:LocationCode>{{/LocationCode}}
|
||||
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||
{{#PurchaseOrder}}<rr:PurchaseOrder>{{PurchaseOrder}}</rr:PurchaseOrder>{{/PurchaseOrder}}
|
||||
|
||||
<!-- Optional customer patch -->
|
||||
{{#Customer}}
|
||||
<rr:Customer>
|
||||
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||
</rr:Customer>
|
||||
{{/Customer}}
|
||||
|
||||
<!-- Optional vehicle patch -->
|
||||
{{#Vehicle}}
|
||||
<rr:Vehicle>
|
||||
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||
</rr:Vehicle>
|
||||
{{/Vehicle}}
|
||||
|
||||
<!-- Line changes: use one of AddedJobLines / UpdatedJobLines / RemovedJobLines -->
|
||||
{{#AddedJobLines}}
|
||||
<rr:AddedJobLine>
|
||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||
{{#PayType}}<rr:PayType>
|
||||
{{PayType}}</rr:PayType>{{/PayType}} <!-- CUST|INS|WARR|INT -->
|
||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||
</rr:AddedJobLine>
|
||||
{{/AddedJobLines}}
|
||||
|
||||
{{#UpdatedJobLines}}
|
||||
<rr:UpdatedJobLine>
|
||||
<!-- Identify the existing line either by Sequence or LineId -->
|
||||
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||
{{#ChangeType}}<rr:ChangeType>
|
||||
{{ChangeType}}</rr:ChangeType>{{/ChangeType}} <!-- PRICE|QTY|DESC|OPCODE|PAYTYPE -->
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||
{{#PayType}}<rr:PayType>{{PayType}}</rr:PayType>{{/PayType}}
|
||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||
</rr:UpdatedJobLine>
|
||||
{{/UpdatedJobLines}}
|
||||
|
||||
{{#RemovedJobLines}}
|
||||
<rr:RemovedJobLine>
|
||||
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
||||
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||
</rr:RemovedJobLine>
|
||||
{{/RemovedJobLines}}
|
||||
|
||||
<!-- Totals (optional patch if RR expects header totals on change) -->
|
||||
{{#Totals}}
|
||||
<rr:Totals>
|
||||
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
||||
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
||||
{{#GrandTotal}}<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>{{/GrandTotal}}
|
||||
</rr:Totals>
|
||||
{{/Totals}}
|
||||
|
||||
<!-- Insurance (optional update) -->
|
||||
{{#Insurance}}
|
||||
<rr:Insurance>
|
||||
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
|
||||
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
|
||||
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
|
||||
</rr:Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
<!-- Notes (append or replace depending on RR semantics) -->
|
||||
{{#Notes}}
|
||||
<rr:Notes>
|
||||
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||
</rr:Notes>
|
||||
{{/Notes}}
|
||||
</rr:RepairOrder>
|
||||
</rr:RepairOrderChgRq>
|
||||
17
server/rr/xml-templates/_EnvelopeFooter.xml
Normal file
17
server/rr/xml-templates/_EnvelopeFooter.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- _EnvelopeFooter.xml -->
|
||||
<rr:Footer xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Optional system trace or session info -->
|
||||
{{#SessionId}}
|
||||
<rr:SessionId>{{SessionId}}</rr:SessionId>
|
||||
{{/SessionId}}
|
||||
|
||||
{{#Checksum}}
|
||||
<rr:Checksum>{{Checksum}}</rr:Checksum>
|
||||
{{/Checksum}}
|
||||
|
||||
{{#Timestamp}}
|
||||
<rr:Timestamp>{{Timestamp}}</rr:Timestamp>
|
||||
{{/Timestamp}}
|
||||
|
||||
<!-- Placeholder for any future required footer elements -->
|
||||
</rr:Footer>
|
||||
29
server/rr/xml-templates/_EnvelopeHeader.xml
Normal file
29
server/rr/xml-templates/_EnvelopeHeader.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<!-- _EnvelopeHeader.xml -->
|
||||
<rr:Authentication xmlns:rr="http://reynoldsandrey.com/">
|
||||
<!-- Required system identifier -->
|
||||
{{#PPSysId}}
|
||||
<rr:PPSysId>{{PPSysId}}</rr:PPSysId>
|
||||
{{/PPSysId}}
|
||||
|
||||
<!-- Dealer / Store / Branch numbers (optional but strongly recommended) -->
|
||||
{{#DealerNumber}}
|
||||
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
|
||||
{{/DealerNumber}}
|
||||
|
||||
{{#StoreNumber}}
|
||||
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
|
||||
{{/StoreNumber}}
|
||||
|
||||
{{#BranchNumber}}
|
||||
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
|
||||
{{/BranchNumber}}
|
||||
|
||||
<!-- Basic user credentials (always required) -->
|
||||
<rr:Username>{{Username}}</rr:Username>
|
||||
<rr:Password>{{Password}}</rr:Password>
|
||||
|
||||
<!-- Optional custom correlation token -->
|
||||
{{#CorrelationId}}
|
||||
<rr:CorrelationId>{{CorrelationId}}</rr:CorrelationId>
|
||||
{{/CorrelationId}}
|
||||
</rr:Authentication>
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user