feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Cache advisors for a week / allow them to refresh said cache
This commit is contained in:
@@ -61,7 +61,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
const getAdvisorNumber = (a) => a?.advisorId;
|
const getAdvisorNumber = (a) => a?.advisorId;
|
||||||
const getAdvisorLabel = (a) => `${a?.firstName} ${a?.lastName}`?.trim();
|
const getAdvisorLabel = (a) => `${a?.firstName} ${a?.lastName}`?.trim();
|
||||||
|
|
||||||
const fetchRrAdvisors = () => {
|
const fetchRrAdvisors = (refresh = false) => {
|
||||||
if (!wsssocket) return;
|
if (!wsssocket) return;
|
||||||
setAdvLoading(true);
|
setAdvLoading(true);
|
||||||
|
|
||||||
@@ -78,8 +78,8 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
|
|
||||||
wsssocket.once("rr-get-advisors:result", onResult);
|
wsssocket.once("rr-get-advisors:result", onResult);
|
||||||
|
|
||||||
// Emit with the correct event name + parse the ack shape
|
// Emit with refresh flag: server will bypass/rebuild cache when true
|
||||||
wsssocket.emit("rr-get-advisors", { departmentType: "B" }, (ack) => {
|
wsssocket.emit("rr-get-advisors", { departmentType: "B", refresh }, (ack) => {
|
||||||
if (ack?.ok) {
|
if (ack?.ok) {
|
||||||
const list = ack.result ?? [];
|
const list = ack.result ?? [];
|
||||||
setAdvisors(Array.isArray(list) ? list : []);
|
setAdvisors(Array.isArray(list) ? list : []);
|
||||||
@@ -92,7 +92,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dms === "rr") fetchRrAdvisors();
|
if (dms === "rr") fetchRrAdvisors(false);
|
||||||
}, [dms, bodyshop?.id]);
|
}, [dms, bodyshop?.id]);
|
||||||
|
|
||||||
const handlePayerSelect = (value, index) => {
|
const handlePayerSelect = (value, index) => {
|
||||||
@@ -217,7 +217,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
|
|
||||||
{dms === "rr" && (
|
{dms === "rr" && (
|
||||||
<Form.Item label=" " colon={false}>
|
<Form.Item label=" " colon={false}>
|
||||||
<Button onClick={fetchRrAdvisors} icon={<ReloadOutlined />} loading={advLoading}>
|
<Button onClick={() => fetchRrAdvisors(true)} icon={<ReloadOutlined />} loading={advLoading}>
|
||||||
{t("general.actions.refresh")}
|
{t("general.actions.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
|||||||
const devDebugLogger = require("./devDebugLogger");
|
const devDebugLogger = require("./devDebugLogger");
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bodyshop cache TTL in seconds
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
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<any|null>}
|
||||||
|
*/
|
||||||
const getSessionData = async (socketId, key) => {
|
const getSessionData = async (socketId, key) => {
|
||||||
try {
|
try {
|
||||||
const data = await pubClient.hget(`socket:${socketId}`, key);
|
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<void>}
|
||||||
|
*/
|
||||||
const setSessionTransactionData = async (socketId, transactionType, key, value, ttl) => {
|
const setSessionTransactionData = async (socketId, transactionType, key, value, ttl) => {
|
||||||
try {
|
try {
|
||||||
await pubClient.hset(getSocketTransactionkey({ socketId, transactionType }), key, JSON.stringify(value)); // Use Redis pubClient
|
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<any|null>}
|
||||||
|
*/
|
||||||
const getSessionTransactionData = async (socketId, transactionType, key) => {
|
const getSessionTransactionData = async (socketId, transactionType, key) => {
|
||||||
try {
|
try {
|
||||||
const data = await pubClient.hget(getSocketTransactionkey({ socketId, transactionType }), key);
|
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<void>}
|
||||||
|
*/
|
||||||
const clearSessionData = async (socketId) => {
|
const clearSessionData = async (socketId) => {
|
||||||
try {
|
try {
|
||||||
await pubClient.del(`socket:${socketId}`);
|
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");
|
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<void>}
|
||||||
|
*/
|
||||||
const clearSessionTransactionData = async (socketId, transactionType) => {
|
const clearSessionTransactionData = async (socketId, transactionType) => {
|
||||||
try {
|
try {
|
||||||
await pubClient.del(getSocketTransactionkey({ socketId, transactionType }));
|
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<void>}
|
||||||
|
*/
|
||||||
const setProviderCache = (ns, field, value, ttl) => setSessionData(`${ns}:provider`, field, value, ttl);
|
const setProviderCache = (ns, field, value, ttl) => setSessionData(`${ns}:provider`, field, value, ttl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider cache data
|
||||||
|
* @param ns
|
||||||
|
* @param field
|
||||||
|
* @returns {Promise<any|null|undefined>}
|
||||||
|
*/
|
||||||
const getProviderCache = (ns, field) => getSessionData(`${ns}:provider`, field);
|
const getProviderCache = (ns, field) => getSessionData(`${ns}:provider`, field);
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ const {
|
|||||||
const { GraphQLClient } = require("graphql-request");
|
const { GraphQLClient } = require("graphql-request");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
|
|
||||||
|
// 1 week TTL for advisors cache
|
||||||
|
const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds
|
||||||
|
|
||||||
// ---------------- utils ----------------
|
// ---------------- utils ----------------
|
||||||
function resolveJobId(explicit, payload, job) {
|
function resolveJobId(explicit, payload, job) {
|
||||||
return explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null;
|
return explicit || payload?.jobId || payload?.jobid || job?.id || job?.jobId || job?.jobid || null;
|
||||||
@@ -69,6 +72,20 @@ async function getBodyshopForSocket({ bodyshopId, socket }) {
|
|||||||
return bodyshop;
|
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)
|
* VIN + Full Name merge (export flow)
|
||||||
*/
|
*/
|
||||||
@@ -114,7 +131,9 @@ async function rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }) {
|
|||||||
blocks,
|
blocks,
|
||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const norm = normalizeCustomerCandidates(res, { ownersSet });
|
const norm = normalizeCustomerCandidates(res, { ownersSet });
|
||||||
@@ -158,17 +177,74 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- Advisors ----------
|
// ---------- Advisors (cached) ----------
|
||||||
socket.on("rr-get-advisors", async (args = {}, ack) => {
|
socket.on("rr-get-advisors", async (args = {}, ack) => {
|
||||||
|
const refresh = !!args?.refresh;
|
||||||
|
const requestedDept = (args?.departmentType || "B").toUpperCase();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||||
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
|
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
|
||||||
CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: begin", { args });
|
CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: begin", { args });
|
||||||
const res = await rrGetAdvisors(bodyshop, args);
|
|
||||||
ack?.({ ok: true, result: res });
|
// Build routing to bind cache key to bodyshop + dealer/store/area
|
||||||
socket.emit("rr-get-advisors:result", res);
|
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", {
|
CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: success", {
|
||||||
count: Array.isArray(res) ? res.length : undefined
|
count: Array.isArray(result) ? result.length : undefined,
|
||||||
|
fromCache
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", { error: err?.message });
|
CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", { error: err?.message });
|
||||||
@@ -255,7 +331,9 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
||||||
} catch {}
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,9 +385,11 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
blocksVin,
|
blocksVin,
|
||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
const ownersSet = ownersFromVinBlocks(blocksVin, job.v_vin);
|
const ownersSet = ownersFromVinBlocks(blocksVin, job.v_vin);
|
||||||
if (ownersSet && ownersSet.size) {
|
if (ownersSet?.size) {
|
||||||
const sel = String(selectedCustNo);
|
const sel = String(selectedCustNo);
|
||||||
if (!ownersSet.has(sel)) {
|
if (!ownersSet.has(sel)) {
|
||||||
const [existingOwner] = Array.from(ownersSet).map(String);
|
const [existingOwner] = Array.from(ownersSet).map(String);
|
||||||
@@ -327,7 +407,9 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
message:
|
message:
|
||||||
"VIN already exists in RR under a different customer. Using the VIN's owner to continue the export."
|
"VIN already exists in RR under a different customer. Using the VIN's owner to continue the export."
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
selectedCustNo = existingOwner;
|
selectedCustNo = existingOwner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,7 +534,9 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
||||||
} catch {}
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
ack?.({ ok: false, error: error.message });
|
ack?.({ ok: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user