feature/Reynolds-and-Reynolds-DMS-API-Integration - Scaffolding
This commit is contained in:
@@ -123,6 +123,7 @@ const applyRoutes = ({ app }) => {
|
|||||||
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
||||||
app.use("/sso", require("./server/routes/ssoRoutes"));
|
app.use("/sso", require("./server/routes/ssoRoutes"));
|
||||||
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
||||||
|
app.use("/rr", require("./server/routes/rrRoutes"));
|
||||||
|
|
||||||
// Default route for forbidden access
|
// Default route for forbidden access
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
|
|||||||
26
server/routes/rrRoutes.js
Normal file
26
server/routes/rrRoutes.js
Normal file
@@ -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;
|
||||||
185
server/rr/rr-helpers.js
Normal file
185
server/rr/rr-helpers.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
496
server/rr/rr-job-export.js
Normal file
496
server/rr/rr-job-export.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
8
server/rr/rr-logger.js
Normal file
8
server/rr/rr-logger.js
Normal file
@@ -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;
|
||||||
@@ -54,7 +54,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
// Store session data in Redis
|
// Store session data in Redis
|
||||||
const setSessionData = async (socketId, key, value, ttl) => {
|
const setSessionData = async (socketId, key, value, ttl) => {
|
||||||
try {
|
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") {
|
if (ttl && typeof ttl === "number") {
|
||||||
await pubClient.expire(`socket:${socketId}`, ttl);
|
await pubClient.expire(`socket:${socketId}`, ttl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const { admin } = require("../firebase/firebase-handler");
|
|||||||
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/fortellis");
|
||||||
const FortellisLogger = require("../fortellis/fortellis-logger");
|
const FortellisLogger = require("../fortellis/fortellis-logger");
|
||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
|
const RRLogger = require("../rr/rr-logger");
|
||||||
|
const { RRJobExport, RRSelectedCustomer } = require("../rr/rr-job-export");
|
||||||
|
|
||||||
const redisSocketEvents = ({
|
const redisSocketEvents = ({
|
||||||
io,
|
io,
|
||||||
redisHelpers: {
|
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
|
// Call Handlers
|
||||||
registerRoomAndBroadcastEvents(socket);
|
registerRoomAndBroadcastEvents(socket);
|
||||||
registerUpdateEvents(socket);
|
registerUpdateEvents(socket);
|
||||||
@@ -339,6 +404,7 @@ const redisSocketEvents = ({
|
|||||||
registerSyncEvents(socket);
|
registerSyncEvents(socket);
|
||||||
registerTaskEvents(socket);
|
registerTaskEvents(socket);
|
||||||
registerFortellisEvents(socket);
|
registerFortellisEvents(socket);
|
||||||
|
registerRREvents(socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Associate Middleware and Handlers
|
// Associate Middleware and Handlers
|
||||||
|
|||||||
Reference in New Issue
Block a user