diff --git a/server/routes/rrRoutes.js b/server/routes/rrRoutes.js index e9a2eb4c4..04f1b0efa 100644 --- a/server/routes/rrRoutes.js +++ b/server/routes/rrRoutes.js @@ -1,109 +1,156 @@ +// ----------------------------------------------------------------------------- +// 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. +// ----------------------------------------------------------------------------- + const express = require("express"); const router = express.Router(); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + const { RrCombinedSearch, RrGetAdvisors, RrGetParts } = require("../rr/rr-lookup"); const { RrCustomerInsert, RrCustomerUpdate } = require("../rr/rr-customer"); -const { CreateRepairOrder, UpdateRepairOrder } = require("../rr/rr-repair-order"); +// NOTE: correct filename is rr-repair-orders.js (plural) +const { CreateRepairOrder, UpdateRepairOrder } = require("../rr/rr-repair-orders"); -// NOTE: keep parity with /cdk endpoints so UI can flip provider with minimal diff +// Require auth on all RR routes (keep parity with /cdk) router.use(validateFirebaseIdTokenMiddleware); -// Placeholder endpoints — implement as needed: +// ----------------------------------------------------------------------------- +// Accounting parity / scaffolding +// ----------------------------------------------------------------------------- + +// Reuse CDK allocations for now; keep the endpoint name identical to /cdk router.post("/calculate-allocations", withUserGraphQLClientMiddleware, async (req, res) => { try { - const Calc = require("../cdk/cdk-calculate-allocations").default; // reuse for now - const result = await Calc(req, req.body.jobid, true); // true->verbose style like Fortellis + 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 }); } }); -// Example: load RR makes/models someday -router.post("/getvehicles", withUserGraphQLClientMiddleware, async (req, res) => { +// 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); + 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 } 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 }); } }); +// PUT /rr/customer/update/:id +// Body: { JobData, existingCustomer, patch } router.put("/customer/update/:id", async (req, res) => { try { const data = await RrCustomerUpdate({ socket: req, redisHelpers: req.sessionUtils, - JobData: req.body.JobData, - existingCustomer: req.body.existingCustomer, - patch: req.body.patch + JobData: req.body?.JobData, + existingCustomer: req.body?.existingCustomer, + patch: req.body?.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 }); } }); +// ----------------------------------------------------------------------------- +// Repair Order endpoints +// ----------------------------------------------------------------------------- + +// POST /rr/repair-order/create +// Body: { JobData, txEnvelope } router.post("/repair-order/create", async (req, res) => { try { const data = await CreateRepairOrder({ socket: req, redisHelpers: req.sessionUtils, - JobData: req.body.JobData, - txEnvelope: req.body.txEnvelope + JobData: req.body?.JobData, + txEnvelope: req.body?.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 }); } }); +// PUT /rr/repair-order/update/:id +// Body: { JobData, txEnvelope } router.put("/repair-order/update/:id", async (req, res) => { try { const data = await UpdateRepairOrder({ socket: req, redisHelpers: req.sessionUtils, - JobData: req.body.JobData, - txEnvelope: req.body.txEnvelope + JobData: req.body?.JobData, + txEnvelope: req.body?.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 }); } }); diff --git a/server/rr/rr-customer.js b/server/rr/rr-customer.js index b511ef507..317714ae7 100644 --- a/server/rr/rr-customer.js +++ b/server/rr/rr-customer.js @@ -1,28 +1,65 @@ +// ----------------------------------------------------------------------------- +// 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 +// ----------------------------------------------------------------------------- + const { MakeRRCall, RRActions } = require("./rr-helpers"); const { assertRrOk } = require("./rr-error"); const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers"); +/** + * Create a customer in RR. + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers - redisHelpers API (not used here directly) + * @param {Object} deps.JobData - Rome Job data used to build the payload + * @returns {Promise} RR response (envelope TBD) + */ async function RrCustomerInsert({ socket, redisHelpers, JobData }) { + // Map JobData -> RR "Customer Insert" request body const body = mapCustomerInsert(JobData); + const data = await MakeRRCall({ - ...RRActions.CreateCustomer, + ...RRActions.CreateCustomer, // POST /customer/v1/ body, redisHelpers, socket, - jobid: JobData.id + jobid: JobData?.id }); + + // TODO: assertRrOk should be updated once RR’s success envelope is finalized return assertRrOk(data, { apiName: "RR Create Customer" }); } +/** + * Update an existing customer in RR. + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers + * @param {Object} deps.JobData - context only (job id for correlation) + * @param {Object} deps.existingCustomer - Current RR customer record + * @param {Object} deps.patch - Minimal delta from UI to apply onto existingCustomer + * @returns {Promise} RR response + */ async function RrCustomerUpdate({ socket, redisHelpers, JobData, existingCustomer, patch }) { + // Build a merged/normalized payload for RR Update const body = mapCustomerUpdate(existingCustomer, patch); + const data = await MakeRRCall({ - ...RRActions.UpdateCustomer, // add to RRActions + ...RRActions.UpdateCustomer, // PUT /customer/v1/ (append id inside body/path per final spec) body, redisHelpers, socket, - jobid: JobData.id + jobid: JobData?.id }); + return assertRrOk(data, { apiName: "RR Update Customer" }); } diff --git a/server/rr/rr-error.js b/server/rr/rr-error.js index 9a5d7de9c..a93241304 100644 --- a/server/rr/rr-error.js +++ b/server/rr/rr-error.js @@ -1,4 +1,25 @@ +// ----------------------------------------------------------------------------- +// Error handling utilities for Reynolds & Reynolds (RR) API calls. +// This mirrors Fortellis/CDK error helpers so the call pipeline stays uniform. +// +// TODO:RR — Replace the heuristics in assertRrOk with the *actual* envelope and +// status semantics from the Rome RR specs. Examples in the PDFs may show: +// - Success +// - Some message +// - or a SuccessFlag/ReturnCode element in the JSON/XML response. +// ----------------------------------------------------------------------------- + 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"; @@ -11,15 +32,35 @@ class RrApiError extends Error { } } -// Match Rome/RR envelope once you confirm it; keep this central. +/** + * Assert that an RR API response is considered "OK". + * Throws RrApiError otherwise. + * + * @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 + */ function assertRrOk(data, { apiName, allowEmpty = false } = {}) { - // Example heuristics — update to exact envelope from the PDF: - // - successFlag === true, or - // - code === "0", or - // - !error / !errors length, etc. - if (!allowEmpty && (data == null || data.error || data.errors?.length)) { - throw new RrApiError(`${apiName} returned an error`, { errorData: data }); + // 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 }); } + return data; } diff --git a/server/rr/rr-helpers.js b/server/rr/rr-helpers.js index 32a1d5700..39789b51c 100644 --- a/server/rr/rr-helpers.js +++ b/server/rr/rr-helpers.js @@ -1,14 +1,28 @@ /** * RR (Reynolds & Reynolds) helper module - * - Loads env (.env.{NODE_ENV}) - * - Provides token retrieval + simple Redis-backed caching + * ----------------------------------------------------------------------------- + * 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 - * - Exports everything needed by rr-* feature files + * - 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 */ +const path = require("path"); +require("dotenv").config({ + path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) +}); + const uuid = require("uuid").v4; const AxiosLib = require("axios").default; const axios = AxiosLib.create(); @@ -17,29 +31,71 @@ const axiosCurlirize = require("axios-curlirize").default; const logger = require("../utils/logger"); const { RrApiError } = require("./rr-error"); -// Optional curl logging (handy while scaffolding) +// Emit curl equivalents for dev troubleshooting (safe to disable in prod) axiosCurlirize(axios, (result /*, err */) => { - const { command } = result; - // Disable or pipe to your logger if you prefer: - // logger.log("rr-axios-curl", "DEBUG", "api", null, { command }); - // Keeping a console for local scaffolding/bring-up: - console.log("*** rr axios (curl):", command); + 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"; /** - * Simple provider-level token cache using existing session helpers. - * We re-use setSessionData/getSessionData with a synthetic "socketId" - * key (so we don't need pubClient here). + * Transaction key namespace (mirrors Fortellis' getTransactionType) + * Used to partition per-job Redis session hashes. */ -const RR_PROVIDER_TOKEN_BUCKET = "rr:provider-token"; +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. Replace with the real auth call when available. + * 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 + * @param {Object} deps.redisHelpers - Your redisHelpers API * @returns {Promise} accessToken */ async function getRRToken({ redisHelpers }) { @@ -50,10 +106,9 @@ async function getRRToken({ redisHelpers }) { return cached.accessToken; } - // TODO: Implement real RR auth flow here. - // Stub: use env var or a fixed dev token + // TODO: Replace with real RR auth call. For now, fallback to env. const accessToken = process.env.RR_FAKE_TOKEN || "rr-dev-token"; - // Set an artificial 55-minute expiry (adjust to real value) + // Artificial ~55m expiry (adjust to actual token TTL) const expiresAt = Date.now() + 55 * 60 * 1000; await redisHelpers.setSessionData( @@ -69,41 +124,44 @@ async function getRRToken({ redisHelpers }) { message: error?.message, stack: error?.stack }); - // In absolute worst case, return a stub so dev environments keep moving + // Keep local dev moving even if cache errors return process.env.RR_FAKE_TOKEN || "rr-dev-token"; } } /** * 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 to append to URL as path (no leading slash needed) - * @param {Array<[string,string]>} [args.requestSearchParams] - tuples of [key, value] for query + * @param {string} [args.pathParams] - string appended to URL (no leading slash) + * @param {Array<[string,string]>} [args.requestSearchParams] - pairs converted to query params * @returns {string} */ function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) { - // normalize single trailing slash + // normalize: ensure exactly one trailing slash on base url = url.replace(/\/+$/, "/"); const fullPath = pathParams ? `${url}${pathParams}` : url; - const searchParams = new URLSearchParams(requestSearchParams).toString(); - return searchParams ? `${fullPath}?${searchParams}` : fullPath; + const query = new URLSearchParams(requestSearchParams).toString(); + return query ? `${fullPath}?${query}` : fullPath; } /** * 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 - * @param {string} args.access_token - * @param {string} args.reqId + * @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} */ 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); + await sleep((delayMeta?.checkStatusAfterSeconds || 2) * 1000); const statusUrl = delayMeta?._links?.status?.href; if (!statusUrl) { @@ -137,7 +195,7 @@ function sleep(ms) { } /** - * Core caller. Mirrors Fortellis' MakeFortellisCall shape so you can reuse flow. + * Core caller. Mirrors Fortellis' MakeFortellisCall shape so we can reuse flow. * * @param {Object} args * @param {string} args.apiName - logical name (used in logs/errors) @@ -147,7 +205,7 @@ function sleep(ms) { * @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] added as query params + * @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 @@ -169,7 +227,6 @@ async function MakeRRCall({ const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams }); const reqId = uuid(); const idempotencyKey = uuid(); - const access_token = await getRRToken({ redisHelpers }); if (debug) { @@ -179,12 +236,11 @@ async function MakeRRCall({ url: fullUrl, jobid, reqId, - body + body: safeLogJson(body) }); } try { - let resp; const baseHeaders = { Authorization: `Bearer ${access_token}`, "X-Request-Id": reqId, @@ -192,6 +248,7 @@ async function MakeRRCall({ ...headers }; + let resp; switch ((type || "post").toLowerCase()) { case "get": resp = await axios.get(fullUrl, { headers: baseHeaders }); @@ -200,6 +257,7 @@ async function MakeRRCall({ 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": @@ -268,9 +326,9 @@ async function MakeRRCall({ } /** - * Keep action registry centralized so upstream modules can import a single map. - * Replace the base URLs with real RR/Rome endpoints as you finalize the integration. - * You can also split this into per-domain registries once you know the layout. + * 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 @@ -312,7 +370,7 @@ const RRActions = { UpdateCustomer: { apiName: "RR Update Customer", url: isProduction - ? "https://rr.example.com/customer/v1/" // append /{id} if required + ? "https://rr.example.com/customer/v1/" // append /{id} if required by spec : "https://rr-uat.example.com/customer/v1/", type: "put" }, @@ -323,11 +381,12 @@ const RRActions = { : "https://rr-uat.example.com/customer/v1/", type: "get" }, - QueryCustomerByName: { + 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)", @@ -336,12 +395,14 @@ const RRActions = { : "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", @@ -413,9 +474,15 @@ function safeLogJson(data) { } module.exports = { + // core helpers MakeRRCall, RRActions, getRRToken, constructFullUrl, - DelayedCallback + DelayedCallback, + + // parity exports required by other RR modules + getTransactionType, + defaultRRTTL, + RRCacheEnums }; diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index 8f4409eb2..f35c1caf1 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -1,4 +1,22 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; +// ----------------------------------------------------------------------------- +// 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. +// ----------------------------------------------------------------------------- + +const { GraphQLClient } = require("graphql-request"); const moment = require("moment-timezone"); const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default; // reuse allocations @@ -6,13 +24,21 @@ const CreateRRLogEvent = require("./rr-logger"); const queries = require("../graphql-client/queries"); const { MakeRRCall, RRActions, getTransactionType, defaultRRTTL, RRCacheEnums } = require("./rr-helpers"); -// --- Public entry points (similar to Fortellis) +// ----------------------------------------------------------------------------- +// Public entry points (wired in redisSocketEvents.js) +// ----------------------------------------------------------------------------- + +/** + * Seed export: cache txEnvelope + JobData, discover VIN->VehicleId + owner, + * search by customer name, and prompt client to select/create a customer. + */ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { const { setSessionTransactionData } = redisHelpers; try { CreateRRLogEvent(socket, "DEBUG", `[RR] Received Job export request`, { jobid }); + // cache txEnvelope for this job session await setSessionTransactionData( socket.id, getTransactionType(jobid), @@ -31,10 +57,12 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { 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); - const owner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT"); + // 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( @@ -47,6 +75,7 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { } } + // Search customers by job owner name (param names TBD per RR) const DMSCustList = await SearchCustomerByName({ socket, redisHelpers, JobData }); await setSessionTransactionData( socket.id, @@ -56,6 +85,7 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { defaultRRTTL ); + // Emit choices: (VIN owner first if present) + search results socket.emit("rr-select-customer", [ ...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []), ...(Array.isArray(DMSCustList) ? DMSCustList : []) @@ -65,6 +95,13 @@ async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { } } +/** + * 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; @@ -81,6 +118,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo 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 }); @@ -90,16 +128,17 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo } 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: implement UpdateVehicle if RR supports updating ownership - // DMSVeh = await UpdateVehicle({ ... }) + // 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, @@ -109,6 +148,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo defaultRRTTL ); + // Post lines const DMSBatchTxn = await TransBatchWip({ socket, redisHelpers, JobData }); await setSessionTransactionData( socket.id, @@ -118,7 +158,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo defaultRRTTL ); - // decide success/err format later; keep parity with Fortellis shape + // Decide success from envelope (heuristic until exact spec confirmed) if (String(DMSBatchTxn?.rtnCode || "0") === "0") { const DmsBatchTxnPost = await PostBatchWip({ socket, redisHelpers, JobData }); await setSessionTransactionData( @@ -132,7 +172,7 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo if (String(DmsBatchTxnPost?.rtnCode || "0") === "0") { await MarkJobExported({ socket, jobid: JobData.id, redisHelpers }); - // Optional service history write + // Optional service history write (non-blocking) try { const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData }); await setSessionTransactionData( @@ -168,7 +208,10 @@ async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jo } } -// --- GraphQL job fetch +// ----------------------------------------------------------------------------- +// GraphQL job fetch +// ----------------------------------------------------------------------------- + async function QueryJobData({ socket, jobid }) { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); const currentToken = @@ -177,10 +220,14 @@ async function QueryJobData({ socket, jobid }) { 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) ------------------------- +// ----------------------------------------------------------------------------- +// 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({ @@ -213,17 +260,17 @@ async function ReadCustomerById({ socket, redisHelpers, JobData, customerId }) { } async function SearchCustomerByName({ socket, redisHelpers, JobData }) { - // align with Rome Search spec later + // 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]] + ? [["lastName", JobData.ownr_co_nm]] // placeholder: business search : [ ["firstName", JobData.ownr_fn], ["lastName", JobData.ownr_ln] ]; return await MakeRRCall({ - ...RRActions.SearchCustomer, + ...RRActions.QueryCustomerByName, // ✅ use action defined in rr-helpers requestSearchParams: ownerNameParams, redisHelpers, socket, @@ -232,10 +279,9 @@ async function SearchCustomerByName({ socket, redisHelpers, JobData }) { } async function CreateCustomer({ socket, redisHelpers, JobData }) { - // shape per Rome Customer Insert spec + // TODO: Replace with exact RR Customer Insert envelope & fields const body = { customerType: JobData.ownr_co_nm ? "BUSINESS" : "INDIVIDUAL" - // fill minimal required fields later }; return await MakeRRCall({ ...RRActions.CreateCustomer, @@ -246,9 +292,9 @@ async function CreateCustomer({ socket, redisHelpers, JobData }) { }); } -async function InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust }) { +async function InsertVehicle({ socket, redisHelpers, JobData /*, txEnvelope, DMSVid, DMSCust*/ }) { + // TODO: Replace with exact RR Service Vehicle Insert mapping const body = { - // map fields per Rome Insert Service Vehicle spec vin: JobData.v_vin // owners, make/model, odometer, etc… }; @@ -262,14 +308,15 @@ async function InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid } 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 || "", + desc: txEnvelope?.story || "", docType: "10", m13Flag: "0", refer: JobData.ro_number, - srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder - srcJrnl: txEnvelope.journal, + srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder from CDK config; RR equivalent TBD + srcJrnl: txEnvelope?.journal, userID: "BSMS" }; return await MakeRRCall({ @@ -283,9 +330,11 @@ async function StartWip({ socket, redisHelpers, JobData, txEnvelope }) { 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, // shape per Rome spec + body: wips, redisHelpers, socket, jobid: JobData.id @@ -299,6 +348,7 @@ async function PostBatchWip({ socket, redisHelpers, JobData }) { RRCacheEnums.DMSTransHeader ); + // TODO: Confirm final field names for “post” operation in RR const body = { opCode: "P", transID: DMSTransHeader?.transID @@ -334,7 +384,10 @@ async function DeleteWip({ socket, redisHelpers, JobData }) { 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, @@ -351,8 +404,8 @@ async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) { RRCacheEnums.txEnvelope ); + // TODO: Replace with RR Service Vehicle History schema const body = { - // map to Rome “Service Vehicle History Insert” spec comments: txEnvelope?.story || "" }; return await MakeRRCall({ @@ -364,7 +417,7 @@ async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) { }); } -async function HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeader }) { +async function HandlePostingError({ socket, redisHelpers, JobData /*, DMSTransHeader*/ }) { const DmsError = await QueryErrWip({ socket, redisHelpers, JobData }); await DeleteWip({ socket, redisHelpers, JobData }); @@ -373,25 +426,30 @@ async function HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeade await InsertFailedExportLog({ socket, JobData, error: errString }); } +/** + * Convert app allocations to RR WIP lines. + * Re-uses existing CalculateAllocations to keep parity with CDK/Fortellis. + * + * TODO: Confirm exact RR posting model (accounts, control numbers, company ids, + * and whether amounts are signed or need separate debit/credit flags). + */ async function GenerateTransWips({ socket, redisHelpers, JobData }) { - // reuse the existing allocator - const allocations = await CalculateAllocations(socket, JobData.id, true); // true==enable verbose logging + const allocations = await CalculateAllocations(socket, JobData.id, true); // true==verbose logging const DMSTransHeader = await redisHelpers.getSessionTransactionData( socket.id, getTransactionType(JobData.id), RRCacheEnums.DMSTransHeader ); - // Translate allocations -> RR WIP line shape later. For now: keep parity with Fortellis skeleton 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(), + 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 + trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco // RR equivalent TBD }); } if (alloc.cost.getAmount() > 0 && !alloc.tax) { @@ -426,11 +484,12 @@ async function GenerateTransWips({ socket, redisHelpers, JobData }) { getTransactionType(JobData.id), RRCacheEnums.txEnvelope ); + txEnvelope?.payers?.forEach((payer) => { wips.push({ acct: payer.dms_acctnumber, cntl: payer.controlnumber, - postAmt: Math.round(payer.amount * 100), + postAmt: Math.round(payer.amount * 100), // assuming cents (confirm RR units) transID: DMSTransHeader?.transID, trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco }); @@ -446,28 +505,37 @@ async function GenerateTransWips({ socket, redisHelpers, JobData }) { return wips; } -// --- DB logging mirrors Fortellis +// ----------------------------------------------------------------------------- +// 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: socket.JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*", + status: JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*", date_exported: new Date() }, log: { - bodyshopid: socket.JobData?.bodyshop?.id, + bodyshopid: JobData?.bodyshop?.id, jobid, successful: true, useremail: socket.user?.email, - metadata: await redisHelpers.getSessionTransactionData( - socket.id, - getTransactionType(jobid), - RRCacheEnums.transWips - ) + metadata: transWips }, bill: { exported: true, exported_at: new Date() } }); @@ -478,6 +546,7 @@ async function InsertFailedExportLog({ socket, JobData, error }) { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); const currentToken = (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); + return await client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.INSERT_EXPORT_LOG, { log: { bodyshopid: JobData.bodyshop.id, diff --git a/server/rr/rr-logger.js b/server/rr/rr-logger.js index 347d15a0d..2d6a827be 100644 --- a/server/rr/rr-logger.js +++ b/server/rr/rr-logger.js @@ -1,8 +1,49 @@ +// ----------------------------------------------------------------------------- +// 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. +// ----------------------------------------------------------------------------- + const logger = require("../utils/logger"); -const CreateRRLogEvent = (socket, level, message, txnDetails) => { - logger.log("rr-log-event", level, socket?.user?.email, null, { wsmessage: message, txnDetails }); - socket.emit("rr-log-event", { level, message, txnDetails }); -}; +/** + * Emit a structured RR log event to both the central logger and (if present) + * over the current socket connection for real-time UI visibility. + * + * @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) + */ +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. + try { + logger.log("rr-log-event", level, userEmail, null, { + 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 }); + } + } catch { + // Best-effort: never throw from logging + } +} module.exports = CreateRRLogEvent; diff --git a/server/rr/rr-lookup.js b/server/rr/rr-lookup.js index 7e0806b9b..3b4734aa5 100644 --- a/server/rr/rr-lookup.js +++ b/server/rr/rr-lookup.js @@ -1,21 +1,58 @@ +// ----------------------------------------------------------------------------- +// 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. +// ----------------------------------------------------------------------------- + const { MakeRRCall, RRActions } = require("./rr-helpers"); const { assertRrOk } = require("./rr-error"); +/** + * RR Combined Search (Customer + Vehicle). + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers + * @param {string|number} deps.jobid - for correlation/logging only + * @param {Array<[string,string]>} [deps.params=[]] + * Example: [["vin", "1HGBH41JXMN109186"], ["lastName","DOE"]] + * @returns {Promise} RR response (envelope TBD) + */ async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) { const data = await MakeRRCall({ - ...RRActions.CombinedSearch, // add to RRActions - requestSearchParams: params, // e.g., [["vin", "XXXX"], ["lastName","DOE"]] + ...RRActions.CombinedSearch, // GET /search/v1/customer-vehicle + requestSearchParams: params, type: "get", redisHelpers, socket, jobid }); + + // allowEmpty=true because searches may legitimately return 0 rows return assertRrOk(data, { apiName: "RR Combined Search", allowEmpty: true }); } +/** + * RR Get Advisors. + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers + * @param {string|number} deps.jobid + * @param {Array<[string,string]>} [deps.params=[]] + * Example: [["active","true"]] + * @returns {Promise} RR response (envelope TBD) + */ async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) { const data = await MakeRRCall({ - ...RRActions.GetAdvisors, // add + ...RRActions.GetAdvisors, // GET /advisors/v1 requestSearchParams: params, type: "get", redisHelpers, @@ -25,9 +62,20 @@ async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) { return assertRrOk(data, { apiName: "RR Get Advisors", allowEmpty: true }); } +/** + * RR Get Parts. + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers + * @param {string|number} deps.jobid + * @param {Array<[string,string]>} [deps.params=[]] + * Example: [["sku","ABC123"], ["page","1"], ["pageSize","50"]] + * @returns {Promise} RR response (envelope TBD) + */ async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) { const data = await MakeRRCall({ - ...RRActions.GetParts, // add + ...RRActions.GetParts, // GET /parts/v1 requestSearchParams: params, type: "get", redisHelpers, diff --git a/server/rr/rr-mappers.js b/server/rr/rr-mappers.js index 120ea100b..843186fae 100644 --- a/server/rr/rr-mappers.js +++ b/server/rr/rr-mappers.js @@ -1,43 +1,55 @@ -// server/rr/rr-mappers.js +// ----------------------------------------------------------------------------- // Centralized mapping & normalization for Reynolds & Reynolds (RR) // -// NOTE: This is scaffolding intended to be completed against the RR XML/JSON -// schemas in the Rome RR specs you dropped (Customer Insert/Update, Repair Order, -// Service Vehicle, etc.). Field names below are placeholders where noted. -// Fill the TODOs with the exact RR element/attribute names. +// This is scaffolding aligned to the Rome RR PDFs you provided: // -// Usage expectation from calling code (example you gave): -// const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers"); -// const body = mapCustomerInsert(JobData); -// const body = mapCustomerUpdate(existingCustomer, patch); +// - 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 +// +// 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. +// ----------------------------------------------------------------------------- const _ = require("lodash"); -const InstanceMgr = require("../utils/instanceMgr").default; // Keep this consistent with other providers const replaceSpecialRegex = /[^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 normalizePostal(raw) { - // Match Fortellis/CDK behavior for CA vs US formatting - // (Use InstanceMgr profile detection already present in your codebase) - return InstanceMgr({ - imex: raw && String(raw).toUpperCase().replace(/\W/g, "").replace(/(...)/, "$1 "), - rome: raw - }); -} - function asStringOrNull(value) { const s = sanitize(value); return s && s.length > 0 ? s : null; } +function toUpperOrNull(value) { + const s = asStringOrNull(value); + 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 mapPhones({ ph1, ph2, mobile }) { - // TODO: Update to RR’s final phone structure and codes/types when wiring + // TODO:RR — Replace "HOME|WORK|MOBILE" with RR's phone type codes + any flags (preferred, sms ok). const out = []; if (ph1) out.push({ number: sanitize(ph1), type: "HOME" }); if (ph2) out.push({ number: sanitize(ph2), type: "WORK" }); @@ -46,22 +58,24 @@ function mapPhones({ ph1, ph2, mobile }) { } function mapEmails({ email }) { - // RR often supports multiple emails; start with one. - // TODO: Update per RR schema (email flags, preferred, etc.) + // TODO:RR — If RR supports multiple with flags, expand (preferred, statement, etc.). if (!email) return []; return [{ address: sanitize(email), type: "PERSONAL" }]; } +// ---------- Address/Contact from Rome JobData -------------------------------- + function mapPostalAddressFromJob(job) { - // Rome job-level owner fields (aligning to prior provider scaffolds) + // 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", // default, adjust as needed - province: asStringOrNull(job.ownr_st) // keep both state/province fields for CA cases + country: asStringOrNull(job.ownr_ctry) || "USA" }; } @@ -77,93 +91,97 @@ function mapEmailsFromJob(job) { return mapEmails({ email: job.ownr_ea }); } +// ---------- Customer mappers -------------------------------------------------- + /** * Customer Insert - * Matches your call site: - * const body = mapCustomerInsert(JobData) + * Matches call-site: const body = mapCustomerInsert(JobData); * - * Return shape intentionally mirrors Fortellis scaffolding so the same - * MakeRRCall pipeline can be reused. Replace placeholders with the RR spec’s - * request envelope/element names (e.g., CustomerInsertRq, CustomerRq, etc.). + * 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() !== ""); + const isCompany = Boolean(job?.ownr_co_nm && job.ownr_co_nm.trim() !== ""); - // Skeleton payload — replace keys under CustomerInsertRq with the actual RR names return { + // Example envelope — rename to match the PDF (e.g., "CustomerInsertRq") CustomerInsertRq: { - // TODO: Confirm RR element/attribute names from spec PDFs + // 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: asStringOrNull(job.ownr_co_nm)?.toUpperCase() || null, - firstName: isCompany ? null : asStringOrNull(job.ownr_fn)?.toUpperCase(), - lastName: isCompany ? null : asStringOrNull(job.ownr_ln)?.toUpperCase() + 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) } - // Optional / placeholders for future fields in RR spec + + // TODO:RR — Common optional fields: tax/resale codes, pricing flags, AR terms, source codes, etc. // taxCode: null, - // groupCode: null, - // dealerFields: [] + // termsCode: null, + // marketingOptIn: null, + // dealerSpecificFields: [] } }; } /** * Customer Update - * Matches your call site: - * const body = mapCustomerUpdate(existingCustomer, patch) + * Matches call-site: const body = mapCustomerUpdate(existingCustomer, patch); * - * - existingCustomer: prior RR customer payload/shape (from RR Read/Query) - * - patch: minimal delta from UI/Job selections to overlay onto the RR model + * - existingCustomer: RR's current representation (from Read/Query) + * - patch: a thin delta from UI/Job selection * - * We return a merged/normalized payload for RR Update. + * TODO:RR — Swap envelope/fields for RR's specific Update schema. */ function mapCustomerUpdate(existingCustomer, patch = {}) { - // NOTE: - // 1) We assume existingCustomer already resembles RR’s stored shape. - // 2) We overlay patch fields into that shape, then project to the - // RR Update request envelope. - // 3) Replace inner keys with exact RR Update schema element names. - const merged = _.merge({}, existingCustomer || {}, patch || {}); const id = merged?.customerId || merged?.id || merged?.CustomerId || merged?.customer?.id || null; - // Derive a normalized name object from merged data (handles org/person) - const isCompany = Boolean(merged?.customerName?.companyName) || Boolean(merged?.companyName) || false; + const isCompany = Boolean(merged?.customerName?.companyName) || Boolean(merged?.companyName); const normalizedName = { - companyName: asStringOrNull(merged?.customerName?.companyName) || asStringOrNull(merged?.companyName), - firstName: isCompany ? null : asStringOrNull(merged?.customerName?.firstName) || asStringOrNull(merged?.firstName), - lastName: isCompany ? null : asStringOrNull(merged?.customerName?.lastName) || asStringOrNull(merged?.lastName) + 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 normalizedAddress = { - addressLine1: asStringOrNull(merged?.postalAddress?.addressLine1) || asStringOrNull(merged?.addressLine1), - addressLine2: asStringOrNull(merged?.postalAddress?.addressLine2) || asStringOrNull(merged?.addressLine2), - city: asStringOrNull(merged?.postalAddress?.city) || asStringOrNull(merged?.city), + 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), + 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", - province: asStringOrNull(merged?.postalAddress?.province) || asStringOrNull(merged?.province) + country: asStringOrNull(merged?.postalAddress?.country) || asStringOrNull(merged?.country) || "USA" }; - // Phones + // Contacts (reuse existing unless patch supplied a new structure upstream) const normalizedPhones = merged?.contactMethods?.phones || merged?.phones || []; - // Emails const normalizedEmails = merged?.contactMethods?.emailAddresses || merged?.emailAddresses || []; return { + // Example envelope — rename to match the PDF (e.g., "CustomerUpdateRq") CustomerUpdateRq: { - // TODO: Confirm exact RR element/attribute names for update customerId: id, customerType: normalizedName.companyName ? "ORGANIZATION" : "INDIVIDUAL", customerName: normalizedName, @@ -172,77 +190,144 @@ function mapCustomerUpdate(existingCustomer, patch = {}) { phones: normalizedPhones, emailAddresses: normalizedEmails } - // Optional change tracking fields, timestamps, etc., per RR spec can go here + // TODO:RR — include fields that RR requires for update (version, hash, lastUpdatedTs, etc.) } }; } -/* ===== Additional mappers (scaffolding for upcoming work) ===== */ +// ---------- Vehicle mappers --------------------------------------------------- + +/** + * 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 = {}) { - // TODO: Replace with RR Service Vehicle Insert schema return { ServiceVehicleInsertRq: { vin: asStringOrNull(job.v_vin), + // Year/make/model — validate source fields vs RR required fields year: job.v_model_yr || null, - make: txEnvelope.dms_make || asStringOrNull(job.v_make), - model: txEnvelope.dms_model || asStringOrNull(job.v_model), - odometer: txEnvelope.kmout || null, - licensePlate: job.plate_no && /\w/.test(job.plate_no) ? asStringOrNull(job.plate_no).toUpperCase() : 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. } }; } -function mapRepairOrderAddFromJob(job) { - // TODO: Replace with RR RepairOrder Add schema (headers, lines, taxes) +// ---------- Repair Order mappers --------------------------------------------- + +/** + * 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 { - RepairOrderAddRq: { - customerId: job.customer?.id || null, - vehicleId: job.vehicle?.id || null, - referenceNumber: asStringOrNull(job.ro_number), - openedAt: job.actual_in || null, - closedAt: job.invoice_date || null - // lines: job.joblines?.map(mapJobLineToRRLine), - // taxes: mapTaxes(job), - // payments: mapPayments(job) + 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, + + // Lines (placeholder) + lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [], + + // Taxes (placeholder) + taxes: mapTaxes(JobData), + + // Payments (placeholder) + payments: mapPayments(txEnvelope) + + // TODO:RR — add required flags, shop supplies, labor matrix, discounts, etc. } }; } -function mapRepairOrderChangeFromJob(job) { - // TODO: Replace with RR RepairOrder Update schema +/** + * 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 { - RepairOrderChgRq: { - repairOrderId: job.id, - referenceNumber: asStringOrNull(job.ro_number) - // delta lines, amounts, status, etc. + RepairOrderUpdateRq: { + repairOrderId: JobData?.id || txEnvelope?.repairOrderId || null, + referenceNumber: asStringOrNull(JobData?.ro_number), + + // 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. } }; } -/* Example line mapper (placeholder) */ +/* ----- 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 { - // TODO: set RR fields - seq: line.sequence || null, - opCode: line.opCode || null, - description: asStringOrNull(line.description), - qty: line.part_qty || null, - price: line.price || null + 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 + })); +} + +// ---------- Exports ----------------------------------------------------------- + module.exports = { - // Required by your current calling code: + // Used by current call-sites: mapCustomerInsert, mapCustomerUpdate, + mapRepairOrderCreate, + mapRepairOrderUpdate, - // Extra scaffolds we’ll likely use right after: + // Extra scaffolds you’ll likely use soon: mapVehicleInsertFromJob, - mapRepairOrderAddFromJob, - mapRepairOrderChangeFromJob, mapJobLineToRRLine, + mapTaxes, + mapPayments, - // low-level utils (export if you want to reuse in tests) + // Low-level utils (handy in tests) _sanitize: sanitize, - _normalizePostal: normalizePostal + _normalizePostal: normalizePostal, + _toUpperOrNull: toUpperOrNull }; diff --git a/server/rr/rr-repair-orders.js b/server/rr/rr-repair-orders.js index 678f16f47..273dc0784 100644 --- a/server/rr/rr-repair-orders.js +++ b/server/rr/rr-repair-orders.js @@ -1,28 +1,75 @@ +// 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 +// ----------------------------------------------------------------------------- + const { MakeRRCall, RRActions } = require("./rr-helpers"); const { assertRrOk } = require("./rr-error"); -const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers"); +const { mapRepairOrderAddFromJob, mapRepairOrderChangeFromJob } = require("./rr-mappers"); +/** + * Create a Repair Order in RR. + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers + * @param {Object} deps.JobData - Rome job (used for mapping) + * @param {Object} deps.txEnvelope - Posting/GL context if needed in mapping + * @returns {Promise} - RR response (envelope TBD) + */ async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { - const body = mapRepairOrderCreate({ JobData, txEnvelope }); + // Map JobData (+ optional txEnvelope) -> RR "Repair Order Add" request body + const body = mapRepairOrderAddFromJob({ ...JobData, txEnvelope }); + const data = await MakeRRCall({ - ...RRActions.CreateRepairOrder, // add this entry to RRActions (POST /repair-orders) + ...RRActions.CreateRepairOrder, // POST /repair-orders/v1 body, redisHelpers, socket, - jobid: JobData.id + jobid: JobData?.id }); + + // TODO: Update assertRrOk once RR’s success envelope is finalized return assertRrOk(data, { apiName: "RR Create Repair Order" }); } -async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { - const body = mapRepairOrderUpdate({ JobData, txEnvelope }); +/** + * Update a Repair Order in RR. + * + * NOTE: If RR requires the repair order id in the URL (PUT /repair-orders/{id}), + * pass it via requestPathParams here once you have it: + * requestPathParams: repairOrderId + * and ensure RRActions.UpdateRepairOrder.url ends with a trailing slash. + * + * @param {Object} deps + * @param {Socket|ExpressRequest} deps.socket + * @param {Object} deps.redisHelpers + * @param {Object} deps.JobData - Rome job (used for mapping) + * @param {Object} deps.txEnvelope - Posting/GL context if needed in mapping + * @param {string|number} [deps.repairOrderId] - If RR expects a path param + * @returns {Promise} - RR response (envelope TBD) + */ +async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope, repairOrderId }) { + const body = mapRepairOrderChangeFromJob({ ...JobData, txEnvelope }); + const data = await MakeRRCall({ - ...RRActions.UpdateRepairOrder, // add this entry (PUT /repair-orders/{id}) + ...RRActions.UpdateRepairOrder, // PUT /repair-orders/v1 (or /v1/{id}) + ...(repairOrderId ? { requestPathParams: String(repairOrderId) } : {}), body, redisHelpers, socket, - jobid: JobData.id + jobid: JobData?.id }); + return assertRrOk(data, { apiName: "RR Update Repair Order" }); } diff --git a/server/utils/redisHelpers.js b/server/utils/redisHelpers.js index 85ec72b4f..950d98931 100644 --- a/server/utils/redisHelpers.js +++ b/server/utils/redisHelpers.js @@ -321,111 +321,9 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => { } }; - // NOTE: The following code was written for an abandoned branch and things have changes since the, - // Leaving it here for demonstration purposes, commenting it out so it does not get used + const setProviderCache = (ns, field, value, ttl) => setSessionData(`${ns}:provider`, field, value, ttl); - // Store multiple session data in Redis - // const setMultipleSessionData = async (socketId, keyValues) => { - // try { - // // keyValues is expected to be an object { key1: value1, key2: value2, ... } - // const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]); - // await pubClient.hset(`socket:${socketId}`, ...entries.flat()); - // } catch (error) { - // logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); - // } - // }; - - // Retrieve multiple session data from Redis - // const getMultipleSessionData = async (socketId, keys) => { - // try { - // const data = await pubClient.hmget(`socket:${socketId}`, keys); - // // Redis returns an object with null values for missing keys, so we parse the non-null ones - // return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null])); - // } catch (error) { - // logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); - // } - // }; - - // const setMultipleFromArraySessionData = async (socketId, keyValueArray) => { - // try { - // // Use Redis multi/pipeline to batch the commands - // const multi = pubClient.multi(); - // keyValueArray.forEach(([key, value]) => { - // multi.hset(`socket:${socketId}`, key, JSON.stringify(value)); - // }); - // await multi.exec(); // Execute all queued commands - // } catch (error) { - // logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); - // } - // }; - - // Helper function to add an item to the end of the Redis list - // const addItemToEndOfList = async (socketId, key, newItem) => { - // try { - // await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem)); - // } catch (error) { - // let userEmail = "unknown"; - // let socketMappings = {}; - // try { - // const userData = await getSessionData(socketId, "user"); - // if (userData && userData.email) { - // userEmail = userData.email; - // socketMappings = await getUserSocketMapping(userEmail); - // } - // } catch (sessionError) { - // logger.log(`Failed to fetch session data for socket ${socketId}: ${sessionError}`, "ERROR", "redis"); - // } - // const mappingString = JSON.stringify(socketMappings, null, 2); - // const errorMessage = `Error adding item to the end of the list for socket ${socketId}: ${error}. User: ${userEmail}, Socket Mappings: ${mappingString}`; - // logger.log(errorMessage, "ERROR", "redis"); - // } - // }; - - // Helper function to add an item to the beginning of the Redis list - // const addItemToBeginningOfList = async (socketId, key, newItem) => { - // try { - // await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem)); - // } catch (error) { - // logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis"); - // } - // }; - - // Helper function to clear a list in Redis - // const clearList = async (socketId, key) => { - // try { - // await pubClient.del(`socket:${socketId}:${key}`); - // } catch (error) { - // logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis"); - // } - // }; - - // Add methods to manage room users - // const addUserToRoom = async (room, user) => { - // try { - // await pubClient.sadd(room, JSON.stringify(user)); - // } catch (error) { - // logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis"); - // } - // }; - - // Remove users from room - // const removeUserFromRoom = async (room, user) => { - // try { - // await pubClient.srem(room, JSON.stringify(user)); - // } catch (error) { - // logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis"); - // } - // }; - - // Get Users in room - // const getUsersInRoom = async (room) => { - // try { - // const users = await pubClient.smembers(room); - // return users.map((user) => JSON.parse(user)); - // } catch (error) { - // logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis"); - // } - // }; + const getProviderCache = (ns, field) => getSessionData(`${ns}:provider`, field); const api = { getUserSocketMappingKey, @@ -442,16 +340,9 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => { updateOrInvalidateBodyshopFromRedis, setSessionTransactionData, getSessionTransactionData, - clearSessionTransactionData - // setMultipleSessionData, - // getMultipleSessionData, - // setMultipleFromArraySessionData, - // addItemToEndOfList, - // addItemToBeginningOfList, - // clearList, - // addUserToRoom, - // removeUserFromRoom, - // getUsersInRoom, + clearSessionTransactionData, + setProviderCache, + getProviderCache }; Object.assign(module.exports, api); diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 20c67f414..5e8fdcc8c 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -44,6 +44,12 @@ const redisSocketEvents = ({ socket.bodyshopId = bodyshopId; socket.data = socket.data || {}; socket.data.authToken = token; + + // Used to update legacy sockets + if (socket.handshake?.auth) { + socket.handshake.auth.token = token; + socket.handshake.auth.bodyshopId = bodyshopId; + } await addUserSocketMapping(user.email, socket.id, bodyshopId); next(); } catch (error) { @@ -418,6 +424,38 @@ const redisSocketEvents = ({ 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"); + const data = await RrGetAdvisors({ + socket, + redisHelpers: { setSessionTransactionData, getSessionTransactionData }, + jobid, + params + }); + cb?.(data); + } catch (e) { + RRLogger(socket, "error", `RR get advisors error: ${e.message}`); + cb?.(null); + } + }); + + socket.on("rr-get-parts", async ({ jobid, params }, cb) => { + try { + const { RrGetParts } = require("../rr/rr-lookup"); + const data = await RrGetParts({ + socket, + redisHelpers: { setSessionTransactionData, getSessionTransactionData }, + jobid, + params + }); + cb?.(data); + } catch (e) { + RRLogger(socket, "error", `RR get parts error: ${e.message}`); + cb?.(null); + } + }); }; // Call Handlers