From 9ce022b5e86c9e6cc1a683abff9cede36a9fbfcc Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 7 Nov 2025 12:26:25 -0500 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Cache advisors for a week / allow them to refresh said cache --- .../dms-post-form/dms-post-form.component.jsx | 10 +- server/utils/redisHelpers.js | 56 ++++++++- .../web-sockets/rr-register-socket-events.js | 106 ++++++++++++++++-- 3 files changed, 152 insertions(+), 20 deletions(-) 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" && ( - 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 }); } });