Files
bodyshop/server/fortellis/fortellis.js
Allan Carr 8db8744782 IO-3629 PostBatchWip Rtn != 0 error
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-27 15:38:27 -07:00

1430 lines
46 KiB
JavaScript

const GraphQLClient = require("graphql-request").GraphQLClient;
const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const InstanceMgr = require("../utils/instanceMgr").default;
const CreateFortellisLogEvent = require("./fortellis-logger");
const queries = require("../graphql-client/queries");
const {
MakeFortellisCall,
FortellisActions,
getTransactionType,
defaultFortellisTTL,
FortellisCacheEnums,
FortellisApiError
} = require("./fortellis-helpers");
const _ = require("lodash");
const moment = require("moment-timezone");
const replaceSpecialRegex = /[^a-zA-Z0-9 ]+/g;
// Helper function to handle FortellisApiError logging
function handleFortellisApiError(socket, error, functionName, additionalDetails = {}) {
if (error instanceof FortellisApiError) {
CreateFortellisLogEvent(socket, "ERROR", `${functionName} failed: ${error.message}`, {
reqId: error.reqId,
url: error.url,
apiName: error.apiName,
errorData: error.errorData,
errorStatus: error.errorStatus,
errorStatusText: error.errorStatusText,
functionName,
...additionalDetails
});
CreateFortellisLogEvent(
socket,
"DEBUG",
`${functionName} failed. , ${JSON.stringify({
reqId: error.reqId,
url: error.url,
apiName: error.apiName,
errorData: error.errorData,
errorStatus: error.errorStatus,
errorStatusText: error.errorStatusText,
functionName,
...additionalDetails
})}`
);
} else {
CreateFortellisLogEvent(socket, "ERROR", `Error in ${functionName} - ${error.message}`, {
error: error.message,
stack: error.stack,
functionName,
...additionalDetails
});
}
}
async function FortellisJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
const {
// setSessionData,
// getSessionData,
// addUserSocketMapping,
// removeUserSocketMapping,
// refreshUserSocketTTL,
// getUserSocketMappingByBodyshop,
setSessionTransactionData
// getSessionTransactionData,
// clearSessionTransactionData
} = redisHelpers;
try {
CreateFortellisLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.txEnvelope,
txEnvelope,
defaultFortellisTTL
);
const JobData = await QueryJobData({ socket, jobid });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.JobData,
JobData,
defaultFortellisTTL
);
CreateFortellisLogEvent(socket, "DEBUG", `{1} Begin Calculate DMS Vehicle ID using VIN: ${JobData.v_vin}`);
const DMSVid = (await CalculateDmsVid({ socket, JobData, redisHelpers }))[0];
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSVid,
DMSVid,
defaultFortellisTTL
);
let DMSVehCustomer;
if (!DMSVid.newId) {
CreateFortellisLogEvent(socket, "DEBUG", `{2.1} Querying the Vehicle using the DMSVid: ${DMSVid.vehiclesVehId}`);
const DMSVeh = await QueryDmsVehicleById({ socket, redisHelpers, JobData, DMSVid });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSVeh,
DMSVeh,
defaultFortellisTTL
);
const DMSVehCustomerFromVehicle =
DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
if (DMSVehCustomerFromVehicle?.id && DMSVehCustomerFromVehicle.id.value) {
CreateFortellisLogEvent(
socket,
"DEBUG",
`{2.2} Querying the Customer using the ID from DMSVeh: ${DMSVehCustomerFromVehicle.id.value}`
);
DMSVehCustomer = await QueryDmsCustomerById({
socket,
redisHelpers,
JobData,
CustomerId: DMSVehCustomerFromVehicle.id.value
});
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSVehCustomer,
DMSVehCustomer,
defaultFortellisTTL
);
}
}
CreateFortellisLogEvent(socket, "DEBUG", `{2.3} Querying the Customer using the name.`);
const DMSCustList = await QueryDmsCustomerByName({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCustList,
DMSCustList,
defaultFortellisTTL
);
socket.emit("fortellis-select-customer", [
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
...DMSCustList
]);
} catch (error) {
CreateFortellisLogEvent(socket, "ERROR", `Error in FortellisJobExport - ${error} `, {
error: error.message,
stack: error.stack
});
}
}
async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jobid }) {
const {
// setSessionData,
// getSessionData,
// addUserSocketMapping,
// removeUserSocketMapping,
// refreshUserSocketTTL,
// getUserSocketMappingByBodyshop,
setSessionTransactionData,
getSessionTransactionData
//clearSessionTransactionData
} = redisHelpers;
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.selectedCustomerId,
selectedCustomerId,
defaultFortellisTTL
);
const JobData = await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.JobData);
const txEnvelope = await getSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.txEnvelope
);
if (!JobData || !txEnvelope) {
const friendlyMessage =
"Fortellis export context was lost after reconnect. Click Post again to restart the Fortellis flow.";
CreateFortellisLogEvent(socket, "WARN", friendlyMessage, {
jobid,
hasJobData: !!JobData,
hasTxEnvelope: !!txEnvelope
});
socket.emit("export-failed", {
title: "Fortellis",
severity: "warning",
errorCode: "FORTELLIS_CONTEXT_MISSING",
friendlyMessage
});
return;
}
try {
const DMSVid = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.DMSVid
);
if (!DMSVid) {
const friendlyMessage =
"Fortellis vehicle context is missing after reconnect. Click Post again to restart the Fortellis flow.";
CreateFortellisLogEvent(socket, "WARN", friendlyMessage, {
jobid,
hasDMSVid: !!DMSVid
});
socket.emit("export-failed", {
title: "Fortellis",
severity: "warning",
errorCode: "FORTELLIS_CONTEXT_MISSING",
friendlyMessage
});
return;
}
let DMSCust;
if (selectedCustomerId) {
CreateFortellisLogEvent(socket, "DEBUG", `{3.1} Querying the Customer using Customer ID: ${selectedCustomerId}`);
//Get cust list from Redis. Return the item
const DMSCustList =
(await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSCustList)) || [];
const existingCustomerInDMSCustList = DMSCustList.find((c) => c.customerId === selectedCustomerId);
DMSCust = existingCustomerInDMSCustList || {
customerId: selectedCustomerId //This is the fall back in case it is the generic customer.
};
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCust,
DMSCust,
defaultFortellisTTL
);
} else {
CreateFortellisLogEvent(socket, "DEBUG", `{3.2} Creating new customer.`);
const DMSCustomerInsertResponse = await InsertDmsCustomer({ socket, redisHelpers, JobData });
DMSCust = { customerId: DMSCustomerInsertResponse.data };
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSCust,
DMSCust,
defaultFortellisTTL
);
}
let DMSVeh;
if (DMSVid.newId) {
CreateFortellisLogEvent(socket, "DEBUG", `{4.1} Inserting new vehicle with ID: ID ${DMSVid.vehiclesVehId}`);
DMSVeh = await InsertDmsVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust });
} else {
DMSVeh = await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSVeh);
CreateFortellisLogEvent(socket, "DEBUG", `{4.3} Updating Existing Vehicle to associate to owner.`);
//Check to see if the vehicle needs to be updated - i.e. the owner is not the selected customer.
if (!DMSVeh?.owners.find((o) => o.id.value === DMSCust.customerId && o.id.assigningPartyId === "CURRENT")) {
DMSVeh = await UpdateDmsVehicle({
socket,
redisHelpers,
JobData,
DMSVeh,
DMSCust,
selectedCustomerId: selectedCustomerId || DMSCust.customerId,
txEnvelope
});
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSVeh,
DMSVeh,
defaultFortellisTTL
);
}
}
// const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData });
// await setSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSVehHistory, DMSVehHistory, defaultFortellisTTL);
CreateFortellisLogEvent(socket, "DEBUG", `{5} Creating Transaction header with Dms Start WIP`);
const DMSTransHeader = await InsertDmsStartWip({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSTransHeader,
DMSTransHeader,
defaultFortellisTTL
);
CreateFortellisLogEvent(socket, "DEBUG", `{5.1} Creating Transaction with ID ${DMSTransHeader.transID}`);
if (DMSTransHeader.rtnCode === "0") {
try {
CreateFortellisLogEvent(
socket,
"DEBUG",
`{6} Attempting to post Transaction with ID ${DMSTransHeader.transID}`
);
const DmsBatchTxnPost = await PostDmsBatchWip({ socket, redisHelpers, JobData }); // 2 in 1 call that includes a post and the transactions.
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DmsBatchTxnPost,
DmsBatchTxnPost,
defaultFortellisTTL
);
if (DmsBatchTxnPost.rtnCode === "0") {
//TODO: Validate this is a string and not #
//something
CreateFortellisLogEvent(socket, "DEBUG", `{6} Successfully posted transaction to DMS.`);
await MarkJobExported({ socket, jobid: JobData.id, JobData, redisHelpers });
try {
CreateFortellisLogEvent(socket, "DEBUG", `{7} Updating Service Vehicle History.`);
const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData });
await setSessionTransactionData(
socket.id,
getTransactionType(jobid),
FortellisCacheEnums.DMSVehHistory,
DMSVehHistory,
defaultFortellisTTL
);
} catch (error) {
CreateFortellisLogEvent(socket, "ERROR", `{7.1} Error posting vehicle service history. ${error.message}`);
}
socket.emit("export-success", JobData.id);
} else {
//There was something wrong. Throw an error to trigger clean up.
const batchPostError = new Error(DmsBatchTxnPost.sendline || "Error posting DMS Batch Transaction");
batchPostError.errorData = { DMSTransHeader, DmsBatchTxnPost };
throw batchPostError;
}
} catch (error) {
//Clean up the transaction and insert a faild error code
// //Get the error code
CreateFortellisLogEvent(socket, "DEBUG", `{6.1} Getting errors for Transaction ID ${DMSTransHeader.transID}`);
let dmsErrors = [];
try {
const DmsError = await QueryDmsErrWip({ socket, redisHelpers, JobData });
dmsErrors = Array.isArray(DmsError?.errMsg) ? DmsError.errMsg.filter((e) => e !== null && e !== "") : [];
dmsErrors.forEach((e) => {
CreateFortellisLogEvent(socket, "ERROR", `Error encountered in posting transaction => ${e} `);
});
} catch (queryError) {
CreateFortellisLogEvent(
socket,
"ERROR",
`{6.1} Unable to read ErrWIP for Transaction ID ${DMSTransHeader.transID}: ${queryError.message}`
);
}
//Delete the transaction, even if querying ErrWIP fails.
CreateFortellisLogEvent(socket, "DEBUG", `{6.2} Deleting Transaction ID ${DMSTransHeader.transID}`);
try {
await DeleteDmsWip({ socket, redisHelpers, JobData });
} catch (cleanupError) {
CreateFortellisLogEvent(
socket,
"ERROR",
`{6.2} Failed cleanup for Transaction ID ${DMSTransHeader.transID}: ${cleanupError.message}`
);
}
if (!error.errorData || typeof error.errorData !== "object") {
error.errorData = {};
}
error.errorData.issues = dmsErrors.length ? dmsErrors : [error.message];
throw error;
}
}
} catch (error) {
CreateFortellisLogEvent(socket, "ERROR", `Error in FortellisSelectedCustomer - ${error} `, {
error: error.message,
stack: error.stack,
data: error.errorData
});
socket.emit("export-failed", {
title: "Fortellis",
severity: "error",
error: error.message,
friendlyMessage: "Fortellis export failed. Please click Post again to retry."
});
await InsertFailedExportLog({
socket,
JobData,
error: error.errorData?.issues || [JSON.stringify(error.errorData)]
});
} finally {
//Ensure we always insert logEvents
//GQL to insert logevents.
//CdkBase.createLogEvent(socket, "DEBUG", `Capturing log events to database.`);
}
}
exports.FortellisSelectedCustomer = FortellisSelectedCustomer;
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;
}
async function CalculateDmsVid({ socket, JobData, redisHelpers }) {
try {
const result = await MakeFortellisCall({
...FortellisActions.GetVehicleId,
requestPathParams: JobData.v_vin,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {}
});
return Array.isArray(result) ? result.filter((v) => v.vehiclesVehId !== null && v.vehiclesVehId !== "") : [];
} catch (error) {
handleFortellisApiError(socket, error, "CalculateDmsVid", {
vin: JobData.v_vin,
jobId: JobData.id
});
throw error;
}
}
async function QueryDmsVehicleById({ socket, redisHelpers, JobData, DMSVid }) {
try {
const result = await MakeFortellisCall({
...FortellisActions.GetVehicleById,
requestPathParams: DMSVid.vehiclesVehId,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "QueryDmsVehicleById", {
vehicleId: DMSVid.vehiclesVehId,
jobId: JobData.id
});
throw error;
}
}
async function QueryDmsCustomerById({ socket, redisHelpers, JobData, CustomerId }) {
try {
const result = await MakeFortellisCall({
...FortellisActions.ReadCustomer,
requestPathParams: CustomerId,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {}
});
return result.data;
} catch (error) {
handleFortellisApiError(socket, error, "QueryDmsCustomerById", {
customerId: CustomerId,
jobId: JobData.id
});
throw error;
}
}
async function QueryDmsCustomerByName({ socket, redisHelpers, JobData }) {
const ownerName =
JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== ""
? //? [["firstName", JobData.ownr_co_nm.replace(replaceSpecialRegex, "").toUpperCase()]] // Commented out until we receive direction.
[["phone", JobData.ownr_ph1?.replace(/[^0-9]/g, "")]]
: [
["firstName", JobData.ownr_fn?.replace(/[^a-zA-Z0-9]/g, "").toUpperCase()],
["lastName", JobData.ownr_ln?.replace(/[^a-zA-Z0-9]/g, "").toUpperCase()]
];
try {
const result = await MakeFortellisCall({
...FortellisActions.QueryCustomerByName,
requestSearchParams: ownerName,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {}
});
return result.data;
} catch (error) {
handleFortellisApiError(socket, error, "QueryDmsCustomerByName", {
searchParams: ownerName,
jobId: JobData.id
});
throw error;
}
}
async function InsertDmsCustomer({ socket, redisHelpers, JobData }) {
try {
const isBusiness = JobData.ownr_co_nm && JobData.ownr_co_nm.replace(replaceSpecialRegex, "").trim() !== "";
const result = await MakeFortellisCall({
...FortellisActions.CreateCustomer,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
customerType: isBusiness ? "BUSINESS" : "INDIVIDUAL",
...(isBusiness
? {
companyName: JobData.ownr_co_nm && JobData.ownr_co_nm.replace(replaceSpecialRegex, "").toUpperCase(),
secondaryCustomerName: {
//lastName: JobData.ownr_co_nm && JobData.ownr_co_nm.replace(replaceSpecialRegex, "").toUpperCase()
}
}
: {
customerName: {
//"suffix": "Mr.",
firstName: JobData.ownr_fn && JobData.ownr_fn.replace(/[^a-zA-Z0-9]/g, "").toUpperCase(),
//"middleName": "",
lastName: JobData.ownr_ln && JobData.ownr_ln.replace(/[^a-zA-Z0-9]/g, "").toUpperCase()
//"title": "",
//"nickName": ""
}
}),
postalAddress: {
addressLine1: JobData.ownr_addr1?.replace(replaceSpecialRegex, "").trim().toUpperCase(),
addressLine2: JobData.ownr_addr2?.replace(replaceSpecialRegex, "").trim().toUpperCase(),
city: JobData.ownr_city?.replace(replaceSpecialRegex, "").trim().toUpperCase(),
postalCode: InstanceMgr({
imex: JobData.ownr_zip && JobData.ownr_zip.toUpperCase().replace(/\W/g, "").replace(/(...)/, "$1 "),
rome: JobData.ownr_zip
}),
state: JobData.ownr_st?.replace(replaceSpecialRegex, "").trim().toUpperCase(),
country: JobData.ownr_ctry?.replace(replaceSpecialRegex, "").trim().toUpperCase()
//"territory": ""
},
// "birthDate": {
// "day": "15",
// "month": "07",
// "year": "1979"
// },
//"gender": "M",
//"language": "English",
contactMethods: {
phones: [
{
//"uuid": "",
number: JobData.ownr_ph1?.replace(/[^0-9]/g, ""),
type: "HOME"
// "doNotCallIndicator": true,
// "doNotCallIndicatorDate": `null,
// "doNotCallRegistrySource": "",
// "isOnDoNotCallRegistry": false,
// "isPrimary": false,
// "isPreferred": false,
// "isTextMessageAllowed": false,
// "textMessageCarrier": "",
// "optInDate": null,
// "optInRequestedDate": null,
// "preferredDay": "",
// "preferredTime": ""
}
// {
// "uuid": "",
// "number": "6666666666",
// "type": "MOBILE",
// "doNotCallIndicator": true,
// "doNotCallIndicatorDate": null,
// "doNotCallRegistrySource": "",
// "isOnDoNotCallRegistry": false,
// "isPrimary": true,
// "isPreferred": true,
// "isTextMessageAllowed": false,
// "textMessageCarrier": "",
// "optInDate": null,
// "optInRequestedDate": null,
// "preferredDay": "",
// "preferredTime": ""
// }
],
emailAddresses: [
...(!_.isEmpty(JobData.ownr_ea)
? [
{
//"uuid": "",
address: JobData.ownr_ea.toUpperCase(),
type: "PERSONAL"
// "doNotEmailSource": "",
// "doNotEmail": false,
// "isPreferred": true,
// "transactionEmailNotificationOptIn": false,
// "optInRequestDate": null,
// "optInDate": null
}
]
: [])
// {
// "uuid": "",
// "address": "jilldoe@test.com",
// "type": "WORK",
// "doNotEmailSource": "",
// "doNotEmail": false,
// "isPreferred": false,
// "transactionEmailNotificationOptIn": false,
// "optInRequestDate": null,
// "optInDate": null
// }
]
}
// // "doNotContact": false,
// // "optOutDate": "",
// // "optOutTime": "",
// // "optOutFlag": false,
// // "isDeleteDataFlag": false,
// // "deleteDataDate": "",
// // "deleteDataTime": "",
// // "blockMailFlag": true,
// // "dateAdded": "",
// // "employer": "employer",
// "insurance": {
// "policy": {
// "effectiveDate": "2022-01-01",
// "expirationDate": "2023-01-01",
// "number": "12345",
// "verifiedBy": "Agent",
// "verifiedDate": "2023-01-01",
// "insPolicyCollisionDed": "",
// "insPolicyComprehensiveDed": "",
// "insPolicyFireTheftDed": ""
// },
// "insuranceAgency": {
// "agencyName": "InsAgency",
// "agentName": "agent",
// "phoneNumber": "9999999999",
// "postalAddress": {
// "addressLine1": "999 Main St",
// "addressLine2": "Suite 999",
// "city": "Austin",
// "state": "TX",
// "postalCode": "78750",
// "county": "Travis",
// "country": "USA"
// }
// },
// "insuranceCompany": {
// "name": "InsCompany",
// "phoneNumber": "8888888888",
// "postalAddress": {
// "addressLine1": "888 Main St",
// "addressLine2": "Suite 888",
// "city": "Austin",
// "state": "TX",
// "postalCode": "78750",
// "county": "Travis",
// "country": "USA"
// }
// }
// },
// "specialInstructions": [
// {
// "lineNuber": "1",
// "specialInstruction": "specialInstruction1"
// },
// {
// "lineNuber": "2",
// "specialInstruction": "specialInstruction2"
// }
// ],
// "groupCode": "PT",
// "priceCode": "5",
// "roPriceCode": "5",
// "taxCode": "3145",
// "dealerLoyaltyIndicator": "PN612345",
// "delCdeServiceNames": "99",
// "deleteCode": "9999",
// "fleetFlag": "1",
// "dealerFields": [
// {
// "lineNumber": null,
// "dealerField": "Custom dealer field value 1"
// },
// {
// "lineNumber": null,
// "dealerField": "Custom dealer field value 2"
// },
// {
// "lineNumber": null,
// "dealerField": "Custom dealer field value 3"
// }
// ]
}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "InsertDmsCustomer", {
customerData: {
firstName: JobData.ownr_fn,
lastName: JobData.ownr_ln,
companyName: JobData.ownr_co_nm
},
jobId: JobData.id
});
throw error;
}
}
async function InsertDmsVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust }) {
try {
const result = await MakeFortellisCall({
...FortellisActions.InsertVehicle,
requestSearchParams: {},
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
dealer: {
//company: JobData.bodyshop.cdk_configuration.srcco || "77",
// "dealNumber": "",
// "dealerAssignedNumber": "82268",
// "dealerDefined1": "2WDSP",
// "dealerDefined2": "33732.71",
// "dealerDefined3": "",
// "dealerDefined4": "G0901",
// "dealerDefined5": "",
// "dealerDefined6": "",
// "dealerDefined7": "",
// "dealerDefined8": "",
// "dealerNumber": "",
...(txEnvelope.inservicedate && {
inServiceDate:
txEnvelope.dms_unsold === true
? ""
: moment(txEnvelope.inservicedate)
//.tz(JobData.bodyshop.timezone)
.startOf("day")
.toISOString()
}),
//"lastServiceDate": "2011-11-23",
vehicleId: DMSVid.vehiclesVehId
// "vehicleLocation": "",
// "vehicleSoldDate": "2021-04-06",
// "wholesaleVehicleInd": false
},
// "manufacturer": {
// "name": "",
// "plant": "",
// "productionNumber": "PZPKM6",
// "vehicleProductionDate": "2020-04-06"
// },
// "invoice": {
// "entryDate": "2012-01-19",
// "freight": {
// "freightInCharge": 995.95,
// "freightOutCharge": 95.95,
// "freightTaxCharge": 5.95
// },
// "vehicleAcquisitionDate": "2012-01-18",
// "vehicleOrderDate": "2012-01-12",
// "vehicleOrderNumber": "",
// "vehicleOrderPriority": "",
// "vehicleOrderType": "TRE RETAIL - STOCK"
// },
vehicle: {
// "axleCode": "GU6/3.42 REAR AXLE RATIO",
// "axleCount": 2,
// "bodyStyle": "PU",
// "brakeSystem": "",
// "cabType": "",
// "certifiedPreownedInd": false,
// "certifiedPreownedNumber": "",
// "chassis": "",
// "color": "",
// "dealerBodyStyle": "",
deliveryDate:
txEnvelope.dms_unsold === true
? ""
: moment()
// .tz(JobData.bodyshop.timezone)
.format("YYYY-MM-DD"),
// "deliveryMileage": 4,
// "doorsQuantity": 4,
// "engineNumber": "",
// "engineType": "LMG/VORTEC 5.3L VVT V8 SFI FLEXFUEL",
// "exteriorColor": "",
// "fleetVehicleId": "",
// "frontTireCode": "",
// "gmRPOCode": "",
// "ignitionKeyNumber": "",
// "interiorColor": "EBONY",
licensePlateNo:
JobData.plate_no === null
? null
: String(JobData.plate_no).replace(/([^\w]|_)/g, "").length === 0
? null
: String(JobData.plate_no)
.replace(/([^\w]|_)/g, "")
.toUpperCase(),
make: txEnvelope.dms_make,
// "model": "CC10753",
modelAbrev: txEnvelope.dms_model,
// "modelDescription": "SILVERADO 1500 2WD EXT CAB LT",
// "modelType": "T",
modelYear:
JobData.v_model_yr &&
(JobData.v_model_yr < 100
? JobData.v_model_yr >= (moment().year() + 1) % 100
? 1900 + parseInt(JobData.v_model_yr, 10)
: 2000 + parseInt(JobData.v_model_yr, 10)
: JobData.v_model_yr),
// "numberOfEngineCylinders": 4,
odometerStatus: txEnvelope.kmout,
// "options": [
// {
// "optionCategory": "SS",
// "optionCode": "A95",
// "optionCost": 875.6,
// "optionDescription": "FRONT BUCKET SEATS INCLUDING: PWR SEAT ADJUST DRIVER 6-WAY",
// "optionPrices": [
// {
// "optionPricingType": "RETAIL",
// "price": 995
// },
// {
// "optionPricingType": "INVOICE",
// "price": 875.6
// }
// ]
// },
// {
// "optionCategory": "E",
// "optionCode": "LMG",
// "optionCost": 0,
// "optionDescription": "VORTEC 5.3L V8 SFI ENGINE W/ACTIVE FUEL MANAGEMENT",
// "optionPrices": [
// {
// "optionPricingType": "RETAIL",
// "price": 0
// },
// {
// "optionPricingType": "INVOICE",
// "price": 0
// }
// ]
// }
// ],
// "rearTireCode": "",
// "restraintSystem": "",
saleClassValue: "MISC",
// "sourceCodeValue": "",
// "sourceDescription": "",
// "standardEquipment": "",
// "stickerNumber": "",
// "transmissionType": "A",
// "transmissionNo": "MYC/ELECTRONIC 6-SPEED AUTOMATIC W/OD",
// "trimCode": "",
// "vehicleNote": "",
// "vehiclePrices": [
// {
// "price": 35894,
// "vehiclePricingType": "SELLINGPRICE"
// },
// {
// "price": 33749.87,
// "vehiclePricingType": "INVOICE"
// },
// {
// "price": 36472,
// "vehiclePricingType": "RETAIL"
// },
// {
// "price": 28276.66,
// "vehiclePricingType": "BASEINVOICE"
// },
// {
// "price": 30405,
// "vehiclePricingType": "BASERETAIL"
// },
// {
// "price": 33749.87,
// "vehiclePricingType": "COMMISSIONPRICE"
// },
// {
// "price": 32702.9,
// "vehiclePricingType": "DRAFTAMOUNT"
// }
// ],
// "vehicleStatus": "G",
// "vehicleStock": "82268",
// "vehicleWeight": "6800",
vin: JobData.v_vin.toUpperCase()
// "warrantyExpDate": "2015-01-12",
// "wheelbase": ""
},
owners: [
{
id: {
assigningPartyId: "CURRENT",
value: DMSCust.customerId
}
}
]
//"inventoryAccount": "237"
}
});
return result.data;
} catch (error) {
handleFortellisApiError(socket, error, "InsertDmsVehicle", {
vin: JobData.v_vin,
vehicleId: DMSVid.vehiclesVehId,
customerId: DMSCust.customerId,
jobId: JobData.id
});
throw error;
}
}
async function UpdateDmsVehicle({ socket, redisHelpers, JobData, DMSVeh, DMSCust, selectedCustomerId, txEnvelope }) {
try {
let ids = [];
//if it's a generic customer, don't update the vehicle owners.
if (selectedCustomerId === JobData.bodyshop.cdk_configuration.generic_customer_number) {
ids = DMSVeh?.owners && DMSVeh.owners;
} else {
const existingOwnerinVeh = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.value === DMSCust.customerId);
if (existingOwnerinVeh) {
ids = DMSVeh.owners.map((o) => {
return {
id: {
assigningPartyId: o.id.value === DMSCust.customerId ? "CURRENT" : "PREVIOUS",
value: o.id.value
}
};
});
} else {
const oldOwner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
ids = [
{
id: {
assigningPartyId: "CURRENT",
value: DMSCust.customerId
}
},
...(oldOwner
? [
{
id: {
assigningPartyId: "PREVIOUS",
value: oldOwner.id.value
}
}
]
: [])
];
}
}
const DMSVehToSend = _.cloneDeep(DMSVeh);
//Remove unsupported fields on the post API.
delete DMSVehToSend.dealer.lastActivityDate;
delete DMSVehToSend.manufacturer;
delete DMSVehToSend.invoice;
delete DMSVehToSend.inventoryAccount;
!DMSVehToSend.vehicle.engineNumber && delete DMSVehToSend.vehicle.engineNumber;
!DMSVehToSend.vehicle.saleClassValue && (DMSVehToSend.vehicle.saleClassValue = "MISC");
!DMSVehToSend.vehicle.exteriorColor && delete DMSVehToSend.vehicle.exteriorColor;
const result = await MakeFortellisCall({
...FortellisActions.UpdateVehicle,
requestSearchParams: {},
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
...DMSVehToSend,
dealer: {
...DMSVehToSend.dealer, //TODO: Check why company is blank on a queried record.
...((txEnvelope.inservicedate || DMSVehToSend.dealer.inServiceDate) && {
inServiceDate:
txEnvelope.dms_unsold === true
? ""
: moment(DMSVehToSend.dealer.inServiceDate || txEnvelope.inservicedate)
// .tz(JobData.bodyshop.timezone)
.toISOString()
})
},
vehicle: {
...DMSVehToSend.vehicle,
...(txEnvelope.dms_model_override
? {
make: txEnvelope.dms_make,
modelAbrev: txEnvelope.dms_model
}
: {}),
deliveryDate:
txEnvelope.dms_unsold === true
? ""
: moment(DMSVehToSend.vehicle.deliveryDate)
//.tz(JobData.bodyshop.timezone)
.toISOString()
},
owners: ids
}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "UpdateDmsVehicle", {
vin: JobData.v_vin,
vehicleId: DMSVeh?.dealer?.vehicleId,
customerId: DMSCust?.customerId,
selectedCustomerId,
jobId: JobData.id
});
throw error;
}
}
async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) {
try {
const txEnvelope = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.txEnvelope
);
const DMSVid = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.DMSVid
);
const result = await MakeFortellisCall({
...FortellisActions.ServiceHistoryInsert,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
header: {
vehId: DMSVid.vehiclesVehId,
roNumber: JobData.ro_number.match(/\d+/g)[0],
mileage: txEnvelope.kmout,
openDate: moment(JobData.actual_in).tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"),
openTime: moment(JobData.actual_in).tz(JobData.bodyshop.timezone).format("HH:mm:ss"),
closeDate: moment(JobData.invoice_date).tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"),
closeTime: moment(JobData.invoice_date).tz(JobData.bodyshop.timezone).format("HH:mm:ss"),
comments: txEnvelope.story?.slice(0, 40).toUpperCase(), // has to be between 0 and 40.
cashierId: JobData.bodyshop.cdk_configuration.cashierid,
referenceNumber: JobData.ro_number.match(/\d+/g)[0]
}
}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "InsertServiceVehicleHistory", {
roNumber: JobData.ro_number,
jobId: JobData.id
});
throw error;
}
}
async function InsertDmsStartWip({ socket, redisHelpers, JobData }) {
try {
const txEnvelope = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.txEnvelope
);
const result = await MakeFortellisCall({
...FortellisActions.StartWip,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
acctgDate: moment().tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"),
desc: txEnvelope.story && txEnvelope.story.replace(replaceSpecialRegex, "").toUpperCase(),
docType: "10",
m13Flag: "0",
refer: JobData.ro_number,
// "rtnCode": "",
// "sendline": "",
// "groupName": "",
srcCo: JobData.bodyshop.cdk_configuration.srcco,
srcJrnl: txEnvelope.journal,
transID: "",
userID: JobData.bodyshop.cdk_configuration.cashierid,
userName: "IMEX"
//Cert Values Below
// userID: "partprgm",
// userName: "PROGRAM, PARTNER"
// acctgDate: "2025-07-07",
// desc: "DOCUMENT DESC. OPTIONAL REQUIREMENT",
// docType: "3",
// m13Flag: "0",
// refer: "707MISC01",
// rtnCode: "",
// sendline: "",
// groupName: "",
// srcCo: "77",
// srcJrnl: "80",
// transID: "",
// userID: "partprgm",
// userName: "PROGRAM, PARTNER"
}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "InsertDmsStartWip", {
roNumber: JobData.ro_number,
jobId: JobData.id
});
throw error;
}
}
async function GenerateTransWips({ socket, redisHelpers, JobData }) {
//3rd prop sets fortellis to true to maintain logging.
const allocations = await CalculateAllocations(socket, JobData.id, true);
const wips = [];
const DMSTransHeader = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.DMSTransHeader
);
allocations.forEach((alloc) => {
//Add the sale item from each allocation.
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
const item = {
acct: alloc.profitCenter.dms_acctnumber,
cntl:
alloc.profitCenter.dms_control_override &&
alloc.profitCenter.dms_control_override !== null &&
alloc.profitCenter.dms_control_override !== undefined &&
alloc.profitCenter.dms_control_override?.trim() !== ""
? alloc.profitCenter.dms_control_override
: JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.sale.multiply(-1).getAmount(),
postDesc: null,
prod: null,
statCnt: 1,
transID: DMSTransHeader.transID,
trgtCoID: JobData.bodyshop.cdk_configuration.srcco
};
wips.push(item);
}
//Add the cost Item.
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
const item = {
acct: alloc.costCenter.dms_acctnumber,
cntl:
alloc.costCenter.dms_control_override &&
alloc.costCenter.dms_control_override !== null &&
alloc.costCenter.dms_control_override !== undefined &&
alloc.costCenter.dms_control_override?.trim() !== ""
? alloc.costCenter.dms_control_override
: JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.cost.getAmount(),
postDesc: null,
prod: null,
statCnt: 1,
transID: DMSTransHeader.transID,
trgtCoID: JobData.bodyshop.cdk_configuration.srcco
};
wips.push(item);
const itemWip = {
acct: alloc.costCenter.dms_wip_acctnumber,
cntl:
alloc.costCenter.dms_control_override &&
alloc.costCenter.dms_control_override !== null &&
alloc.costCenter.dms_control_override !== undefined &&
alloc.costCenter.dms_control_override?.trim() !== ""
? alloc.costCenter.dms_control_override
: JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.cost.multiply(-1).getAmount(),
postDesc: null,
prod: null,
statCnt: 1,
transID: DMSTransHeader.transID,
trgtCoID: JobData.bodyshop.cdk_configuration.srcco
};
wips.push(itemWip);
//Add to the WIP account.
}
if (alloc.tax) {
if (alloc.sale.getAmount() > 0) {
const item2 = {
acct: alloc.profitCenter.dms_acctnumber,
cntl:
alloc.profitCenter.dms_control_override &&
alloc.profitCenter.dms_control_override !== null &&
alloc.profitCenter.dms_control_override !== undefined &&
alloc.profitCenter.dms_control_override?.trim() !== ""
? alloc.profitCenter.dms_control_override
: JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.sale.multiply(-1).getAmount(),
postDesc: null,
prod: null,
statCnt: 1,
transID: DMSTransHeader.transID,
trgtCoID: JobData.bodyshop.cdk_configuration.srcco
};
wips.push(item2);
}
}
});
const txEnvelope = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.txEnvelope
);
txEnvelope.payers.forEach((payer) => {
const item = {
acct: payer.dms_acctnumber,
cntl: payer.controlnumber,
cntl2: null,
credtMemoNo: null,
postAmt: Math.round(payer.amount * 100),
postDesc: null,
prod: null,
statCnt: 1,
transID: DMSTransHeader.transID,
trgtCoID: JobData.bodyshop.cdk_configuration.srcco
};
wips.push(item);
});
await redisHelpers.setSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.transWips,
wips,
defaultFortellisTTL
);
return wips;
}
async function PostDmsBatchWip({ socket, redisHelpers, JobData }) {
let DMSTransHeader;
try {
DMSTransHeader = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.DMSTransHeader
);
const result = await MakeFortellisCall({
...FortellisActions.PostBatchWip,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
opCode: "P",
transID: DMSTransHeader.transID,
transWipReqList: await GenerateTransWips({ socket, redisHelpers, JobData })
}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "PostDmsBatchWip", {
transId: DMSTransHeader?.transID,
jobId: JobData.id
});
throw error;
}
}
async function QueryDmsErrWip({ socket, redisHelpers, JobData }) {
let DMSTransHeader;
try {
DMSTransHeader = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.DMSTransHeader
);
const result = await MakeFortellisCall({
...FortellisActions.QueryErrorWip,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
requestPathParams: DMSTransHeader.transID,
body: {}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "QueryDmsErrWip", {
transId: DMSTransHeader?.transID,
jobId: JobData.id
});
throw error;
}
}
async function DeleteDmsWip({ socket, redisHelpers, JobData }) {
let DMSTransHeader;
try {
DMSTransHeader = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.DMSTransHeader
);
const result = await MakeFortellisCall({
...FortellisActions.DeleteTranWip,
headers: {},
redisHelpers,
socket,
jobid: JobData.id,
body: {
opCode: "D",
transID: DMSTransHeader.transID
}
});
return result;
} catch (error) {
handleFortellisApiError(socket, error, "DeleteDmsWip", {
transId: DMSTransHeader?.transID,
jobId: JobData.id
});
throw error;
}
}
async function MarkJobExported({ socket, jobid, JobData, redisHelpers }) {
CreateFortellisLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
const transwips = await redisHelpers.getSessionTransactionData(
socket.id,
getTransactionType(JobData.id),
FortellisCacheEnums.transWips
);
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.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
status: JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date()
},
log: {
bodyshopid: JobData.bodyshop.id,
jobid: jobid,
successful: true,
useremail: socket.user.email,
metadata: transwips
},
bill: {
exported: true,
exported_at: new Date()
}
});
return result;
}
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);
const result = await client
.setHeaders({ Authorization: `Bearer ${currentToken}` })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: JobData.bodyshop.id,
jobid: JobData.id,
successful: false,
message: JSON.stringify(error),
useremail: socket.user.email
}
]
});
return result;
} catch (error2) {
CreateFortellisLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error}`, {
message: error2.message,
stack: error2.stack
});
}
}
exports.getTransactionType = getTransactionType;
exports.FortellisJobExport = FortellisJobExport;