feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Enhance logging

This commit is contained in:
Dave
2025-11-13 15:39:29 -05:00
parent 9c2c0b665d
commit 4c250f6189
9 changed files with 98 additions and 128 deletions

View File

@@ -1,6 +1,6 @@
const { RRClient } = require("./lib/index.cjs");
const { getRRConfigFromBodyshop } = require("./rr-config");
const RRLogger = require("./rr-logger");
const CreateRRLogEvent = require("./rr-logger-event");
const InstanceManager = require("../utils/instanceMgr").default;
/**
@@ -206,30 +206,35 @@ const buildCustomerPayloadFromJob = (job, overrides = {}) => {
* Maps data.dmsRecKey -> customerNo for compatibility with existing callers.
*/
const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
const log = RRLogger(socket, { ns: "rr" });
const { client, opts } = buildClientAndOpts(bodyshop);
const payload = buildCustomerPayloadFromJob(job, overrides);
let res;
let response;
try {
const safePayload = sanitizeRRCustomerPayload(payload);
res = await client.insertCustomer(safePayload, opts);
response = await client.insertCustomer(safePayload, opts);
CreateRRLogEvent(socket, "SILLY", "RR createRRCustomer called", { response });
} catch (e) {
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack, payload });
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer transport error", {
message: e?.message,
stack: e?.stack,
payload
});
throw e;
}
const data = res?.data ?? res;
const trx = res?.statusBlocks?.transaction;
const data = response?.data ?? response;
const trx = response?.statusBlocks?.transaction;
let customerNo = data?.dmsRecKey;
if (!customerNo) {
log("error", "RR insertCustomer returned no dmsRecKey/custNo", {
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer returned no dmsRecKey/custNo", {
status: trx?.status,
statusCode: trx?.statusCode,
message: trx?.message,
data
});
throw new Error(
`RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${
trx?.message ? ` msg=${trx.message}` : ""

View File

@@ -1,16 +1,15 @@
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
const { buildClientAndOpts } = require("./rr-lookup");
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
const RRLogger = require("./rr-logger");
const CreateRRLogEvent = require("./rr-logger-event");
/**
* Export a job to Reynolds & Reynolds as a Repair Order (create or update).
* @param args
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, xml: *, parsed: any, customerNo: string, svId: null, roNo: *}>}
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks,customerNo: string, svId: null, roNo: *}>}
*/
const exportJobToRR = async (args) => {
const { bodyshop, job, advisorNo, selectedCustomer, existing, socket } = args || {};
const log = RRLogger(socket, { ns: "rr-export" });
if (!bodyshop) throw new Error("exportJobToRR: bodyshop is required");
if (!job) throw new Error("exportJobToRR: job is required");
@@ -48,9 +47,9 @@ const exportJobToRR = async (args) => {
socket
});
svId = svRes?.svId || null;
log("info", "RR service vehicle ensured", { created: svRes?.created, svId });
CreateRRLogEvent(socket, "INFO", "RR service vehicle ensured", { created: svRes?.created, svId });
} catch (e) {
log("warn", "RR ensure service vehicle failed; continuing", { error: e?.message });
CreateRRLogEvent(socket, "WARN", "RR ensure service vehicle failed; continuing", { error: e?.message });
}
}
@@ -64,23 +63,25 @@ const exportJobToRR = async (args) => {
// Canonical update key is "roNo" (prefer DMS RO number); accept fallbacks from "existing"
const roNoForUpdate = existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId || null;
const rrRes = roNoForUpdate
const response = roNoForUpdate
? await client.updateRepairOrder({ ...payload, roNo: String(roNoForUpdate) }, finalOpts) // ✅ use roNo on update
: await client.createRepairOrder(payload, finalOpts);
const data = rrRes?.data || null;
CreateRRLogEvent(socket, "INFO", `RR raw Repair Order ${roNoForUpdate ? "updated" : "created"}`, {
response
});
const data = response?.data || null;
const roStatus = data?.roStatus || null;
// Extract canonical roNo you'll need for finalize step
const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null;
return {
success: rrRes?.success === true || roStatus?.status === "Success",
success: response?.success === true || roStatus?.status === "Success",
data,
roStatus,
statusBlocks: rrRes?.statusBlocks || [],
xml: rrRes?.xml,
parsed: rrRes?.parsed,
statusBlocks: response?.statusBlocks || [],
customerNo: String(selected),
svId,
roNo
@@ -90,11 +91,10 @@ const exportJobToRR = async (args) => {
/**
* Finalize an RR Repair Order by sending finalUpdate: "Y".
* @param args
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, xml: *, parsed: any}>}
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks}>}
*/
const finalizeRRRepairOrder = async (args) => {
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
const log = RRLogger(socket, { ns: "rr-finalize" });
if (!bodyshop) throw new Error("finalizeRRRepairOrder: bodyshop is required");
if (!job) throw new Error("finalizeRRRepairOrder: job is required");
@@ -142,7 +142,7 @@ const finalizeRRRepairOrder = async (args) => {
estimate: { estimateType: "Final" }
};
log("info", "RR finalize updateRepairOrder", {
CreateRRLogEvent(socket, "INFO", "Finalizing RR Repair Order", {
roNo: roNoToSend,
outsdRoNo: String(externalRo),
customerNo: String(customerNo),
@@ -157,9 +157,7 @@ const finalizeRRRepairOrder = async (args) => {
success: rrRes?.success === true || roStatus?.status === "Success",
data,
roStatus,
statusBlocks: rrRes?.statusBlocks || [],
xml: rrRes?.xml,
parsed: rrRes?.parsed
statusBlocks: rrRes?.statusBlocks || []
};
};

View File

@@ -1,55 +0,0 @@
const baseLogger = require("../utils/logger");
const safeSerialize = (value) => {
try {
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 {
try {
return String(value);
} catch {
return "[Unserializable]";
}
}
};
const RRLogger = (_socket, defaults = {}) => {
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 {
// swallow
}
}
};
};
module.exports = RRLogger;

View File

@@ -136,7 +136,7 @@ const rrCombinedSearch = async (bodyshop, args = {}) => {
const { client, opts } = buildClientAndOpts(bodyshop);
const payload = toCombinedSearchPayload(args);
const res = await client.combinedSearch(payload, opts);
return res?.data ?? res; // lib returns { success, data, ... }
return res;
};
/**
@@ -156,8 +156,7 @@ const rrGetAdvisors = async (bodyshop, args = {}) => {
advisorNumber: args.advisorNumber ? String(args.advisorNumber) : undefined
};
const res = await client.getAdvisors(payload, opts);
return res?.data ?? res;
return client.getAdvisors(payload, opts);
};
module.exports = {

View File

@@ -177,16 +177,20 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
for (const { q, fromVin } of queriesList) {
try {
CreateRRLogEvent(socket, "DEBUG", `{RR-SEARCH} Executing ${q.kind} query`, { q });
const res = await rrCombinedSearch(bodyshop, q);
const multiResponse = await rrCombinedSearch(bodyshop, q);
CreateRRLogEvent(socket, "SILLY", "Multi Customer Search - raw combined search", { response: multiResponse });
if (fromVin) {
const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
ownersSet = ownersFromVinBlocks(blocks, job?.v_vin);
const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : [];
ownersSet = ownersFromVinBlocks(multiBlocks, job?.v_vin);
try {
await redisHelpers.setSessionTransactionData(
socket.id,
getTransactionType(job.id),
RRCacheEnums.VINCandidates,
blocks,
multiBlocks,
defaultRRTTL
);
} catch {
@@ -194,7 +198,7 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
}
}
const norm = normalizeCustomerCandidates(res, { ownersSet });
const norm = normalizeCustomerCandidates(multiResponse, { ownersSet });
merged.push(...norm);
} catch (e) {
CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", { kind: q.kind, error: e.message });
@@ -220,24 +224,29 @@ const registerRREvents = ({ socket, redisHelpers }) => {
CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: begin", { jobid, params });
const res = await rrCombinedSearch(bodyshop, params || {});
const response = await rrCombinedSearch(bodyshop, params || {});
CreateRRLogEvent(socket, "SILLY", "rr-lookup-combined: received response", {
response
});
let ownersSet = null;
if ((params?.kind || "").toLowerCase() === "vin") {
const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
const blocks = Array.isArray(response?.data) ? response.data : [];
ownersSet = ownersFromVinBlocks(blocks);
}
const normalized = sortVehicleOwnerFirst(normalizeCustomerCandidates(res, { ownersSet }));
const normalized = sortVehicleOwnerFirst(normalizeCustomerCandidates(response, { ownersSet }));
const rid = resolveJobId(jobid, { jobid }, null);
const decorated = normalized.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner }));
cb?.({ jobid: rid, data: decorated });
socket.emit("rr-select-customer", decorated);
CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: emitted rr-select-customer", {
count: decorated.length,
res
count: decorated.length
});
} catch (e) {
CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", { error: e.message, jobid });
@@ -291,10 +300,13 @@ const registerRREvents = ({ socket, redisHelpers }) => {
// 2) Fetch + cache when no cache or forced refresh
if (!result) {
const live = await rrGetAdvisors(bodyshop, { departmentType: requestedDept });
result = Array.isArray(live) ? live : [];
const getAdvisorsCall = await rrGetAdvisors(bodyshop, { departmentType: requestedDept });
result = Array.isArray(getAdvisorsCall?.data) ? getAdvisorsCall.data : [];
try {
await redisHelpers.setProviderCache(ns, field, result, ADVISORS_CACHE_TTL);
CreateRRLogEvent(socket, "SILLY", "rr-get-advisors: fetched live data", {
getAdvisorsCall
});
CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache populated", {
ns,
field,
@@ -440,22 +452,25 @@ const registerRREvents = ({ socket, redisHelpers }) => {
try {
const vehQ = makeVehicleSearchPayloadFromJob(job);
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
const resVin = await rrCombinedSearch(bodyshop, vehQ);
const blocksVin = Array.isArray(resVin?.data) ? resVin.data : Array.isArray(resVin) ? resVin : [];
const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
CreateRRLogEvent(socket, "SILLY", `VIN owner pre-check response`, { response: vinResponse });
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
try {
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.VINCandidates,
blocksVin,
vinBlocks,
defaultRRTTL
);
} catch {
//
}
const ownersSet = ownersFromVinBlocks(blocksVin, job.v_vin);
const ownersSet = ownersFromVinBlocks(vinBlocks, job.v_vin);
if (ownersSet?.size) {
const sel = String(selectedCustNo);

View File

@@ -1,5 +1,5 @@
const RRLogger = require("./rr-logger");
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
const CreateRRLogEvent = require("./rr-logger-event");
/**
* Pick and normalize VIN from inputs
@@ -36,7 +36,7 @@ const pickCustNo = ({ selectedCustomerNo, custNo, customerNo }) => {
* @returns {Set<any>}
*/
const ownersFromCombined = (res, wantedVin) => {
const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
const blocks = Array.isArray(res?.data) ? res.data : [];
const owners = new Set();
for (const blk of blocks) {
const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
@@ -72,7 +72,6 @@ const isAlreadyExistsError = (e) => {
* - selectedCustomerNo / custNo / customerNo
* - license (optional)
* - socket (for logging)
* - logNs (namespace for logs)
*
* Returns: { created:boolean, exists:boolean, vin, customerNo, svId?, status? }
*/
@@ -87,12 +86,9 @@ const ensureRRServiceVehicle = async (args = {}) => {
custNo,
customerNo,
license,
socket,
logNs = "rr-service-vehicles"
socket
} = args;
const log = RRLogger(socket, { ns: logNs });
// Build/derive essentials
let client = inClient;
let routing = inRouting;
@@ -116,17 +112,24 @@ const ensureRRServiceVehicle = async (args = {}) => {
try {
let owners = new Set();
if (bodyshop) {
const comb = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
owners = ownersFromCombined(comb, vinStr);
const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
CreateRRLogEvent(socket, "silly", "{SV} Preflight combined search by VIN: raw response", {
response: combinedSearchResponse
});
owners = ownersFromCombined(combinedSearchResponse, vinStr);
}
// Short-circuit: VIN exists anywhere -> don't try to insert
if (owners.size > 0) {
const ownedBySame = owners.has(custNoStr);
log(ownedBySame ? "info" : "warn", "{SV} VIN already present in RR; skipping insert", {
CreateRRLogEvent(socket, ownedBySame ? "info" : "warn", "{SV} VIN already present in RR; skipping insert", {
vin: vinStr,
selectedCustomerNo: custNoStr,
owners: Array.from(owners)
});
return {
created: false,
exists: true,
@@ -137,7 +140,10 @@ const ensureRRServiceVehicle = async (args = {}) => {
}
} catch (e) {
// Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled)
log("warn", "{SV} VIN preflight lookup failed; continuing to insert", { vin: vinStr, error: e?.message });
CreateRRLogEvent(socket, "WARN", "{SV} VIN preflight lookup failed; continuing to insert", {
vin: vinStr,
error: e?.message
});
}
// --- Attempt insert (idempotent) ---
@@ -163,30 +169,26 @@ const ensureRRServiceVehicle = async (args = {}) => {
}
};
log("debug", "{SV} Inserting service vehicle", {
CreateRRLogEvent(socket, "info", "{SV} Inserting service vehicle", {
vin: vinStr,
selectedCustomerNo: custNoStr,
payloadShape: Object.keys(insertPayload).filter((k) => insertPayload[k] != null)
});
// Be tolerant to method name differences
const fnName =
typeof client.insertServiceVehicle === "function"
? "insertServiceVehicle"
: typeof client.serviceVehicleInsert === "function"
? "serviceVehicleInsert"
: null;
if (!fnName) {
throw new Error("RR client does not expose insertServiceVehicle/serviceVehicleInsert");
}
try {
const res = await client[fnName](insertPayload, insertOpts);
const res = await client.insertServiceVehicle(insertPayload, insertOpts);
CreateRRLogEvent(socket, "silly", "{SV} insertServiceVehicle: raw response", { res });
const data = res?.data ?? {};
const svId = data?.dmsRecKey || data?.svId || undefined;
log("info", "{SV} insertServiceVehicle: success", { vin: vinStr, customerNo: custNoStr, svId });
CreateRRLogEvent(socket, "info", "{SV} insertServiceVehicle: success", {
vin: vinStr,
customerNo: custNoStr,
svId
});
return {
created: true,
exists: false,
@@ -198,12 +200,13 @@ const ensureRRServiceVehicle = async (args = {}) => {
} catch (e) {
if (isAlreadyExistsError(e)) {
// Treat as idempotent success
log("warn", "{SV} insertServiceVehicle: already exists; treating as success", {
CreateRRLogEvent(socket, "warn", "{SV} insertServiceVehicle: already exists; treating as success", {
vin: vinStr,
customerNo: custNoStr,
code: e?.code,
status: e?.meta?.status || e?.status
});
return {
created: false,
exists: true,
@@ -212,11 +215,13 @@ const ensureRRServiceVehicle = async (args = {}) => {
status: e?.meta?.status || e?.status
};
}
log("error", "{SV} insertServiceVehicle: failure", {
CreateRRLogEvent(socket, "error", "{SV} insertServiceVehicle: failure", {
message: e?.message,
code: e?.code,
status: e?.meta?.status || e?.status
});
throw e;
}
};

View File

@@ -52,7 +52,7 @@ const makeVehicleSearchPayloadFromJob = (job) => {
* @returns {any[]}
*/
const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
const blocks = Array.isArray(res?.data) ? res.data : [];
const out = [];
const pickAddr = (addrArr) => {