feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration -Insert Export Log
This commit is contained in:
132
server/rr/rr-export-logs.js
Normal file
132
server/rr/rr-export-logs.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -7,6 +7,9 @@ const { createRRCustomer } = require("./rr-customers");
|
|||||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||||
const { classifyRRVendorError } = require("./rr-errors");
|
const { classifyRRVendorError } = require("./rr-errors");
|
||||||
|
|
||||||
|
// NEW: export logs (success/failure) parity with Fortellis/PBS
|
||||||
|
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
makeVehicleSearchPayloadFromJob,
|
makeVehicleSearchPayloadFromJob,
|
||||||
ownersFromVinBlocks,
|
ownersFromVinBlocks,
|
||||||
@@ -385,6 +388,9 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
// 2) Selection (or create) -> ensure vehicle -> export
|
// 2) Selection (or create) -> ensure vehicle -> export
|
||||||
socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
|
socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
|
||||||
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
|
||||||
|
let bodyshop = null;
|
||||||
|
let job = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!rid) throw new Error("jobid required");
|
if (!rid) throw new Error("jobid required");
|
||||||
CreateRRLogEvent(socket, "DEBUG", `{3} rr-selected-customer`, {
|
CreateRRLogEvent(socket, "DEBUG", `{3} rr-selected-customer`, {
|
||||||
@@ -400,12 +406,12 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
(selectedCustomerId && String(selectedCustomerId)) ||
|
(selectedCustomerId && String(selectedCustomerId)) ||
|
||||||
(await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer));
|
(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)) || {};
|
const txEnvelope = (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.txEnvelope)) || {};
|
||||||
if (!job) throw new Error("Staged JobData not found (run rr-export-job first).");
|
if (!job) throw new Error("Staged JobData not found (run rr-export-job first).");
|
||||||
|
|
||||||
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||||
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
|
bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
|
||||||
|
|
||||||
// Create customer (if requested or none chosen)
|
// Create customer (if requested or none chosen)
|
||||||
if (create === true || !selectedCustNo) {
|
if (create === true || !selectedCustNo) {
|
||||||
@@ -526,6 +532,15 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);
|
const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);
|
||||||
if (!advisorNo) {
|
if (!advisorNo) {
|
||||||
CreateRRLogEvent(socket, "ERROR", `Advisor is required (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)." });
|
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: "Advisor is required (advisorNo)." });
|
||||||
return ack?.({ ok: false, 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) {
|
if (result?.success) {
|
||||||
CreateRRLogEvent(socket, "DEBUG", `{5} Export success`, { roStatus: result.roStatus });
|
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 });
|
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: result.roStatus });
|
||||||
ack?.({ ok: true, result });
|
ack?.({ ok: true, result });
|
||||||
} else {
|
} else {
|
||||||
@@ -567,10 +592,21 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
classification: cls
|
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", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
vendor: "rr",
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
error: result?.error || cls.friendlyMessage || "RR export failed",
|
error: cls?.friendlyMessage || result?.error || "RR export failed",
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
// Optional: a user-focused channel if you want to show inline banners
|
// Optional: a user-focused channel if you want to show inline banners
|
||||||
@@ -591,7 +627,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
result || {},
|
result || {},
|
||||||
defaultRRTTL
|
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) {
|
} catch (error) {
|
||||||
const cls = classifyRRVendorError(error);
|
const cls = classifyRRVendorError(error);
|
||||||
|
|
||||||
@@ -604,6 +640,29 @@ function registerRREvents({ socket, redisHelpers }) {
|
|||||||
jobid: rid
|
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 {
|
try {
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
vendor: "rr",
|
||||||
|
|||||||
Reference in New Issue
Block a user