diff --git a/client/src/components/dms-post-form/dms-post-form.component.jsx b/client/src/components/dms-post-form/dms-post-form.component.jsx
index 211e56043..6370dccf6 100644
--- a/client/src/components/dms-post-form/dms-post-form.component.jsx
+++ b/client/src/components/dms-post-form/dms-post-form.component.jsx
@@ -61,7 +61,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
const getAdvisorNumber = (a) => a?.advisorId;
const getAdvisorLabel = (a) => `${a?.firstName} ${a?.lastName}`?.trim();
- const fetchRrAdvisors = () => {
+ const fetchRrAdvisors = (refresh = false) => {
if (!wsssocket) return;
setAdvLoading(true);
@@ -78,8 +78,8 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
wsssocket.once("rr-get-advisors:result", onResult);
- // Emit with the correct event name + parse the ack shape
- wsssocket.emit("rr-get-advisors", { departmentType: "B" }, (ack) => {
+ // Emit with refresh flag: server will bypass/rebuild cache when true
+ wsssocket.emit("rr-get-advisors", { departmentType: "B", refresh }, (ack) => {
if (ack?.ok) {
const list = ack.result ?? [];
setAdvisors(Array.isArray(list) ? list : []);
@@ -92,7 +92,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
};
useEffect(() => {
- if (dms === "rr") fetchRrAdvisors();
+ if (dms === "rr") fetchRrAdvisors(false);
}, [dms, bodyshop?.id]);
const handlePayerSelect = (value, index) => {
@@ -217,7 +217,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
{dms === "rr" && (
- } loading={advLoading}>
+
diff --git a/server/utils/redisHelpers.js b/server/utils/redisHelpers.js
index 950d98931..c8336b607 100644
--- a/server/utils/redisHelpers.js
+++ b/server/utils/redisHelpers.js
@@ -2,6 +2,10 @@ const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
const devDebugLogger = require("./devDebugLogger");
const client = require("../graphql-client/graphql-client").client;
+/**
+ * Bodyshop cache TTL in seconds
+ * @type {number}
+ */
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
/**
@@ -63,7 +67,12 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
}
};
- // Retrieve session data from Redis
+ /**
+ * Retrieve session data from Redis
+ * @param socketId
+ * @param key
+ * @returns {Promise}
+ */
const getSessionData = async (socketId, key) => {
try {
const data = await pubClient.hget(`socket:${socketId}`, key);
@@ -73,6 +82,15 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
}
};
+ /**
+ * Store session transaction data in Redis
+ * @param socketId
+ * @param transactionType
+ * @param key
+ * @param value
+ * @param ttl
+ * @returns {Promise}
+ */
const setSessionTransactionData = async (socketId, transactionType, key, value, ttl) => {
try {
await pubClient.hset(getSocketTransactionkey({ socketId, transactionType }), key, JSON.stringify(value)); // Use Redis pubClient
@@ -88,7 +106,13 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
}
};
- // Retrieve session transaction data from Redis
+ /**
+ * Retrieve session transaction data from Redis
+ * @param socketId
+ * @param transactionType
+ * @param key
+ * @returns {Promise}
+ */
const getSessionTransactionData = async (socketId, transactionType, key) => {
try {
const data = await pubClient.hget(getSocketTransactionkey({ socketId, transactionType }), key);
@@ -102,7 +126,11 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
}
};
- // Clear session data from Redis
+ /**
+ * Clear session data from Redis
+ * @param socketId
+ * @returns {Promise}
+ */
const clearSessionData = async (socketId) => {
try {
await pubClient.del(`socket:${socketId}`);
@@ -110,7 +138,13 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
logger.log(`Error Clearing Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
}
};
- // Clear session data from Redis
+
+ /**
+ * Clear session transaction data from Redis
+ * @param socketId
+ * @param transactionType
+ * @returns {Promise}
+ */
const clearSessionTransactionData = async (socketId, transactionType) => {
try {
await pubClient.del(getSocketTransactionkey({ socketId, transactionType }));
@@ -321,8 +355,22 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
}
};
+ /**
+ * Set provider cache data
+ * @param ns
+ * @param field
+ * @param value
+ * @param ttl
+ * @returns {Promise}
+ */
const setProviderCache = (ns, field, value, ttl) => setSessionData(`${ns}:provider`, field, value, ttl);
+ /**
+ * Get provider cache data
+ * @param ns
+ * @param field
+ * @returns {Promise}
+ */
const getProviderCache = (ns, field) => getSessionData(`${ns}:provider`, field);
const api = {
diff --git a/server/web-sockets/rr-register-socket-events.js b/server/web-sockets/rr-register-socket-events.js
index 70f8089b8..81a38543f 100644
--- a/server/web-sockets/rr-register-socket-events.js
+++ b/server/web-sockets/rr-register-socket-events.js
@@ -21,6 +21,9 @@ const {
const { GraphQLClient } = require("graphql-request");
const queries = require("../graphql-client/queries");
+// 1 week TTL for advisors cache
+const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds
+
// ---------------- utils ----------------
function resolveJobId(explicit, payload, job) {
return explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null;
@@ -69,6 +72,20 @@ async function getBodyshopForSocket({ bodyshopId, socket }) {
return bodyshop;
}
+/**
+ * Build advisors cache namespace + field (per bodyshop + routing + department)
+ */
+function buildAdvisorsCacheNS({ bodyshopId, routing, departmentType = "B" }) {
+ const dealer = routing?.dealerNumber || "unknown";
+ const store = routing?.storeNumber || "none";
+ const area = routing?.areaNumber || "none";
+ const dept = (departmentType || "B").toUpperCase();
+ return {
+ ns: `rr:advisors:${bodyshopId}:${dealer}:${store}:${area}`,
+ field: `dept:${dept}`
+ };
+}
+
/**
* VIN + Full Name merge (export flow)
*/
@@ -114,7 +131,9 @@ async function rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }) {
blocks,
defaultRRTTL
);
- } catch {}
+ } catch {
+ //
+ }
}
const norm = normalizeCustomerCandidates(res, { ownersSet });
@@ -158,17 +177,74 @@ function registerRREvents({ socket, redisHelpers }) {
}
});
- // ---------- Advisors ----------
+ // ---------- Advisors (cached) ----------
socket.on("rr-get-advisors", async (args = {}, ack) => {
+ const refresh = !!args?.refresh;
+ const requestedDept = (args?.departmentType || "B").toUpperCase();
+
try {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: begin", { args });
- const res = await rrGetAdvisors(bodyshop, args);
- ack?.({ ok: true, result: res });
- socket.emit("rr-get-advisors:result", res);
+
+ // Build routing to bind cache key to bodyshop + dealer/store/area
+ const { client, opts } = await buildClientAndOpts(bodyshop);
+ const routing = opts?.routing || client?.opts?.routing || {};
+ if (!routing?.dealerNumber) {
+ throw new Error("rr-get-advisors: routing.dealerNumber required");
+ }
+
+ const { ns, field } = buildAdvisorsCacheNS({
+ bodyshopId,
+ routing,
+ departmentType: requestedDept
+ });
+
+ let result = null;
+ let fromCache = false;
+
+ // 1) Try cache (unless forced refresh)
+ if (!refresh) {
+ try {
+ const cached = await redisHelpers.getProviderCache(ns, field);
+ if (cached && Array.isArray(cached)) {
+ result = cached;
+ fromCache = true;
+ CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache hit", {
+ ns,
+ field,
+ count: cached.length,
+ ttl: ADVISORS_CACHE_TTL
+ });
+ }
+ } catch (e) {
+ CreateRRLogEvent(socket, "WARN", "rr-get-advisors: cache read failed", { ns, field, error: e?.message });
+ }
+ }
+
+ // 2) Fetch + cache when no cache or forced refresh
+ if (!result) {
+ const live = await rrGetAdvisors(bodyshop, { departmentType: requestedDept });
+ result = Array.isArray(live) ? live : [];
+ try {
+ await redisHelpers.setProviderCache(ns, field, result, ADVISORS_CACHE_TTL);
+ CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache populated", {
+ ns,
+ field,
+ count: result.length,
+ ttl: ADVISORS_CACHE_TTL
+ });
+ } catch (e) {
+ CreateRRLogEvent(socket, "WARN", "rr-get-advisors: cache write failed", { ns, field, error: e?.message });
+ }
+ }
+
+ // 3) Respond
+ ack?.({ ok: true, result, fromCache });
+ socket.emit("rr-get-advisors:result", { result, fromCache });
CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: success", {
- count: Array.isArray(res) ? res.length : undefined
+ count: Array.isArray(result) ? result.length : undefined,
+ fromCache
});
} catch (err) {
CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", { error: err?.message });
@@ -255,7 +331,9 @@ function registerRREvents({ socket, redisHelpers }) {
});
try {
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
- } catch {}
+ } catch {
+ //
+ }
}
});
@@ -307,9 +385,11 @@ function registerRREvents({ socket, redisHelpers }) {
blocksVin,
defaultRRTTL
);
- } catch {}
+ } catch {
+ //
+ }
const ownersSet = ownersFromVinBlocks(blocksVin, job.v_vin);
- if (ownersSet && ownersSet.size) {
+ if (ownersSet?.size) {
const sel = String(selectedCustNo);
if (!ownersSet.has(sel)) {
const [existingOwner] = Array.from(ownersSet).map(String);
@@ -327,7 +407,9 @@ function registerRREvents({ socket, redisHelpers }) {
message:
"VIN already exists in RR under a different customer. Using the VIN's owner to continue the export."
});
- } catch {}
+ } catch {
+ //
+ }
selectedCustNo = existingOwner;
}
}
@@ -452,7 +534,9 @@ function registerRREvents({ socket, redisHelpers }) {
});
try {
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
- } catch {}
+ } catch {
+ //
+ }
ack?.({ ok: false, error: error.message });
}
});