feature/Reynolds-and-Reynolds-DMS-API-Integration -Expand

This commit is contained in:
Dave
2025-10-01 14:38:59 -04:00
parent d0eeb7d55d
commit 99b79126c3
7 changed files with 605 additions and 126 deletions

View File

@@ -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;

29
server/rr/rr-customer.js Normal file
View File

@@ -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 };

26
server/rr/rr-error.js Normal file
View File

@@ -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 };

View File

@@ -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<string>} accessToken
*/
async function getRRToken({ redisHelpers }) {
try {
// Try the cache first
const cached = await redisHelpers.getSessionData(RR_PROVIDER_TOKEN_BUCKET, RR_PROVIDER_TOKEN_FIELD);
if (cached?.accessToken && cached?.expiresAt && Date.now() < cached.expiresAt - 5000) {
return cached.accessToken;
}
// --- 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<any>}
*/
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<any>}
*/
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
};

40
server/rr/rr-lookup.js Normal file
View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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