From 4c250f618986fc482690e8c293ca29aaa3626bae Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 13 Nov 2025 15:39:29 -0500 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Enhance logging --- .../dms-log-events.component.jsx | 2 + client/src/pages/dms/dms.container.jsx | 1 + server/rr/rr-customers.js | 21 ++++--- server/rr/rr-job-export.js | 32 +++++----- server/rr/rr-logger.js | 55 ----------------- server/rr/rr-lookup.js | 5 +- server/rr/rr-register-socket-events.js | 47 +++++++++----- server/rr/rr-service-vehicles.js | 61 ++++++++++--------- server/rr/rr-utils.js | 2 +- 9 files changed, 98 insertions(+), 128 deletions(-) delete mode 100644 server/rr/rr-logger.js diff --git a/client/src/components/dms-log-events/dms-log-events.component.jsx b/client/src/components/dms-log-events/dms-log-events.component.jsx index d01afb230..25f0981cf 100644 --- a/client/src/components/dms-log-events/dms-log-events.component.jsx +++ b/client/src/components/dms-log-events/dms-log-events.component.jsx @@ -129,6 +129,8 @@ const normalizeLog = (input) => { */ const logLevelColor = (level) => { switch ((level || "").toUpperCase()) { + case "SILLY": + return "purple"; case "DEBUG": return "orange"; case "INFO": diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index e4acf5862..5f81ed8b4 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -412,6 +412,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse setActiveLogLevel(value); }} > + SILLY DEBUG INFO WARN diff --git a/server/rr/rr-customers.js b/server/rr/rr-customers.js index f8eec1c54..5ad966974 100644 --- a/server/rr/rr-customers.js +++ b/server/rr/rr-customers.js @@ -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}` : "" diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index e34ddbf86..64b38f280 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -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 || [] }; }; diff --git a/server/rr/rr-logger.js b/server/rr/rr-logger.js deleted file mode 100644 index c76125e1e..000000000 --- a/server/rr/rr-logger.js +++ /dev/null @@ -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 ``; - } - 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; diff --git a/server/rr/rr-lookup.js b/server/rr/rr-lookup.js index cf9183f6e..5fa300a92 100644 --- a/server/rr/rr-lookup.js +++ b/server/rr/rr-lookup.js @@ -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 = { diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js index 5d799a0dc..5cc9b845d 100644 --- a/server/rr/rr-register-socket-events.js +++ b/server/rr/rr-register-socket-events.js @@ -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); diff --git a/server/rr/rr-service-vehicles.js b/server/rr/rr-service-vehicles.js index 877a081c7..bdfa4c78d 100644 --- a/server/rr/rr-service-vehicles.js +++ b/server/rr/rr-service-vehicles.js @@ -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} */ 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; } }; diff --git a/server/rr/rr-utils.js b/server/rr/rr-utils.js index b39d686e4..dd898b12a 100644 --- a/server/rr/rr-utils.js +++ b/server/rr/rr-utils.js @@ -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) => {