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 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(() => {
|
||||
document.title = t("titles.dms", {
|
||||
app: InstanceRenderManager({
|
||||
@@ -134,17 +170,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
|
||||
// RR channels (over wss)
|
||||
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("export-success", handleExportSuccess);
|
||||
wsssocket.on("export-failed", handleExportFailed);
|
||||
|
||||
return () => {
|
||||
wsssocket.off("connect", handleConnect);
|
||||
wsssocket.off("reconnect", handleReconnect);
|
||||
wsssocket.off("connect_error", handleConnectError);
|
||||
|
||||
wsssocket.off("rr-log-event", handleLogEvent);
|
||||
wsssocket.off("export-success", handleExportSuccess);
|
||||
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)
|
||||
wsssocket.on("fortellis-log-event", handleLogEvent);
|
||||
wsssocket.on("export-success", handleExportSuccess);
|
||||
wsssocket.on("export-failed", handleExportFailed);
|
||||
|
||||
return () => {
|
||||
wsssocket.off("fortellis-log-event", handleLogEvent);
|
||||
wsssocket.off("export-success", handleExportSuccess);
|
||||
wsssocket.off("export-failed", handleExportFailed);
|
||||
};
|
||||
} else {
|
||||
// CDK/PBS via legacy /ws socket
|
||||
socket.on("export-failed", handleExportFailed);
|
||||
|
||||
socket.on("connect", () => socket.emit("set-log-level", logLevel));
|
||||
socket.on("reconnect", () => {
|
||||
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
|
||||
// RR events aligned to Fortellis flow with Fortellis-style logging via CreateRRLogEvent
|
||||
|
||||
const CreateRRLogEvent = require("../rr/rr-logger-event");
|
||||
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("../rr/rr-lookup");
|
||||
const { QueryJobData } = require("../rr/rr-job-helpers");
|
||||
const { exportJobToRR } = require("../rr/rr-job-export");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
|
||||
const { QueryJobData } = require("./rr-job-helpers");
|
||||
const { exportJobToRR } = require("./rr-job-export");
|
||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
const { createRRCustomer } = require("../rr/rr-customers");
|
||||
const { ensureRRServiceVehicle } = require("../rr/rr-service-vehicles");
|
||||
const { createRRCustomer } = require("./rr-customers");
|
||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||
const { classifyRRVendorError } = require("./rr-errors");
|
||||
|
||||
const {
|
||||
makeVehicleSearchPayloadFromJob,
|
||||
ownersFromVinBlocks,
|
||||
@@ -16,24 +15,44 @@ const {
|
||||
normalizeCustomerCandidates,
|
||||
defaultRRTTL,
|
||||
RRCacheEnums
|
||||
} = require("../rr/rr-utils");
|
||||
} = require("./rr-utils");
|
||||
|
||||
const { GraphQLClient } = require("graphql-request");
|
||||
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
|
||||
|
||||
// ---------------- utils ----------------
|
||||
/**
|
||||
* Resolve job ID from various shapes
|
||||
* @param explicit
|
||||
* @param payload
|
||||
* @param job
|
||||
* @returns {*|null}
|
||||
*/
|
||||
function resolveJobId(explicit, payload, job) {
|
||||
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 }) {
|
||||
// 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) {
|
||||
return list
|
||||
.map((v, i) => ({ v, i }))
|
||||
@@ -73,6 +92,12 @@ function mergeByCustNo(items = []) {
|
||||
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) {
|
||||
let sess = null;
|
||||
try {
|
||||
@@ -86,6 +111,12 @@ async function getSessionOrSocket(redisHelpers, socket) {
|
||||
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 }) {
|
||||
const endpoint = process.env.GRAPHQL_ENDPOINT;
|
||||
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 });
|
||||
ack?.({ ok: true, result });
|
||||
} 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", {
|
||||
vendor: "rr",
|
||||
jobId: rid,
|
||||
roStatus: result?.roStatus,
|
||||
error: result?.error || "RR export failed"
|
||||
error: result?.error || cls.friendlyMessage || "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(
|
||||
@@ -541,17 +593,31 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
);
|
||||
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId, result });
|
||||
} catch (error) {
|
||||
const cls = classifyRRVendorError(error);
|
||||
|
||||
CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
});
|
||||
|
||||
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 {
|
||||
//
|
||||
/* 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 { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
||||
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 }) => {
|
||||
// Destructure helpers locally, but keep full objects available for downstream modules
|
||||
|
||||
Reference in New Issue
Block a user