Files
bodyshop/server/rr/rr-job-export.js

500 lines
16 KiB
JavaScript

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 currentToken =
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
const result = await client
.setHeaders({ Authorization: `Bearer ${currentToken}` })
.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
};