From 1e3b3b853eaa37f7be01e7b3c2ebfa242759b574 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 10 Nov 2025 12:34:22 -0500 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration -Insert Export Log --- server/rr/rr-export-logs.js | 132 +++++++++++++++++++++++++ server/rr/rr-register-socket-events.js | 67 ++++++++++++- 2 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 server/rr/rr-export-logs.js diff --git a/server/rr/rr-export-logs.js b/server/rr/rr-export-logs.js new file mode 100644 index 000000000..7574eccd8 --- /dev/null +++ b/server/rr/rr-export-logs.js @@ -0,0 +1,132 @@ +// File: server/rr/rr-export-logs.js +// Mark job exported + insert export logs (success/failure) for Reynolds, mirroring Fortellis/PBS. + +const { GraphQLClient } = require("graphql-request"); +const queries = require("../graphql-client/queries"); +const CreateRRLogEvent = require("./rr-logger-event"); + +/** Get bearer token from the socket (same approach used elsewhere) */ +function getAuthToken(socket) { + return (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token) || null; +} + +/** Build a compact ExportsLog metadata object for RR */ +function buildRRExportMeta({ result, extra = {} }) { + // Avoid gigantic payloads; keep the useful bits + const roStatus = result?.roStatus || result?.data?.roStatus || null; + + return { + provider: "rr", + success: Boolean(result?.success || roStatus?.status === "Success"), + customerNo: result?.customerNo, + svId: result?.svId, + roStatus: roStatus && { + status: roStatus.status ?? roStatus.Status, + statusCode: roStatus.statusCode ?? roStatus.StatusCode, + message: roStatus.message ?? roStatus.Message + }, + statusBlocks: result?.statusBlocks || undefined, + xml: result?.xml, + parsed: result?.parsed, + ...extra + }; +} + +/** + * Success path: mark job exported + insert success ExportsLog w/ metadata. + * Uses queries.MARK_JOB_EXPORTED (same shape as Fortellis/PBS). + */ +async function markRRExportSuccess({ socket, jobId, job, bodyshop, result, metaExtra = {} }) { + const endpoint = process.env.GRAPHQL_ENDPOINT; + if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured"); + const token = getAuthToken(socket); + if (!token) throw new Error("Auth token missing on socket"); + + const client = new GraphQLClient(endpoint, {}); + client.setHeaders({ Authorization: `Bearer ${token}` }); + + const exportedStatus = + job?.bodyshop?.md_ro_statuses?.default_exported || bodyshop?.md_ro_statuses?.default_exported || "Exported*"; + + const meta = buildRRExportMeta({ result, extra: metaExtra }); + + try { + await client.request(queries.MARK_JOB_EXPORTED, { + jobId, + job: { + status: exportedStatus, + date_exported: new Date() + }, + log: { + bodyshopid: bodyshop?.id || job?.bodyshop?.id, + jobid: jobId, + successful: true, + useremail: socket?.user?.email || null, + metadata: meta + }, + bill: { + exported: true, + exported_at: new Date() + } + }); + + CreateRRLogEvent(socket, "INFO", "RR export: job marked exported + success log inserted", { + jobId, + exportedStatus + }); + } catch (e) { + // Non-fatal: export already succeeded; just surface the DB log failure + CreateRRLogEvent(socket, "ERROR", "RR export: failed to persist success markers/log", { + jobId, + error: e?.message + }); + } +} + +/** + * Failure path: insert failure ExportsLog (no job status flip). + * Uses queries.INSERT_EXPORT_LOG (same shape as Fortellis/PBS). + */ +async function insertRRFailedExportLog({ socket, jobId, job, bodyshop, error, classification, result }) { + const endpoint = process.env.GRAPHQL_ENDPOINT; + if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured"); + const token = getAuthToken(socket); + if (!token) throw new Error("Auth token missing on socket"); + + const client = new GraphQLClient(endpoint, {}); + client.setHeaders({ Authorization: `Bearer ${token}` }); + + const meta = buildRRExportMeta({ + result, + extra: { + error: error?.message || String(error), + classification: classification || undefined + } + }); + + try { + await client.request(queries.INSERT_EXPORT_LOG, { + log: { + bodyshopid: bodyshop?.id || job?.bodyshop?.id, + jobid: jobId, + successful: false, + message: error?.message || String(error), + useremail: socket?.user?.email || null, + metadata: meta + } + }); + + CreateRRLogEvent(socket, "INFO", "RR export: failure log inserted", { jobId }); + } catch (e) { + // Best-effort; don't throw + CreateRRLogEvent(socket, "ERROR", "RR export: failed to insert failure log", { + jobId, + error: e?.message + }); + } +} + +module.exports = { + markRRExportSuccess, + insertRRFailedExportLog +}; diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js index b8af6ae8b..a0f78e386 100644 --- a/server/rr/rr-register-socket-events.js +++ b/server/rr/rr-register-socket-events.js @@ -7,6 +7,9 @@ const { createRRCustomer } = require("./rr-customers"); const { ensureRRServiceVehicle } = require("./rr-service-vehicles"); const { classifyRRVendorError } = require("./rr-errors"); +// NEW: export logs (success/failure) parity with Fortellis/PBS +const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs"); + const { makeVehicleSearchPayloadFromJob, ownersFromVinBlocks, @@ -385,6 +388,9 @@ function registerRREvents({ socket, redisHelpers }) { // 2) Selection (or create) -> ensure vehicle -> export socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => { const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null); + let bodyshop = null; + let job = null; + try { if (!rid) throw new Error("jobid required"); CreateRRLogEvent(socket, "DEBUG", `{3} rr-selected-customer`, { @@ -400,12 +406,12 @@ function registerRREvents({ socket, redisHelpers }) { (selectedCustomerId && String(selectedCustomerId)) || (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer)); - const job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData); + job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData); const txEnvelope = (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.txEnvelope)) || {}; if (!job) throw new Error("Staged JobData not found (run rr-export-job first)."); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); - const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); + bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); // Create customer (if requested or none chosen) if (create === true || !selectedCustNo) { @@ -526,6 +532,15 @@ function registerRREvents({ socket, redisHelpers }) { const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor); if (!advisorNo) { CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`); + // Failure log (no advisor) + await insertRRFailedExportLog({ + socket, + jobId: rid, + job, + bodyshop, + error: new Error("Advisor is required (advisorNo)."), + classification: { errorCode: "RR_MISSING_ADVISOR", friendlyMessage: "Advisor is required." } + }); socket.emit("export-failed", { vendor: "rr", jobId: rid, error: "Advisor is required (advisorNo)." }); return ack?.({ ok: false, error: "Advisor is required (advisorNo)." }); } @@ -550,6 +565,16 @@ function registerRREvents({ socket, redisHelpers }) { if (result?.success) { CreateRRLogEvent(socket, "DEBUG", `{5} Export success`, { roStatus: result.roStatus }); + + // ✅ Mark exported + success log (with metadata) + await markRRExportSuccess({ + socket, + jobId: rid, + job, + bodyshop, + result + }); + socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: result.roStatus }); ack?.({ ok: true, result }); } else { @@ -567,10 +592,21 @@ function registerRREvents({ socket, redisHelpers }) { classification: cls }); + // ❌ Failure log (with classification + bits of response) + await insertRRFailedExportLog({ + socket, + jobId: rid, + job, + bodyshop, + error: new Error(cls.friendlyMessage || result?.error || "RR export failed"), + classification: cls, + result + }); + socket.emit("export-failed", { vendor: "rr", jobId: rid, - error: result?.error || cls.friendlyMessage || "RR export failed", + error: cls?.friendlyMessage || result?.error || "RR export failed", ...cls }); // Optional: a user-focused channel if you want to show inline banners @@ -591,7 +627,7 @@ function registerRREvents({ socket, redisHelpers }) { result || {}, defaultRRTTL ); - socket.emit("rr-export-job:result", { jobId: rid, bodyshopId, result }); + socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result }); } catch (error) { const cls = classifyRRVendorError(error); @@ -604,6 +640,29 @@ function registerRREvents({ socket, redisHelpers }) { jobid: rid }); + // ❌ Failure log for thrown error path + try { + // Load bodyshop/job if not loaded yet (best-effort) + if (!bodyshop || !job) { + const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); + bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket })); + job = + job || + (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData)); + } + } catch { + // ignore + } + + await insertRRFailedExportLog({ + socket, + jobId: rid, + job, + bodyshop, + error, + classification: cls + }); + try { socket.emit("export-failed", { vendor: "rr",