diff --git a/server/routes/rrRoutes.js b/server/routes/rrRoutes.js index bb25ab0fe..e9a2eb4c4 100644 --- a/server/routes/rrRoutes.js +++ b/server/routes/rrRoutes.js @@ -3,6 +3,9 @@ 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: keep parity with /cdk endpoints so UI can flip provider with minimal diff router.use(validateFirebaseIdTokenMiddleware); @@ -23,4 +26,86 @@ router.post("/getvehicles", withUserGraphQLClientMiddleware, async (req, res) => res.status(501).json({ error: "RR getvehicles not implemented yet" }); }); +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({ data }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +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) { + res.status(500).json({ error: e.message }); + } +}); + +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) { + res.status(500).json({ error: e.message }); + } +}); + +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) { + res.status(500).json({ error: e.message }); + } +}); + +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 + }); + res.status(200).json({ data }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +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 + }); + res.status(200).json({ data }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +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 + }); + res.status(200).json({ data }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + module.exports = router; diff --git a/server/rr/rr-customer.js b/server/rr/rr-customer.js new file mode 100644 index 000000000..b511ef507 --- /dev/null +++ b/server/rr/rr-customer.js @@ -0,0 +1,29 @@ +const { MakeRRCall, RRActions } = require("./rr-helpers"); +const { assertRrOk } = require("./rr-error"); +const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers"); + +async function RrCustomerInsert({ socket, redisHelpers, JobData }) { + const body = mapCustomerInsert(JobData); + const data = await MakeRRCall({ + ...RRActions.CreateCustomer, + body, + redisHelpers, + socket, + jobid: JobData.id + }); + return assertRrOk(data, { apiName: "RR Create Customer" }); +} + +async function RrCustomerUpdate({ socket, redisHelpers, JobData, existingCustomer, patch }) { + const body = mapCustomerUpdate(existingCustomer, patch); + const data = await MakeRRCall({ + ...RRActions.UpdateCustomer, // add to RRActions + body, + redisHelpers, + socket, + jobid: JobData.id + }); + return assertRrOk(data, { apiName: "RR Update Customer" }); +} + +module.exports = { RrCustomerInsert, RrCustomerUpdate }; diff --git a/server/rr/rr-error.js b/server/rr/rr-error.js new file mode 100644 index 000000000..9a5d7de9c --- /dev/null +++ b/server/rr/rr-error.js @@ -0,0 +1,26 @@ +class RrApiError extends Error { + 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; + } +} + +// Match Rome/RR envelope once you confirm it; keep this central. +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 }); + } + return data; +} + +module.exports = { RrApiError, assertRrOk }; diff --git a/server/rr/rr-helpers.js b/server/rr/rr-helpers.js index c8f8aa086..d28a7afad 100644 --- a/server/rr/rr-helpers.js +++ b/server/rr/rr-helpers.js @@ -1,185 +1,431 @@ +// server/rr/rr-helpers.js + +/** + * RR (Reynolds & Reynolds) helper module + * - Loads env (.env.{NODE_ENV}) + * - Provides token retrieval + 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 + */ + 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(); -const CreateRRLogEvent = require("./rr-logger"); +const axiosCurlirize = require("axios-curlirize").default; + +const logger = require("../utils/logger"); +const { RrApiError } = require("./rr-error"); + +// Optional curl logging (handy while scaffolding) +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); +}); -// --- ENV / mode const isProduction = process.env.NODE_ENV === "production"; -// --- Public cache keys (mirrors FortellisCacheEnums) -const RRCacheEnums = { - txEnvelope: "txEnvelope", - SubscriptionMeta: "SubscriptionMeta", // keep shape parity with fortellis if you reuse UI/redis - JobData: "JobData", - DMSVid: "DMSVid", // vehicle id result - DMSVeh: "DMSVeh", // vehicle read - DMSVehCustomer: "DMSVehCustomer", - DMSCustList: "DMSCustList", - DMSCust: "DMSCust", - selectedCustomerId: "selectedCustomerId", - DMSTransHeader: "DMSTransHeader", - transWips: "transWips", - DMSBatchTxn: "DMSBatchTxn", - DmsBatchTxnPost: "DmsBatchTxnPost", - DMSVehHistory: "DMSVehHistory" -}; +/** + * 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). + */ +const RR_PROVIDER_TOKEN_BUCKET = "rr:provider-token"; +const RR_PROVIDER_TOKEN_FIELD = "token"; -// --- Transaction namespacing in Redis -const getTransactionType = (jobid) => `rr:${jobid}`; -const defaultRRTTL = 60 * 60; +/** + * Fetch an RR access token. Replace with the real auth call when available. + * @param {Object} deps + * @param {Object} deps.redisHelpers - your redisHelpers api + * @returns {Promise} accessToken + */ +async function getRRToken({ redisHelpers }) { + try { + // Try the cache first + const cached = await redisHelpers.getSessionData(RR_PROVIDER_TOKEN_BUCKET, RR_PROVIDER_TOKEN_FIELD); + if (cached?.accessToken && cached?.expiresAt && Date.now() < cached.expiresAt - 5000) { + return cached.accessToken; + } -// --- API catalog (stub URLs: swap in real ones from Rome specs) -const RRActions = { - SearchCustomer: { - apiName: "RR Search Customer", - url: isProduction ? "https://rr.example.com/api/customer/search" : "https://rr-uat.example.com/api/customer/search", - type: "get" - }, - ReadCustomer: { - apiName: "RR Read Customer", - url: isProduction ? "https://rr.example.com/api/customer/" : "https://rr-uat.example.com/api/customer/", - type: "get" // append /{id} - }, - CreateCustomer: { - apiName: "RR Create Customer", - url: isProduction ? "https://rr.example.com/api/customer" : "https://rr-uat.example.com/api/customer", - type: "post" - }, - InsertVehicle: { - apiName: "RR Insert Vehicle", - url: isProduction ? "https://rr.example.com/api/service-vehicle" : "https://rr-uat.example.com/api/service-vehicle", - type: "post" - }, - ReadVehicle: { - apiName: "RR Read Vehicle", - url: isProduction - ? "https://rr.example.com/api/service-vehicle/" - : "https://rr-uat.example.com/api/service-vehicle/", - type: "get" // append /{vehicleId} - }, - GetVehicleId: { - apiName: "RR Get Vehicle Id By VIN", - url: isProduction - ? "https://rr.example.com/api/service-vehicle/by-vin/" - : "https://rr-uat.example.com/api/service-vehicle/by-vin/", - type: "get" // append /{vin} - }, - StartWip: { - apiName: "RR Start WIP", - url: isProduction ? "https://rr.example.com/api/gl/start-wip" : "https://rr-uat.example.com/api/gl/start-wip", - type: "post" - }, - TranBatchWip: { - apiName: "RR Trans Batch WIP", - url: isProduction - ? "https://rr.example.com/api/gl/trans-batch-wip" - : "https://rr-uat.example.com/api/gl/trans-batch-wip", - type: "post" - }, - PostBatchWip: { - apiName: "RR Post Batch WIP", - url: isProduction - ? "https://rr.example.com/api/gl/post-batch-wip" - : "https://rr-uat.example.com/api/gl/post-batch-wip", - type: "post" - }, - QueryErrorWip: { - apiName: "RR Query Error WIP", - url: isProduction ? "https://rr.example.com/api/gl/error-wip/" : "https://rr-uat.example.com/api/gl/error-wip/", - type: "get" // append /{transId} - }, - ServiceHistoryInsert: { - apiName: "RR Insert Service Vehicle History", - url: isProduction - ? "https://rr.example.com/api/service-vehicle-history" - : "https://rr-uat.example.com/api/service-vehicle-history", - type: "post" + // TODO: Implement real RR auth flow here. + // Stub: use env var or a fixed dev token + const accessToken = process.env.RR_FAKE_TOKEN || "rr-dev-token"; + // Set an artificial 55-minute expiry (adjust to real value) + 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 + }); + // In absolute worst case, return a stub so dev environments keep moving + return process.env.RR_FAKE_TOKEN || "rr-dev-token"; } -}; - -// --- Auth (stub). Replace with RR auth handshake from Rome specs. -async function getRRToken() { - // TODO: implement RR token retrieval (client credentials, basic, or session) per spec - // Return a bearer (or session cookie) string - return process.env.RR_FAKE_TOKEN || "rr-dev-token"; } -// --- URL constructor (same shape as Fortellis) +/** + * Construct a full URL including optional path segment and query params. + * @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 + * @returns {string} + */ function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) { - const base = url.replace(/\/+$/, "/"); - const fullPath = pathParams ? `${base}${pathParams}` : base; - const qs = new URLSearchParams(requestSearchParams).toString(); - return qs ? `${fullPath}?${qs}` : fullPath; + // normalize single trailing slash + url = url.replace(/\/+$/, "/"); + const fullPath = pathParams ? `${url}${pathParams}` : url; + const searchParams = new URLSearchParams(requestSearchParams).toString(); + return searchParams ? `${fullPath}?${searchParams}` : fullPath; } -// --- General caller (same ergonomics as MakeFortellisCall) +/** + * 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 + * @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); + + const statusUrl = delayMeta?._links?.status?.href; + if (!statusUrl) { + return { error: "No status URL provided by RR batch envelope." }; + } + + const statusResult = await axios.get(statusUrl, { + headers: { + Authorization: `Bearer ${access_token}`, + "X-Request-Id": reqId + } + }); + + 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; + } + } + return { error: "Batch result still not complete after max attempts." }; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Core caller. Mirrors Fortellis' MakeFortellisCall shape so you can reuse flow. + * + * @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] added as 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} + */ async function MakeRRCall({ apiName, url, headers = {}, body = {}, type = "post", + debug = true, requestPathParams, requestSearchParams = [], - debug = true, jobid, redisHelpers, socket }) { - const ReqId = uuid(); const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams }); - const access_token = await getRRToken(); + const reqId = uuid(); + const idempotencyKey = uuid(); + + const access_token = await getRRToken({ redisHelpers }); if (debug) { - console.log(`[RR] ${apiName} | ${type.toUpperCase()} ${fullUrl} | ReqId=${ReqId}`); - if (type !== "get") console.log(`[RR] payload: ${JSON.stringify(body, null, 2)}`); + logger.log("rr-call", "DEBUG", socket?.user?.email, null, { + apiName, + type, + url: fullUrl, + jobid, + reqId, + body + }); } try { - const commonHeaders = { + let resp; + const baseHeaders = { Authorization: `Bearer ${access_token}`, - "X-Request-Id": ReqId, + "X-Request-Id": reqId, + "Idempotency-Key": idempotencyKey, ...headers }; - let resp; - switch (type) { + switch ((type || "post").toLowerCase()) { case "get": - resp = await axios.get(fullUrl, { headers: commonHeaders }); + resp = await axios.get(fullUrl, { headers: baseHeaders }); break; case "put": - resp = await axios.put(fullUrl, body, { headers: commonHeaders }); + resp = await axios.put(fullUrl, body, { headers: baseHeaders }); + break; + case "delete": + resp = await axios.delete(fullUrl, { headers: baseHeaders, data: body }); break; case "post": default: - resp = await axios.post(fullUrl, body, { headers: commonHeaders }); + resp = await axios.post(fullUrl, body, { headers: baseHeaders }); break; } - if (debug) console.log(`[RR] ${apiName} OK | ReqId=${ReqId}`); - return resp.data; + if (debug) { + logger.log("rr-response", "DEBUG", socket?.user?.email, null, { + apiName, + reqId, + data: safeLogJson(resp?.data) + }); + } + + // 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; + } + + return resp?.data; } catch (error) { - CreateRRLogEvent(socket, "ERROR", `[RR] ${apiName} failed: ${error.message}`, { - reqId: ReqId, + // 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, + headers, + body, + type, + debug, + requestPathParams, + requestSearchParams, + jobid, + redisHelpers, + socket + }); + } + + const errPayload = { + reqId, url: fullUrl, apiName, - errorData: error.response?.data, - errorStatus: error.response?.status, - errorStatusText: error.response?.statusText, - stack: error.stack + 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 error; + + throw new RrApiError(`RR API call failed for ${apiName}: ${error?.message}`, errPayload); + } +} + +/** + * 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. + */ +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" + }, + + // 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 + : "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" + }, + QueryCustomerByName: { + 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]"; } } module.exports = { - RRActions, MakeRRCall, - RRCacheEnums, - getTransactionType, - defaultRRTTL + RRActions, + getRRToken, + constructFullUrl, + DelayedCallback }; diff --git a/server/rr/rr-lookup.js b/server/rr/rr-lookup.js new file mode 100644 index 000000000..7e0806b9b --- /dev/null +++ b/server/rr/rr-lookup.js @@ -0,0 +1,40 @@ +const { MakeRRCall, RRActions } = require("./rr-helpers"); +const { assertRrOk } = require("./rr-error"); + +async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) { + const data = await MakeRRCall({ + ...RRActions.CombinedSearch, // add to RRActions + requestSearchParams: params, // e.g., [["vin", "XXXX"], ["lastName","DOE"]] + type: "get", + redisHelpers, + socket, + jobid + }); + return assertRrOk(data, { apiName: "RR Combined Search", allowEmpty: true }); +} + +async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) { + const data = await MakeRRCall({ + ...RRActions.GetAdvisors, // add + requestSearchParams: params, + type: "get", + redisHelpers, + socket, + jobid + }); + return assertRrOk(data, { apiName: "RR Get Advisors", allowEmpty: true }); +} + +async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) { + const data = await MakeRRCall({ + ...RRActions.GetParts, // add + requestSearchParams: params, + type: "get", + redisHelpers, + socket, + jobid + }); + return assertRrOk(data, { apiName: "RR Get Parts", allowEmpty: true }); +} + +module.exports = { RrCombinedSearch, RrGetAdvisors, RrGetParts }; diff --git a/server/rr/rr-repair-orders.js b/server/rr/rr-repair-orders.js new file mode 100644 index 000000000..678f16f47 --- /dev/null +++ b/server/rr/rr-repair-orders.js @@ -0,0 +1,29 @@ +const { MakeRRCall, RRActions } = require("./rr-helpers"); +const { assertRrOk } = require("./rr-error"); +const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers"); + +async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { + const body = mapRepairOrderCreate({ JobData, txEnvelope }); + const data = await MakeRRCall({ + ...RRActions.CreateRepairOrder, // add this entry to RRActions (POST /repair-orders) + body, + redisHelpers, + socket, + jobid: JobData.id + }); + return assertRrOk(data, { apiName: "RR Create Repair Order" }); +} + +async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) { + const body = mapRepairOrderUpdate({ JobData, txEnvelope }); + const data = await MakeRRCall({ + ...RRActions.UpdateRepairOrder, // add this entry (PUT /repair-orders/{id}) + body, + redisHelpers, + socket, + jobid: JobData.id + }); + return assertRrOk(data, { apiName: "RR Update Repair Order" }); +} + +module.exports = { CreateRepairOrder, UpdateRepairOrder }; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 26bb8094a..7f89e3794 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -394,6 +394,30 @@ const redisSocketEvents = ({ logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack }); } }); + + socket.on("rr-lookup-combined", async ({ jobid, params }, cb) => { + try { + const { RrCombinedSearch } = require("../rr/rr-lookup"); + const data = await RrCombinedSearch({ + socket, + redisHelpers: { setSessionTransactionData, getSessionTransactionData }, + jobid, + params + }); + cb?.(data); + } catch (e) { + RRLogger(socket, "error", `RR combined lookup error: ${e.message}`); + cb?.(null); + } + }); + + 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 + }); }; // Call Handlers