feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
File diff suppressed because one or more lines are too long
1
server/rr/lib/index.mjs
Normal file
1
server/rr/lib/index.mjs
Normal file
File diff suppressed because one or more lines are too long
3
server/rr/lib/types.cjs
Normal file
3
server/rr/lib/types.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
// CJS access point for JSDoc typedefs
|
||||
module.exports = require('./types.js');
|
||||
43
server/rr/rr-logger-event.js
Normal file
43
server/rr/rr-logger-event.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// File: server/rr/rr-logger-event.js
|
||||
// Fortellis-style log helper for RR flows.
|
||||
// Usage: CreateRRLogEvent(socket, "DEBUG"|"INFO"|"WARN"|"ERROR", message, details?)
|
||||
|
||||
const RRLogger = require("../rr/rr-logger");
|
||||
|
||||
// Normalize level to upper + provide console + socket event with coherent payload
|
||||
function CreateRRLogEvent(socket, level = "DEBUG", message = "", details = {}) {
|
||||
const lvl = String(level || "DEBUG").toUpperCase();
|
||||
const ts = Date.now();
|
||||
|
||||
// Console (uses existing RRLogger, which also emits "RR:LOG" to sockets for live tail)
|
||||
try {
|
||||
const log = RRLogger(socket);
|
||||
const fn = lvl === "ERROR" ? "error" : lvl === "WARN" ? "warn" : lvl === "INFO" ? "info" : "debug";
|
||||
log(fn, message, { ts, ...safeJson(details) });
|
||||
} catch {
|
||||
/* ignore console/log failures */
|
||||
}
|
||||
|
||||
// Structured RR event for FE debug panel (parity with Fortellis' CreateFortellisLogEvent)
|
||||
try {
|
||||
socket?.emit?.("rr-log-event", {
|
||||
level: lvl,
|
||||
message,
|
||||
ts,
|
||||
...safeJson(details)
|
||||
});
|
||||
} catch {
|
||||
/* ignore socket emit failures */
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort ensure details are JSON-safe (avoid circular / BigInt)
|
||||
function safeJson(obj) {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj ?? {}));
|
||||
} catch {
|
||||
return { _unsafe: true };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CreateRRLogEvent;
|
||||
@@ -1,66 +1,56 @@
|
||||
const util = require("util");
|
||||
const appLogger = require("../utils/logger");
|
||||
// File: server/rr/rr-logger.js
|
||||
// Console logger for RR flows with safe JSON. No socket emission by default.
|
||||
|
||||
function RRLogger(socket, baseCtx = {}) {
|
||||
const levels = new Set(["error", "warn", "info", "http", "verbose", "debug", "silly"]);
|
||||
const baseLogger = require("../utils/logger");
|
||||
|
||||
const safeString = (v) => {
|
||||
if (v instanceof Error) return v.message;
|
||||
if (typeof v === "string") return v;
|
||||
function RRLogger(_socket, defaults = {}) {
|
||||
function safeSerialize(value) {
|
||||
try {
|
||||
return JSON.stringify(v);
|
||||
} catch {
|
||||
return util.inspect(v, { depth: 2, maxArrayLength: 50 });
|
||||
}
|
||||
};
|
||||
|
||||
return function log(levelOrMsg, msgOrCtx, ctx) {
|
||||
let level = "info";
|
||||
let message = undefined;
|
||||
let meta = {};
|
||||
|
||||
if (typeof levelOrMsg === "string" && levels.has(levelOrMsg)) {
|
||||
level = levelOrMsg;
|
||||
message = msgOrCtx;
|
||||
meta = ctx || {};
|
||||
} else {
|
||||
message = levelOrMsg;
|
||||
meta = msgOrCtx || {};
|
||||
}
|
||||
|
||||
// Prepare console line + metadata
|
||||
const emitError = message instanceof Error;
|
||||
if (emitError) {
|
||||
meta.err = {
|
||||
name: message.name,
|
||||
message: message.message,
|
||||
stack: message.stack
|
||||
};
|
||||
message = message.message;
|
||||
if (level === "info") level = "error";
|
||||
}
|
||||
|
||||
const messageString = safeString(message);
|
||||
const line = `[RR] ${new Date().toISOString()} [${String(level).toUpperCase()}] ${messageString}`;
|
||||
const loggerFn = appLogger?.logger?.[level] || appLogger?.logger?.info || ((...args) => console.log(...args));
|
||||
|
||||
loggerFn(line, { ...baseCtx, ...meta });
|
||||
|
||||
// Always emit a STRING for `message` to sockets to avoid React crashes
|
||||
// If the original message was an object, include it in `details`
|
||||
const details = message && typeof message !== "string" && !emitError ? message : undefined;
|
||||
|
||||
try {
|
||||
socket?.emit?.("rr-log-event", {
|
||||
level,
|
||||
message: messageString, // <-- normalized string for UI
|
||||
ctx: { ...baseCtx, ...meta },
|
||||
...(details ? { details } : {}),
|
||||
ts: Date.now()
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(value, (key, val) => {
|
||||
if (typeof val === "bigint") return val.toString();
|
||||
if (val instanceof Error) return { name: val.name, message: val.message, stack: val.stack };
|
||||
if (typeof val === "function") return undefined;
|
||||
if (typeof val === "object" && val !== null) {
|
||||
if (seen.has(val)) return "[Circular]";
|
||||
seen.add(val);
|
||||
if (val instanceof Date) return val.toISOString();
|
||||
if (val instanceof Map) return Object.fromEntries(val);
|
||||
if (val instanceof Set) return Array.from(val);
|
||||
if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(val)) return `<Buffer len=${val.length}>`;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
} catch {
|
||||
/* ignore socket emission errors */
|
||||
try {
|
||||
return String(value);
|
||||
} catch {
|
||||
return "[Unserializable]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function log(level = "info", message = "", ctx = {}) {
|
||||
const lvl = String(level || "info").toLowerCase();
|
||||
const iso = new Date().toISOString();
|
||||
|
||||
const msgStr = typeof message === "string" ? message : safeSerialize(message);
|
||||
const mergedCtx = ctx && typeof ctx === "object" ? { ...defaults, ...ctx } : { ...defaults };
|
||||
|
||||
const ctxStr = Object.keys(mergedCtx || {}).length ? ` ${safeSerialize(mergedCtx)}` : "";
|
||||
const line = `[RR] ${iso} [${lvl.toUpperCase()}] ${msgStr}${ctxStr}`;
|
||||
|
||||
const logger = baseLogger?.logger || baseLogger || console;
|
||||
const fn = (logger[lvl] || logger.log || logger.info || console[lvl] || console.log).bind(logger);
|
||||
|
||||
try {
|
||||
fn(line);
|
||||
} catch {
|
||||
try {
|
||||
console.log(line);
|
||||
} catch {}
|
||||
}
|
||||
// INTENTIONALLY no socket emit here to avoid FE duplicates.
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,76 +41,90 @@ function buildClientAndOpts(bodyshop) {
|
||||
*/
|
||||
function toCombinedSearchPayload(args = {}) {
|
||||
const q = { ...args };
|
||||
let kind = (q.kind || "").toString().trim().toLowerCase();
|
||||
|
||||
// Decide kind if not provided
|
||||
let kind = (q.kind || "").toString().trim();
|
||||
if (!kind) {
|
||||
if (q.phone) kind = "phone";
|
||||
else if (q.license) kind = "license";
|
||||
else if (q.vin) kind = "vin";
|
||||
else if (q.nameRecId || q.custId) kind = "namerecid";
|
||||
else if (q.name && (q.name.fname || q.name.lname || q.name.mname || q.name.name)) kind = "name";
|
||||
else if (q.stkNo || q.stock) kind = "stkno";
|
||||
else if (q.nameRecId || q.custId) kind = "nameRecId";
|
||||
else if (typeof q.name === "string" || (q.name && (q.name.fname || q.name.lname || q.name.mname || q.name.name))) {
|
||||
kind = "name";
|
||||
} else if (q.stkNo || q.stock) kind = "stkNo";
|
||||
}
|
||||
|
||||
// Map loose aliases into the RR builder’s expected fields
|
||||
const payload = { maxResults: q.maxResults || q.maxRecs || 50, kind };
|
||||
const payload = {
|
||||
maxResults: q.maxResults || q.maxRecs || 50,
|
||||
kind
|
||||
};
|
||||
|
||||
switch (kind) {
|
||||
switch ((kind || "").toLowerCase()) {
|
||||
case "phone":
|
||||
payload.kind = "phone";
|
||||
payload.phone = q.phone;
|
||||
payload.phone = String(q.phone ?? "").trim();
|
||||
break;
|
||||
|
||||
case "license":
|
||||
payload.kind = "license";
|
||||
payload.license = q.license;
|
||||
payload.license = String(q.license ?? "").trim();
|
||||
break;
|
||||
|
||||
case "vin":
|
||||
payload.kind = "vin";
|
||||
payload.vin = q.vin;
|
||||
payload.vin = String(q.vin ?? "").trim();
|
||||
break;
|
||||
case "namerecid":
|
||||
payload.kind = "nameRecId";
|
||||
payload.nameRecId = q.nameRecId || q.custId;
|
||||
payload.nameRecId = String(q.nameRecId ?? q.custId ?? "").trim();
|
||||
break;
|
||||
|
||||
case "stkno":
|
||||
case "stkNo":
|
||||
payload.kind = "stkNo";
|
||||
payload.stkNo = String(q.stkNo ?? q.stock ?? "").trim();
|
||||
break;
|
||||
case "name": {
|
||||
payload.kind = "name";
|
||||
const name = q.name;
|
||||
if (name.name) {
|
||||
payload.name = { name: name.name }; // For LName
|
||||
} else if (name.fname && name.lname) {
|
||||
payload.name = {
|
||||
fname: name.fname,
|
||||
lname: name.lname,
|
||||
...(name.mname ? { mname: name.mname } : {})
|
||||
}; // For FullName
|
||||
} else if (name.lname) {
|
||||
payload.name = { name: name.lname }; // Fallback to LName if only lname
|
||||
} else {
|
||||
// Invalid; but to handle gracefully, perhaps throw or skip
|
||||
const n = q.name;
|
||||
// STRING => last-name-only intent
|
||||
if (typeof n === "string") {
|
||||
const last = n.trim();
|
||||
if (last) payload.name = { name: last }; // <LName Name="..."/>
|
||||
break;
|
||||
}
|
||||
// OBJECT => always treat as FullName (even if only one of the parts is present)
|
||||
const fname = n?.fname && String(n.fname).trim();
|
||||
const lname = n?.lname && String(n.lname).trim();
|
||||
const mname = n?.mname && String(n.mname).trim();
|
||||
const lastOnly = n?.name && String(n.name).trim();
|
||||
|
||||
if (fname || lname || mname) {
|
||||
const full = {};
|
||||
if (fname) full.fname = fname;
|
||||
if (mname) full.mname = mname;
|
||||
if (lname) full.lname = lname;
|
||||
payload.name = full; // will render <FullName .../>
|
||||
} else if (lastOnly) {
|
||||
payload.name = { name: lastOnly }; // explicit last-only
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "stkno":
|
||||
payload.kind = "stkNo";
|
||||
payload.stkNo = q.stkNo || q.stock;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Let the RR builder throw the canonical “Unsupported CombinedSearch kind”
|
||||
payload.kind = q.kind; // may be undefined; RR lib will validate
|
||||
payload.kind = kind;
|
||||
}
|
||||
|
||||
// Add compatible secondary fields for combinations
|
||||
if (q.vin && kind !== "vin") payload.vin = q.vin;
|
||||
if (q.phone && kind !== "phone") payload.phone = q.phone;
|
||||
if (q.license && kind !== "license") payload.license = q.license;
|
||||
|
||||
// Optional vehicle narrowing; the RR builder defaults to ANY/ANY/ANY if omitted
|
||||
if (q.make || q.model || q.year) {
|
||||
payload.make = q.make || "ANY";
|
||||
payload.model = q.model || "ANY";
|
||||
payload.year = q.year || "ANY";
|
||||
}
|
||||
|
||||
if (q.vin && payload.kind !== "vin") payload.vin = String(q.vin).trim();
|
||||
if (q.phone && payload.kind !== "phone") payload.phone = String(q.phone).trim();
|
||||
if (q.license && payload.kind !== "license") payload.license = String(q.license).trim();
|
||||
|
||||
return payload;
|
||||
}
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user