feature/Reynolds-and-Reynolds-DMS-API-Integration -Expand
This commit is contained in:
@@ -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
29
server/rr/rr-customer.js
Normal 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
26
server/rr/rr-error.js
Normal 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 };
|
||||
@@ -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
40
server/rr/rr-lookup.js
Normal 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 };
|
||||
29
server/rr/rr-repair-orders.js
Normal file
29
server/rr/rr-repair-orders.js
Normal 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 };
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user