From b995e1f35dfe2ab47f26516813385575055c268d Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 1 Oct 2025 14:19:44 -0400 Subject: [PATCH] feature/Reynolds-and-Reynolds-DMS-API-Integration - Scaffolding --- server.js | 1 + server/routes/rrRoutes.js | 26 ++ server/rr/rr-helpers.js | 185 +++++++++ server/rr/rr-job-export.js | 496 ++++++++++++++++++++++++ server/rr/rr-logger.js | 8 + server/utils/redisHelpers.js | 2 +- server/web-sockets/redisSocketEvents.js | 66 ++++ 7 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 server/routes/rrRoutes.js create mode 100644 server/rr/rr-helpers.js create mode 100644 server/rr/rr-job-export.js create mode 100644 server/rr/rr-logger.js diff --git a/server.js b/server.js index 4751d7d00..3b2a491e5 100644 --- a/server.js +++ b/server.js @@ -123,6 +123,7 @@ const applyRoutes = ({ app }) => { app.use("/payroll", require("./server/routes/payrollRoutes")); app.use("/sso", require("./server/routes/ssoRoutes")); app.use("/integrations", require("./server/routes/intergrationRoutes")); + app.use("/rr", require("./server/routes/rrRoutes")); // Default route for forbidden access app.get("/", (req, res) => { diff --git a/server/routes/rrRoutes.js b/server/routes/rrRoutes.js new file mode 100644 index 000000000..bb25ab0fe --- /dev/null +++ b/server/routes/rrRoutes.js @@ -0,0 +1,26 @@ +const express = require("express"); +const router = express.Router(); + +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +// NOTE: keep parity with /cdk endpoints so UI can flip provider with minimal diff +router.use(validateFirebaseIdTokenMiddleware); + +// Placeholder endpoints — implement as needed: +router.post("/calculate-allocations", withUserGraphQLClientMiddleware, async (req, res) => { + try { + const Calc = require("../cdk/cdk-calculate-allocations").default; // reuse for now + const result = await Calc(req, req.body.jobid, true); // true->verbose style like Fortellis + res.status(200).json({ data: result }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Example: load RR makes/models someday +router.post("/getvehicles", withUserGraphQLClientMiddleware, async (req, res) => { + res.status(501).json({ error: "RR getvehicles not implemented yet" }); +}); + +module.exports = router; diff --git a/server/rr/rr-helpers.js b/server/rr/rr-helpers.js new file mode 100644 index 000000000..c8f8aa086 --- /dev/null +++ b/server/rr/rr-helpers.js @@ -0,0 +1,185 @@ +const path = require("path"); +require("dotenv").config({ + path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) +}); +const uuid = require("uuid").v4; +const AxiosLib = require("axios").default; +const axios = AxiosLib.create(); +const CreateRRLogEvent = require("./rr-logger"); + +// --- ENV / mode +const isProduction = process.env.NODE_ENV === "production"; + +// --- Public cache keys (mirrors FortellisCacheEnums) +const RRCacheEnums = { + txEnvelope: "txEnvelope", + SubscriptionMeta: "SubscriptionMeta", // keep shape parity with fortellis if you reuse UI/redis + JobData: "JobData", + DMSVid: "DMSVid", // vehicle id result + DMSVeh: "DMSVeh", // vehicle read + DMSVehCustomer: "DMSVehCustomer", + DMSCustList: "DMSCustList", + DMSCust: "DMSCust", + selectedCustomerId: "selectedCustomerId", + DMSTransHeader: "DMSTransHeader", + transWips: "transWips", + DMSBatchTxn: "DMSBatchTxn", + DmsBatchTxnPost: "DmsBatchTxnPost", + DMSVehHistory: "DMSVehHistory" +}; + +// --- Transaction namespacing in Redis +const getTransactionType = (jobid) => `rr:${jobid}`; +const defaultRRTTL = 60 * 60; + +// --- API catalog (stub URLs: swap in real ones from Rome specs) +const RRActions = { + SearchCustomer: { + apiName: "RR Search Customer", + url: isProduction ? "https://rr.example.com/api/customer/search" : "https://rr-uat.example.com/api/customer/search", + type: "get" + }, + ReadCustomer: { + apiName: "RR Read Customer", + url: isProduction ? "https://rr.example.com/api/customer/" : "https://rr-uat.example.com/api/customer/", + type: "get" // append /{id} + }, + CreateCustomer: { + apiName: "RR Create Customer", + url: isProduction ? "https://rr.example.com/api/customer" : "https://rr-uat.example.com/api/customer", + type: "post" + }, + InsertVehicle: { + apiName: "RR Insert Vehicle", + url: isProduction ? "https://rr.example.com/api/service-vehicle" : "https://rr-uat.example.com/api/service-vehicle", + type: "post" + }, + ReadVehicle: { + apiName: "RR Read Vehicle", + url: isProduction + ? "https://rr.example.com/api/service-vehicle/" + : "https://rr-uat.example.com/api/service-vehicle/", + type: "get" // append /{vehicleId} + }, + GetVehicleId: { + apiName: "RR Get Vehicle Id By VIN", + url: isProduction + ? "https://rr.example.com/api/service-vehicle/by-vin/" + : "https://rr-uat.example.com/api/service-vehicle/by-vin/", + type: "get" // append /{vin} + }, + StartWip: { + apiName: "RR Start WIP", + url: isProduction ? "https://rr.example.com/api/gl/start-wip" : "https://rr-uat.example.com/api/gl/start-wip", + type: "post" + }, + TranBatchWip: { + apiName: "RR Trans Batch WIP", + url: isProduction + ? "https://rr.example.com/api/gl/trans-batch-wip" + : "https://rr-uat.example.com/api/gl/trans-batch-wip", + type: "post" + }, + PostBatchWip: { + apiName: "RR Post Batch WIP", + url: isProduction + ? "https://rr.example.com/api/gl/post-batch-wip" + : "https://rr-uat.example.com/api/gl/post-batch-wip", + type: "post" + }, + QueryErrorWip: { + apiName: "RR Query Error WIP", + url: isProduction ? "https://rr.example.com/api/gl/error-wip/" : "https://rr-uat.example.com/api/gl/error-wip/", + type: "get" // append /{transId} + }, + ServiceHistoryInsert: { + apiName: "RR Insert Service Vehicle History", + url: isProduction + ? "https://rr.example.com/api/service-vehicle-history" + : "https://rr-uat.example.com/api/service-vehicle-history", + type: "post" + } +}; + +// --- Auth (stub). Replace with RR auth handshake from Rome specs. +async function getRRToken() { + // TODO: implement RR token retrieval (client credentials, basic, or session) per spec + // Return a bearer (or session cookie) string + return process.env.RR_FAKE_TOKEN || "rr-dev-token"; +} + +// --- URL constructor (same shape as Fortellis) +function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) { + const base = url.replace(/\/+$/, "/"); + const fullPath = pathParams ? `${base}${pathParams}` : base; + const qs = new URLSearchParams(requestSearchParams).toString(); + return qs ? `${fullPath}?${qs}` : fullPath; +} + +// --- General caller (same ergonomics as MakeFortellisCall) +async function MakeRRCall({ + apiName, + url, + headers = {}, + body = {}, + type = "post", + requestPathParams, + requestSearchParams = [], + debug = true, + jobid, + redisHelpers, + socket +}) { + const ReqId = uuid(); + const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams }); + const access_token = await getRRToken(); + + if (debug) { + console.log(`[RR] ${apiName} | ${type.toUpperCase()} ${fullUrl} | ReqId=${ReqId}`); + if (type !== "get") console.log(`[RR] payload: ${JSON.stringify(body, null, 2)}`); + } + + try { + const commonHeaders = { + Authorization: `Bearer ${access_token}`, + "X-Request-Id": ReqId, + ...headers + }; + + let resp; + switch (type) { + case "get": + resp = await axios.get(fullUrl, { headers: commonHeaders }); + break; + case "put": + resp = await axios.put(fullUrl, body, { headers: commonHeaders }); + break; + case "post": + default: + resp = await axios.post(fullUrl, body, { headers: commonHeaders }); + break; + } + + if (debug) console.log(`[RR] ${apiName} OK | ReqId=${ReqId}`); + return resp.data; + } catch (error) { + CreateRRLogEvent(socket, "ERROR", `[RR] ${apiName} failed: ${error.message}`, { + reqId: ReqId, + url: fullUrl, + apiName, + errorData: error.response?.data, + errorStatus: error.response?.status, + errorStatusText: error.response?.statusText, + stack: error.stack + }); + throw error; + } +} + +module.exports = { + RRActions, + MakeRRCall, + RRCacheEnums, + getTransactionType, + defaultRRTTL +}; diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js new file mode 100644 index 000000000..8a657b25e --- /dev/null +++ b/server/rr/rr-job-export.js @@ -0,0 +1,496 @@ +const GraphQLClient = require("graphql-request").GraphQLClient; +const _ = require("lodash"); +const moment = require("moment-timezone"); + +const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default; // reuse allocations +const CreateRRLogEvent = require("./rr-logger"); +const queries = require("../graphql-client/queries"); +const { MakeRRCall, RRActions, getTransactionType, defaultRRTTL, RRCacheEnums } = require("./rr-helpers"); + +// --- Public entry points (similar to Fortellis) +async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) { + const { setSessionTransactionData } = redisHelpers; + + try { + CreateRRLogEvent(socket, "DEBUG", `[RR] Received Job export request`, { jobid }); + + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.txEnvelope, + txEnvelope, + defaultRRTTL + ); + + const JobData = await QueryJobData({ socket, jobid }); + await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData, JobData, defaultRRTTL); + + CreateRRLogEvent(socket, "DEBUG", `[RR] Get Vehicle Id via VIN`, { vin: JobData.v_vin }); + + const DMSVid = await GetVehicleId({ socket, redisHelpers, JobData }); + await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid, DMSVid, defaultRRTTL); + + let DMSVehCustomer; + if (!DMSVid?.newId) { + const DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId }); + await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL); + + const owner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT"); + if (owner?.id?.value) { + DMSVehCustomer = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: owner.id.value }); + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.DMSVehCustomer, + DMSVehCustomer, + defaultRRTTL + ); + } + } + + const DMSCustList = await SearchCustomerByName({ socket, redisHelpers, JobData }); + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.DMSCustList, + DMSCustList, + defaultRRTTL + ); + + socket.emit("rr-select-customer", [ + ...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []), + ...(Array.isArray(DMSCustList) ? DMSCustList : []) + ]); + } catch (error) { + CreateRRLogEvent(socket, "ERROR", `[RR] RRJobExport failed: ${error.message}`, { stack: error.stack }); + } +} + +async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jobid }) { + const { setSessionTransactionData, getSessionTransactionData } = redisHelpers; + + try { + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.selectedCustomerId, + selectedCustomerId, + defaultRRTTL + ); + + const JobData = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData); + const txEnvelope = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.txEnvelope); + const DMSVid = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid); + + let DMSCust; + if (selectedCustomerId) { + DMSCust = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: selectedCustomerId }); + } else { + const createRes = await CreateCustomer({ socket, redisHelpers, JobData }); + DMSCust = { customerId: createRes?.data || createRes?.customerId || createRes?.id }; + } + await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSCust, DMSCust, defaultRRTTL); + + let DMSVeh; + if (DMSVid?.newId) { + DMSVeh = await InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust }); + } else { + DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId }); + // TODO: implement UpdateVehicle if RR supports updating ownership + // DMSVeh = await UpdateVehicle({ ... }) + } + await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL); + + const DMSTransHeader = await StartWip({ socket, redisHelpers, JobData, txEnvelope }); + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.DMSTransHeader, + DMSTransHeader, + defaultRRTTL + ); + + const DMSBatchTxn = await TransBatchWip({ socket, redisHelpers, JobData }); + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.DMSBatchTxn, + DMSBatchTxn, + defaultRRTTL + ); + + // decide success/err format later; keep parity with Fortellis shape + if (String(DMSBatchTxn?.rtnCode || "0") === "0") { + const DmsBatchTxnPost = await PostBatchWip({ socket, redisHelpers, JobData }); + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.DmsBatchTxnPost, + DmsBatchTxnPost, + defaultRRTTL + ); + + if (String(DmsBatchTxnPost?.rtnCode || "0") === "0") { + await MarkJobExported({ socket, jobid: JobData.id, redisHelpers }); + + // Optional service history write + try { + const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData }); + await setSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.DMSVehHistory, + DMSVehHistory, + defaultRRTTL + ); + } catch (e) { + CreateRRLogEvent(socket, "WARN", `[RR] ServiceVehicleHistory optional step failed: ${e.message}`); + } + + socket.emit("export-success", JobData.id); + } else { + await HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeader }); + } + } else { + await InsertFailedExportLog({ + socket, + JobData, + error: `RR DMSBatchTxn not successful: ${JSON.stringify(DMSBatchTxn)}` + }); + } + } catch (error) { + CreateRRLogEvent(socket, "ERROR", `[RR] RRSelectedCustomer failed: ${error.message}`, { stack: error.stack }); + const JobData = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.JobData + ); + if (JobData) await InsertFailedExportLog({ socket, JobData, error }); + } +} + +// --- GraphQL job fetch +async function QueryJobData({ socket, jobid }) { + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); + const result = await client + .setHeaders({ Authorization: `Bearer ${socket.handshake?.auth?.token}` }) + .request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobid }); + return result.jobs_by_pk; +} + +// --- RR API step stubs (wire to MakeRRCall) ------------------------- + +async function GetVehicleId({ socket, redisHelpers, JobData }) { + return await MakeRRCall({ + ...RRActions.GetVehicleId, + requestPathParams: JobData.v_vin, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function ReadVehicleById({ socket, redisHelpers, JobData, vehicleId }) { + return await MakeRRCall({ + ...RRActions.ReadVehicle, + requestPathParams: vehicleId, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function ReadCustomerById({ socket, redisHelpers, JobData, customerId }) { + return await MakeRRCall({ + ...RRActions.ReadCustomer, + requestPathParams: customerId, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function SearchCustomerByName({ socket, redisHelpers, JobData }) { + // align with Rome Search spec later + const ownerNameParams = + JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== "" + ? [["lastName", JobData.ownr_co_nm]] + : [ + ["firstName", JobData.ownr_fn], + ["lastName", JobData.ownr_ln] + ]; + + return await MakeRRCall({ + ...RRActions.SearchCustomer, + requestSearchParams: ownerNameParams, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function CreateCustomer({ socket, redisHelpers, JobData }) { + // shape per Rome Customer Insert spec + const body = { + customerType: JobData.ownr_co_nm ? "BUSINESS" : "INDIVIDUAL" + // fill minimal required fields later + }; + return await MakeRRCall({ + ...RRActions.CreateCustomer, + body, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust }) { + const body = { + // map fields per Rome Insert Service Vehicle spec + vin: JobData.v_vin + // owners, make/model, odometer, etc… + }; + return await MakeRRCall({ + ...RRActions.InsertVehicle, + body, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function StartWip({ socket, redisHelpers, JobData, txEnvelope }) { + const body = { + acctgDate: moment().tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"), + desc: txEnvelope.story || "", + docType: "10", + m13Flag: "0", + refer: JobData.ro_number, + srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder + srcJrnl: txEnvelope.journal, + userID: "BSMS" + }; + return await MakeRRCall({ + ...RRActions.StartWip, + body, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function TransBatchWip({ socket, redisHelpers, JobData }) { + const wips = await GenerateTransWips({ socket, redisHelpers, JobData }); + return await MakeRRCall({ + ...RRActions.TranBatchWip, + body: wips, // shape per Rome spec + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function PostBatchWip({ socket, redisHelpers, JobData }) { + const DMSTransHeader = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.DMSTransHeader + ); + + const body = { + opCode: "P", + transID: DMSTransHeader?.transID + }; + + return await MakeRRCall({ + ...RRActions.PostBatchWip, + body, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function QueryErrWip({ socket, redisHelpers, JobData }) { + const DMSTransHeader = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.DMSTransHeader + ); + return await MakeRRCall({ + ...RRActions.QueryErrorWip, + requestPathParams: DMSTransHeader?.transID, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function DeleteWip({ socket, redisHelpers, JobData }) { + const DMSTransHeader = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.DMSTransHeader + ); + const body = { opCode: "D", transID: DMSTransHeader?.transID }; + return await MakeRRCall({ + ...RRActions.PostBatchWip, + body, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) { + const txEnvelope = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.txEnvelope + ); + + const body = { + // map to Rome “Service Vehicle History Insert” spec + comments: txEnvelope?.story || "" + }; + return await MakeRRCall({ + ...RRActions.ServiceHistoryInsert, + body, + redisHelpers, + socket, + jobid: JobData.id + }); +} + +async function HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeader }) { + const DmsError = await QueryErrWip({ socket, redisHelpers, JobData }); + await DeleteWip({ socket, redisHelpers, JobData }); + + const errString = DmsError?.errMsg || JSON.stringify(DmsError); + errString?.split("|")?.forEach((e) => e && CreateRRLogEvent(socket, "ERROR", `[RR] Post error: ${e}`)); + await InsertFailedExportLog({ socket, JobData, error: errString }); +} + +async function GenerateTransWips({ socket, redisHelpers, JobData }) { + // reuse the existing allocator + const allocations = await CalculateAllocations(socket, JobData.id, true); // true==enable verbose logging + const DMSTransHeader = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.DMSTransHeader + ); + + // Translate allocations -> RR WIP line shape later. For now: keep parity with Fortellis skeleton + const wips = []; + allocations.forEach((alloc) => { + if (alloc.sale.getAmount() > 0 && !alloc.tax) { + wips.push({ + acct: alloc.profitCenter.dms_acctnumber, + cntl: alloc.profitCenter.dms_control_override || JobData.ro_number, + postAmt: alloc.sale.multiply(-1).getAmount(), + transID: DMSTransHeader?.transID, + trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco + }); + } + if (alloc.cost.getAmount() > 0 && !alloc.tax) { + wips.push({ + acct: alloc.costCenter.dms_acctnumber, + cntl: alloc.costCenter.dms_control_override || JobData.ro_number, + postAmt: alloc.cost.getAmount(), + transID: DMSTransHeader?.transID, + trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco + }); + wips.push({ + acct: alloc.costCenter.dms_wip_acctnumber, + cntl: alloc.costCenter.dms_control_override || JobData.ro_number, + postAmt: alloc.cost.multiply(-1).getAmount(), + transID: DMSTransHeader?.transID, + trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco + }); + } + if (alloc.tax && alloc.sale.getAmount() > 0) { + wips.push({ + acct: alloc.profitCenter.dms_acctnumber, + cntl: alloc.profitCenter.dms_control_override || JobData.ro_number, + postAmt: alloc.sale.multiply(-1).getAmount(), + transID: DMSTransHeader?.transID, + trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco + }); + } + }); + + const txEnvelope = await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.txEnvelope + ); + txEnvelope?.payers?.forEach((payer) => { + wips.push({ + acct: payer.dms_acctnumber, + cntl: payer.controlnumber, + postAmt: Math.round(payer.amount * 100), + transID: DMSTransHeader?.transID, + trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco + }); + }); + + await redisHelpers.setSessionTransactionData( + socket.id, + getTransactionType(JobData.id), + RRCacheEnums.transWips, + wips, + defaultRRTTL + ); + return wips; +} + +// --- DB logging mirrors Fortellis +async function MarkJobExported({ socket, jobid, redisHelpers }) { + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); + const currentToken = + (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); + + return client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.MARK_JOB_EXPORTED, { + jobId: jobid, + job: { + status: socket.JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*", + date_exported: new Date() + }, + log: { + bodyshopid: socket.JobData?.bodyshop?.id, + jobid, + successful: true, + useremail: socket.user?.email, + metadata: await redisHelpers.getSessionTransactionData( + socket.id, + getTransactionType(jobid), + RRCacheEnums.transWips + ) + }, + bill: { exported: true, exported_at: new Date() } + }); +} + +async function InsertFailedExportLog({ socket, JobData, error }) { + try { + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); + const currentToken = + (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); + return await client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.INSERT_EXPORT_LOG, { + log: { + bodyshopid: JobData.bodyshop.id, + jobid: JobData.id, + successful: false, + message: typeof error === "string" ? error : JSON.stringify(error), + useremail: socket.user?.email + } + }); + } catch (error2) { + CreateRRLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error2.message}`, { stack: error2.stack }); + } +} + +module.exports = { + RRJobExport, + RRSelectedCustomer +}; diff --git a/server/rr/rr-logger.js b/server/rr/rr-logger.js new file mode 100644 index 000000000..347d15a0d --- /dev/null +++ b/server/rr/rr-logger.js @@ -0,0 +1,8 @@ +const logger = require("../utils/logger"); + +const CreateRRLogEvent = (socket, level, message, txnDetails) => { + logger.log("rr-log-event", level, socket?.user?.email, null, { wsmessage: message, txnDetails }); + socket.emit("rr-log-event", { level, message, txnDetails }); +}; + +module.exports = CreateRRLogEvent; diff --git a/server/utils/redisHelpers.js b/server/utils/redisHelpers.js index 7ce1066d2..85ec72b4f 100644 --- a/server/utils/redisHelpers.js +++ b/server/utils/redisHelpers.js @@ -54,7 +54,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => { // Store session data in Redis const setSessionData = async (socketId, key, value, ttl) => { try { - await pubClient.hset(`socket:${socketId}`, key, JSON.stringify(value), ttl); // Use Redis pubClient + await pubClient.hset(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient if (ttl && typeof ttl === "number") { await pubClient.expire(`socket:${socketId}`, ttl); } diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 10472fba1..26bb8094a 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -2,6 +2,9 @@ const { admin } = require("../firebase/firebase-handler"); const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis"); const FortellisLogger = require("../fortellis/fortellis-logger"); const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; +const RRLogger = require("../rr/rr-logger"); +const { RRJobExport, RRSelectedCustomer } = require("../rr/rr-job-export"); + const redisSocketEvents = ({ io, redisHelpers: { @@ -331,6 +334,68 @@ const redisSocketEvents = ({ }); }; + const registerRREvents = (socket) => { + socket.on("rr-export-job", async ({ jobid, txEnvelope }) => { + try { + await RRJobExport({ + socket, + redisHelpers: { + setSessionData, + getSessionData, + addUserSocketMapping, + removeUserSocketMapping, + refreshUserSocketTTL, + getUserSocketMappingByBodyshop, + setSessionTransactionData, + getSessionTransactionData, + clearSessionTransactionData + }, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }, + jobid, + txEnvelope + }); + } catch (error) { + RRLogger(socket, "error", `Error during RR export: ${error.message}`); + logger.log("rr-job-export-error", "error", null, null, { message: error.message, stack: error.stack }); + } + }); + + socket.on("rr-selected-customer", async ({ jobid, selectedCustomerId }) => { + try { + await RRSelectedCustomer({ + socket, + redisHelpers: { + setSessionData, + getSessionData, + addUserSocketMapping, + removeUserSocketMapping, + refreshUserSocketTTL, + getUserSocketMappingByBodyshop, + setSessionTransactionData, + getSessionTransactionData, + clearSessionTransactionData + }, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }, + jobid, + selectedCustomerId + }); + } catch (error) { + RRLogger(socket, "error", `Error during RR selected-customer: ${error.message}`); + logger.log("rr-selected-customer-error", "error", null, null, { message: error.message, stack: error.stack }); + } + }); + + socket.on("rr-calculate-allocations", async (jobid, callback) => { + try { + const allocations = await CdkCalculateAllocations(socket, jobid); + callback(allocations); + } catch (error) { + RRLogger(socket, "error", `Error during RR calculate allocations: ${error.message}`); + logger.log("rr-calc-allocations-error", "error", null, null, { message: error.message, stack: error.stack }); + } + }); + }; + // Call Handlers registerRoomAndBroadcastEvents(socket); registerUpdateEvents(socket); @@ -339,6 +404,7 @@ const redisSocketEvents = ({ registerSyncEvents(socket); registerTaskEvents(socket); registerFortellisEvents(socket); + registerRREvents(socket); }; // Associate Middleware and Handlers