feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration -Checkpoint
This commit is contained in:
@@ -79,6 +79,42 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
});
|
});
|
||||||
const logsRef = useRef(null);
|
const logsRef = useRef(null);
|
||||||
|
|
||||||
|
const handleExportFailed = (payload = {}) => {
|
||||||
|
const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload;
|
||||||
|
|
||||||
|
// Prefer server-provided nice text; otherwise generic fallback
|
||||||
|
const msg =
|
||||||
|
friendlyMessage ||
|
||||||
|
error ||
|
||||||
|
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
|
||||||
|
|
||||||
|
// Title defaults by DMS
|
||||||
|
const vendorTitle = title || (dms === "rr" ? "Reynolds" : "DMS");
|
||||||
|
|
||||||
|
// Severity: warn for known soft-stops like 507, else error
|
||||||
|
const sev =
|
||||||
|
severity || (vendorStatusCode === 507 || (errorCode || "").includes("MAX_OPEN_ROS") ? "warning" : "error");
|
||||||
|
|
||||||
|
const notifyKind = sev === "warning" && typeof notification.warning === "function" ? "warning" : "error";
|
||||||
|
|
||||||
|
notification[notifyKind]({
|
||||||
|
message: vendorTitle,
|
||||||
|
description: msg,
|
||||||
|
duration: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mirror to the on-screen log card
|
||||||
|
setLogs((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
timestamp: new Date(),
|
||||||
|
level: (sev || "error").toUpperCase(),
|
||||||
|
message: `${vendorTitle}: ${msg}`,
|
||||||
|
meta: { errorCode, vendorStatusCode, raw: payload }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t("titles.dms", {
|
document.title = t("titles.dms", {
|
||||||
app: InstanceRenderManager({
|
app: InstanceRenderManager({
|
||||||
@@ -134,17 +170,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
|
|
||||||
// RR channels (over wss)
|
// RR channels (over wss)
|
||||||
wsssocket.on("rr-log-event", handleLogEvent);
|
wsssocket.on("rr-log-event", handleLogEvent);
|
||||||
wsssocket.on("RR:LOG", handleLogEvent);
|
|
||||||
wsssocket.on("export-success", handleExportSuccess);
|
|
||||||
wsssocket.on("rr-export-job:result", handleRrExportResult);
|
wsssocket.on("rr-export-job:result", handleRrExportResult);
|
||||||
|
|
||||||
|
wsssocket.on("export-success", handleExportSuccess);
|
||||||
|
wsssocket.on("export-failed", handleExportFailed);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
wsssocket.off("connect", handleConnect);
|
wsssocket.off("connect", handleConnect);
|
||||||
wsssocket.off("reconnect", handleReconnect);
|
wsssocket.off("reconnect", handleReconnect);
|
||||||
wsssocket.off("connect_error", handleConnectError);
|
wsssocket.off("connect_error", handleConnectError);
|
||||||
|
|
||||||
wsssocket.off("rr-log-event", handleLogEvent);
|
wsssocket.off("rr-log-event", handleLogEvent);
|
||||||
wsssocket.off("export-success", handleExportSuccess);
|
|
||||||
wsssocket.off("rr-export-job:result", handleRrExportResult);
|
wsssocket.off("rr-export-job:result", handleRrExportResult);
|
||||||
|
|
||||||
|
wsssocket.off("export-success", handleExportSuccess);
|
||||||
|
wsssocket.off("export-failed", handleExportFailed);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,13 +206,17 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
// Fortellis logs (wss)
|
// Fortellis logs (wss)
|
||||||
wsssocket.on("fortellis-log-event", handleLogEvent);
|
wsssocket.on("fortellis-log-event", handleLogEvent);
|
||||||
wsssocket.on("export-success", handleExportSuccess);
|
wsssocket.on("export-success", handleExportSuccess);
|
||||||
|
wsssocket.on("export-failed", handleExportFailed);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
wsssocket.off("fortellis-log-event", handleLogEvent);
|
wsssocket.off("fortellis-log-event", handleLogEvent);
|
||||||
wsssocket.off("export-success", handleExportSuccess);
|
wsssocket.off("export-success", handleExportSuccess);
|
||||||
|
wsssocket.off("export-failed", handleExportFailed);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// CDK/PBS via legacy /ws socket
|
// CDK/PBS via legacy /ws socket
|
||||||
|
socket.on("export-failed", handleExportFailed);
|
||||||
|
|
||||||
socket.on("connect", () => socket.emit("set-log-level", logLevel));
|
socket.on("connect", () => socket.emit("set-log-level", logLevel));
|
||||||
socket.on("reconnect", () => {
|
socket.on("reconnect", () => {
|
||||||
setLogs((prev) => [
|
setLogs((prev) => [
|
||||||
|
|||||||
48
server/rr/rr-errors.js
Normal file
48
server/rr/rr-errors.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Map RR vendor/status failures into user-friendly messages the FE can show.
|
||||||
|
|
||||||
|
function parseVendorStatusCode(err) {
|
||||||
|
// Prefer explicit numeric props when available
|
||||||
|
const codeProp = err?.code ?? err?.statusCode ?? err?.meta?.status?.StatusCode ?? err?.status?.StatusCode;
|
||||||
|
const num = Number(codeProp);
|
||||||
|
if (!Number.isNaN(num) && num > 0) return num;
|
||||||
|
|
||||||
|
// Fallback: parse from message text (e.g., "... 507 CANNOT EXCEED ...")
|
||||||
|
const m = String(err?.message || "").match(/\b(\d{3})\b/);
|
||||||
|
return m ? Number(m[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify RR vendor errors into a small set of stable codes/messages for the FE.
|
||||||
|
* @param {any} err
|
||||||
|
* @returns {{vendorStatusCode:number|null, errorCode:string, title:string, friendlyMessage:string, severity:'info'|'warning'|'error', canRetry:boolean}}
|
||||||
|
*/
|
||||||
|
function classifyRRVendorError(err) {
|
||||||
|
const code = parseVendorStatusCode(err);
|
||||||
|
const rawMsg = String(err?.meta?.status?.Message || err?.status?.Message || err?.message || "").toUpperCase();
|
||||||
|
|
||||||
|
// Open RO cap exceeded
|
||||||
|
if (code === 507 || /MAXIMUM NUMBER OF ROS/.test(rawMsg)) {
|
||||||
|
return {
|
||||||
|
vendorStatusCode: 507,
|
||||||
|
errorCode: "RR_MAX_OPEN_ROS",
|
||||||
|
title: "Customer Open RO limit reached",
|
||||||
|
friendlyMessage:
|
||||||
|
"Reynolds has reached the maximum number of open Repair Orders for this Customer. Close or finalize an RO in Reynolds, then try again.",
|
||||||
|
severity: "warning",
|
||||||
|
canRetry: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
return {
|
||||||
|
vendorStatusCode: code ?? null,
|
||||||
|
errorCode: "RR_VENDOR_ERROR",
|
||||||
|
title: "Reynolds request failed",
|
||||||
|
friendlyMessage:
|
||||||
|
"We couldn’t complete the request with Reynolds. Please try again or contact support if this persists.",
|
||||||
|
severity: "error",
|
||||||
|
canRetry: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { classifyRRVendorError };
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
// File: server/web-sockets/rr-register-socket-events.js
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
// RR events aligned to Fortellis flow with Fortellis-style logging via CreateRRLogEvent
|
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
|
||||||
|
const { QueryJobData } = require("./rr-job-helpers");
|
||||||
const CreateRRLogEvent = require("../rr/rr-logger-event");
|
const { exportJobToRR } = require("./rr-job-export");
|
||||||
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("../rr/rr-lookup");
|
|
||||||
const { QueryJobData } = require("../rr/rr-job-helpers");
|
|
||||||
const { exportJobToRR } = require("../rr/rr-job-export");
|
|
||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
const { createRRCustomer } = require("../rr/rr-customers");
|
const { createRRCustomer } = require("./rr-customers");
|
||||||
const { ensureRRServiceVehicle } = require("../rr/rr-service-vehicles");
|
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||||
|
const { classifyRRVendorError } = require("./rr-errors");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
makeVehicleSearchPayloadFromJob,
|
makeVehicleSearchPayloadFromJob,
|
||||||
ownersFromVinBlocks,
|
ownersFromVinBlocks,
|
||||||
@@ -16,24 +15,44 @@ const {
|
|||||||
normalizeCustomerCandidates,
|
normalizeCustomerCandidates,
|
||||||
defaultRRTTL,
|
defaultRRTTL,
|
||||||
RRCacheEnums
|
RRCacheEnums
|
||||||
} = require("../rr/rr-utils");
|
} = require("./rr-utils");
|
||||||
|
|
||||||
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
|
/**
|
||||||
|
* Advisors cache TTL (7 days)
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds
|
const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds
|
||||||
|
|
||||||
// ---------------- utils ----------------
|
/**
|
||||||
|
* Resolve job ID from various shapes
|
||||||
|
* @param explicit
|
||||||
|
* @param payload
|
||||||
|
* @param job
|
||||||
|
* @returns {*|null}
|
||||||
|
*/
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve VIN from tx/job shapes
|
||||||
|
* @param tx
|
||||||
|
* @param job
|
||||||
|
* @returns {*|null}
|
||||||
|
*/
|
||||||
function resolveVin({ tx, job }) {
|
function resolveVin({ tx, job }) {
|
||||||
// Prefer cached tx vin (if we made one), then common job shapes (v_vin for our schema)
|
// Prefer cached tx vin (if we made one), then common job shapes (v_vin for our schema)
|
||||||
return tx?.jobData?.vin || job?.v_vin || job?.vehicle?.vin || job?.vin || job?.vehicleVin || null;
|
return tx?.jobData?.vin || job?.v_vin || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort vehicle owners first in the list, preserving original order otherwise.
|
||||||
|
* @param list
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
function sortVehicleOwnerFirst(list) {
|
function sortVehicleOwnerFirst(list) {
|
||||||
return list
|
return list
|
||||||
.map((v, i) => ({ v, i }))
|
.map((v, i) => ({ v, i }))
|
||||||
@@ -73,6 +92,12 @@ function mergeByCustNo(items = []) {
|
|||||||
return Array.from(byId.values());
|
return Array.from(byId.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session data or socket fallback
|
||||||
|
* @param redisHelpers
|
||||||
|
* @param socket
|
||||||
|
* @returns {Promise<{bodyshopId: *, email: *, sess: null}>}
|
||||||
|
*/
|
||||||
async function getSessionOrSocket(redisHelpers, socket) {
|
async function getSessionOrSocket(redisHelpers, socket) {
|
||||||
let sess = null;
|
let sess = null;
|
||||||
try {
|
try {
|
||||||
@@ -86,6 +111,12 @@ async function getSessionOrSocket(redisHelpers, socket) {
|
|||||||
return { bodyshopId, email, sess };
|
return { bodyshopId, email, sess };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch bodyshop data for socket
|
||||||
|
* @param bodyshopId
|
||||||
|
* @param socket
|
||||||
|
* @returns {Promise<{id: string, intellipay_config: {payment_map: {amex: string}}}|{id: string, intellipay_config: null}|*>}
|
||||||
|
*/
|
||||||
async function getBodyshopForSocket({ bodyshopId, socket }) {
|
async function getBodyshopForSocket({ bodyshopId, socket }) {
|
||||||
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
||||||
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured");
|
if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured");
|
||||||
@@ -522,14 +553,35 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: result.roStatus });
|
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: result.roStatus });
|
||||||
ack?.({ ok: true, result });
|
ack?.({ ok: true, result });
|
||||||
} else {
|
} else {
|
||||||
CreateRRLogEvent(socket, "ERROR", `Export failed`, { roStatus: result?.roStatus, error: result?.error });
|
// NEW: classify vendor status for a friendly FE message
|
||||||
|
const vendorStatusCode = Number(
|
||||||
|
result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode
|
||||||
|
);
|
||||||
|
const cls = classifyRRVendorError({
|
||||||
|
code: vendorStatusCode,
|
||||||
|
message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed"
|
||||||
|
});
|
||||||
|
|
||||||
|
CreateRRLogEvent(socket, "ERROR", `Export failed`, {
|
||||||
|
roStatus: result?.roStatus,
|
||||||
|
classification: cls
|
||||||
|
});
|
||||||
|
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
vendor: "rr",
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
roStatus: result?.roStatus,
|
error: result?.error || cls.friendlyMessage || "RR export failed",
|
||||||
error: result?.error || "RR export failed"
|
...cls
|
||||||
|
});
|
||||||
|
// Optional: a user-focused channel if you want to show inline banners
|
||||||
|
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
||||||
|
|
||||||
|
ack?.({
|
||||||
|
ok: false,
|
||||||
|
error: cls.friendlyMessage || result?.error || "RR export failed",
|
||||||
|
result,
|
||||||
|
classification: cls
|
||||||
});
|
});
|
||||||
ack?.({ ok: false, error: result?.error || "RR export failed", result });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
await redisHelpers.setSessionTransactionData(
|
||||||
@@ -541,17 +593,31 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
);
|
);
|
||||||
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId, result });
|
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId, result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const cls = classifyRRVendorError(error);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, {
|
CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
|
vendorStatusCode: cls.vendorStatusCode,
|
||||||
|
code: cls.errorCode,
|
||||||
|
friendly: cls.friendlyMessage,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
jobid: rid
|
jobid: rid
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
socket.emit("export-failed", {
|
||||||
|
vendor: "rr",
|
||||||
|
jobId: rid,
|
||||||
|
error: error.message,
|
||||||
|
...cls
|
||||||
|
});
|
||||||
|
// Optional UX hook for inline banners/toasts
|
||||||
|
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
||||||
} catch {
|
} catch {
|
||||||
//
|
/* ignore */
|
||||||
}
|
}
|
||||||
ack?.({ ok: false, error: error.message });
|
|
||||||
|
ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ const { admin } = require("../firebase/firebase-handler");
|
|||||||
const FortellisLogger = require("../fortellis/fortellis-logger");
|
const FortellisLogger = require("../fortellis/fortellis-logger");
|
||||||
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
const registerRREvents = require("./rr-register-socket-events");
|
const registerRREvents = require("../rr/rr-register-socket-events");
|
||||||
|
|
||||||
const redisSocketEvents = ({ io, redisHelpers, ioHelpers, logger }) => {
|
const redisSocketEvents = ({ io, redisHelpers, ioHelpers, logger }) => {
|
||||||
// Destructure helpers locally, but keep full objects available for downstream modules
|
// Destructure helpers locally, but keep full objects available for downstream modules
|
||||||
|
|||||||
Reference in New Issue
Block a user