feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration -Checkpoint

This commit is contained in:
Dave
2025-11-10 12:20:30 -05:00
parent d45d557a81
commit 02eb212758
4 changed files with 182 additions and 24 deletions

48
server/rr/rr-errors.js Normal file
View 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 couldnt complete the request with Reynolds. Please try again or contact support if this persists.",
severity: "error",
canRetry: true
};
}
module.exports = { classifyRRVendorError };

View File

@@ -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 });
}
});

View File

@@ -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