Merge remote-tracking branch 'origin/feature/IO-2776-cdk-fortellis' into feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration

This commit is contained in:
Dave
2025-10-06 12:56:01 -04:00
41 changed files with 1164 additions and 235 deletions

View File

@@ -177,11 +177,9 @@ exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
async function QueryBillData(socket, billids) {
WsLogger.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
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}` })
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
WsLogger.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
@@ -201,11 +199,9 @@ function getCostAccount(billline, respcenters) {
async function MarkApExported(socket, billids) {
WsLogger.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
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}` })
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_BILLS_EXPORTED, {
billids,
bill: {

View File

@@ -6,10 +6,6 @@ const PBS_CREDENTIALS = {
};
exports.PBS_CREDENTIALS = PBS_CREDENTIALS;
// const cdkDomain =
// process.env.NODE_ENV === "production"
// ? "https://3pa.dmotorworks.com"
// : "https://uat-3pa.dmotorworks.com";
const pbsDomain = `https://partnerhub.pbsdealers.com/json/reply`;
exports.PBS_ENDPOINTS = {
@@ -18,5 +14,9 @@ exports.PBS_ENDPOINTS = {
VehicleGet: `${pbsDomain}/VehicleGet`,
AccountingPostingChange: `${pbsDomain}/AccountingPostingChange`,
ContactChange: `${pbsDomain}/ContactChange`,
VehicleChange: `${pbsDomain}/VehicleChange`
VehicleChange: `${pbsDomain}/VehicleChange`,
RepairOrderChange: `${pbsDomain}/RepairOrderChange`, //TODO: Verify that this is correct. Docs have /reply/ in path.
RepairOrderGet: `${pbsDomain}/RepairOrderGet`,
RepairOrderContactVehicleGet: `${pbsDomain}/RepairOrderContactVehicleGet`,
RepairOrderContactVehicleChange: `${pbsDomain}/RepairOrderContactVehicleChange`,
};

View File

@@ -3,6 +3,8 @@ const AxiosLib = require("axios").default;
const queries = require("../../graphql-client/queries");
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
const WsLogger = require("../../web-sockets/createLogEvent");
const fs = require("fs");
const path = require("path");
//const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").default;
@@ -22,9 +24,9 @@ axios.interceptors.request.use((x) => {
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
//logRequestToFile(printable);
WsLogger.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
WsLogger.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -32,23 +34,36 @@ axios.interceptors.request.use((x) => {
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
WsLogger.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
const printable = `${new Date()} | Response: ${x.status} ${x.statusText} |${JSON.stringify(x.data)}`;
//logRequestToFile(printable);
WsLogger.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
return x;
});
function logRequestToFile(printable) {
try {
const logDir = path.join(process.cwd(), "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, "pbs-http.log");
fs.appendFileSync(logFile, `${printable}\n`);
} catch (err) {
console.error("Unexpected error in logRequestToFile:", err);
}
}
const defaultHandler = async (socket, { txEnvelope, jobid }) => {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
WsLogger.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
WsLogger.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
socket.JobData = JobData;
WsLogger.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
WsLogger.createLogEvent(socket, "INFO", `Querying the DMS for the Vehicle Record.`);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
//Todo: Need to validate the lines and methods below.
@@ -71,42 +86,53 @@ exports.default = defaultHandler;
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
WsLogger.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
socket.selectedCustomerId = selectedCustomerId;
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle !== true) {
WsLogger.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
//Upsert the contact information as per Wafaa's Email.
WsLogger.createLogEvent(
socket,
"DEBUG",
"INFO",
`Upserting contact information to DMS for ${
socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
socket.ownerRef = ownerRef;
WsLogger.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
WsLogger.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
socket.vehicleRef = vehicleRef;
} else {
WsLogger.createLogEvent(
socket,
"DEBUG",
`Contact and Vehicle updates disabled. Skipping to accounting data insert.`
"INFO",
`Contact and Vehicle updates disabled. Querying data and skipping to accounting data insert.`
);
//Must query for records to insert $0 RO.
if (!socket.ownerRef) {
const ownerRef = (await QueryCustomerBycodeFromDms(socket, selectedCustomerId))?.[0];
socket.ownerRef = ownerRef;
}
const vehicleRef = await GetVehicleData(socket, socket.ownerRef?.ReferenceId || socket.selectedCustomerId);
socket.vehicleRef = vehicleRef;
}
WsLogger.createLogEvent(socket, "DEBUG", `Inserting account data.`);
WsLogger.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
WsLogger.createLogEvent(socket, "INFO", `Inserting account posting data...`);
const insertResponse = await InsertAccountPostingData(socket);
if (insertResponse.WasSuccessful) {
WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
if (socket.JobData.bodyshop.pbs_configuration.ro_posting) {
await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef);
}
WsLogger.createLogEvent(socket, "INFO", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
socket.emit("export-success", socket.JobData.id);
} else {
WsLogger.createLogEvent(socket, "ERROR", `Export was not successful.`);
}
} catch (error) {
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
@@ -114,26 +140,24 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
// Was Successful
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
WsLogger.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
WsLogger.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
} else {
WsLogger.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
WsLogger.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
WsLogger.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
WsLogger.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
WsLogger.createLogEvent(socket, "INFO", `Querying job data for id ${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}` })
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
WsLogger.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
WsLogger.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -338,7 +362,7 @@ async function UpsertVehicleData(socket, ownerRef) {
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
ModelNumber: socket.JobData.vehicle?.v_makecode,
// ModelNumber: socket.JobData.vehicle?.v_makecode,
Make: socket.JobData.v_make_desc,
Model: socket.JobData.v_model_desc,
Trim: socket.JobData.vehicle?.v_trimcode,
@@ -346,7 +370,7 @@ async function UpsertVehicleData(socket, ownerRef) {
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
ExteriorColor: {
Code: socket.JobData.v_color,
// Code: socket.JobData.v_color,
Description: socket.JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
@@ -476,6 +500,56 @@ async function UpsertVehicleData(socket, ownerRef) {
}
}
async function GetVehicleData(socket, ownerRef) {
try {
const {
data: { Vehicles }
} = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
// "VehicleId": "00000000000000000000000000000000",
// "Year": "String",
// "YearFrom": "String",
// "YearTo": "String",
// "Make": "String",
// "Model": "String",
// "Trim": "String",
// "ModelNumber": "String",
// "StockNumber": "String",
VIN: socket.JobData.v_vin
// "LicenseNumber": "String",
// "Lot": "String",
// "Status": "String",
// "StatusList": ["String"],
// "OwnerRef": "00000000000000000000000000000000",
// "ModifiedSince": "0001-01-01T00:00:00.0000000Z",
// "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
// "LastSaleSince": "0001-01-01T00:00:00.0000000Z",
// "VehicleIDList": ["00000000000000000000000000000000"],
// "IncludeInactive": false,
// "IncludeBuildVehicles": false,
// "IncludeBlankLot": false,
// "ShortVIN": "String",
// "ResultLimit": 0,
// "LotAccessDivisions": [0],
// "OdometerTo": 0,
// "OdometerFrom": 0
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, Vehicles);
if (Vehicles.length === 1) {
return Vehicles[0];
} else {
WsLogger.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
}
} catch (error) {
WsLogger.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
throw new Error(error);
}
}
async function InsertAccountPostingData(socket) {
try {
const allocations = await CalculateAllocations(socket, socket.JobData.id);
@@ -579,13 +653,11 @@ async function InsertAccountPostingData(socket) {
}
async function MarkJobExported(socket, jobid) {
WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
WsLogger.createLogEvent(socket, "INFO", `Marking job as exported for id ${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}` })
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
@@ -611,11 +683,9 @@ async function MarkJobExported(socket, jobid) {
async function InsertFailedExportLog(socket, 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}` })
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: socket.JobData.bodyshop.id,
@@ -631,3 +701,161 @@ async function InsertFailedExportLog(socket, error) {
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
}
}
async function CreateRepairOrderInPBS(socket) {
try {
const { RepairOrders } = await RepairOrderGet(socket);
if (RepairOrders.length === 0) {
const InsertedRepairOrder = await RepairOrderChange(socket);
socket.InsertedRepairOrder = InsertedRepairOrder;
WsLogger.createLogEvent(socket, "INFO", `No repair orders found for vehicle. Inserting record.`);
} else if (RepairOrders.length > 0) {
//Find out if it's a matching RO.
//This logic is used because the integration will simply add another line to an open RO if it exists.
const matchingRo = RepairOrders.find((ro) =>
ro.Memo?.toLowerCase()?.includes(socket.JobData.ro_number.toLowerCase())
);
if (!matchingRo) {
WsLogger.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
const InsertedRepairOrder = await RepairOrderChange(socket);
socket.InsertedRepairOrder = InsertedRepairOrder;
} else {
WsLogger.createLogEvent(
socket,
"WARN",
`Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`
);
}
}
} catch (error) {
WsLogger.createLogEvent(socket, "ERROR", `Error in CreateRepairOrderInPBS - ${error} - ${JSON.stringify(error)}`);
}
}
async function RepairOrderGet(socket) {
try {
const { data: RepairOrderGet } = await axios.post(
PBS_ENDPOINTS.RepairOrderGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//"RepairOrderId": "374728766",
//"RepairOrderNumber": "4" || socket.JobData.ro_number,
//"RawRepairOrderNumber": socket.JobData.ro_number,
// "Tag": "String",
//"ContactRef": socket.contactRef,
// "ContactRefList": ["00000000000000000000000000000000"],
VehicleRef: socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId
// "VehicleRefList": ["00000000000000000000000000000000"],
// "Status": "String",
// "CashieredSince": "0001-01-01T00:00:00.0000000Z",
// "CashieredUntil": "0001-01-01T00:00:00.0000000Z",
// "OpenDateSince": "0001-01-01T00:00:00.0000000Z",
// "OpenDateUntil": "0001-01-01T00:00:00.0000000Z",
//"ModifiedSince": "2025-01-01T00:00:00.0000000Z",
// "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
// "Shop": "String"
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, RepairOrderGet);
return RepairOrderGet;
} catch (error) {
WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
throw new Error(error);
}
}
async function RepairOrderChange(socket) {
try {
const { data: RepairOrderChangeResponse } = await axios.post(
PBS_ENDPOINTS.RepairOrderChange,
{
//Additional details at https://partnerhub.pbsdealers.com/json/metadata?op=RepairOrderChange
RepairOrderInfo: {
//"Id": "string/00000000-0000-0000-0000-000000000000",
//"RepairOrderId": "00000000000000000000000000000000",
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
RepairOrderNumber: "00000000000000000000000000000000", //This helps force a new RO.
RawRepairOrderNumber: "00000000000000000000000000000000",
// "RepairOrderNumber": socket.JobData.ro_number, //These 2 values are ignored as confirmed by PBS.
// "RawRepairOrderNumber": socket.JobData.ro_number,
DateOpened: moment(),
// "DateOpenedUTC": "0001-01-01T00:00:00.0000000Z",
// "DateCashiered": "0001-01-01T00:00:00.0000000Z",
// "DateCashieredUTC": "0001-01-01T00:00:00.0000000Z",
DatePromised: socket.JobData.scheduled_completion,
// "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z",
DateVehicleCompleted: socket.JobData.actual_completion,
// "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z",
// "CSR": "String",
// "CSRRef": "00000000000000000000000000000000",
// "BookingUser": "String",
// "BookingUserRef": "00000000000000000000000000000000",
ContactRef: socket.ownerRef?.ReferenceId || socket.ownerRef?.ContactId,
VehicleRef: socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
MileageIn: socket.JobData.km_in,
Tag: "BODYSHOP",
//"Status": "CLOSED", //Values here do not impact the status. Confirmed by PBS support.
Requests: [
{
// "RepairOrderRequestRef": "b1842ecad62c4279bbc2fef4f6bf6cde",
// "RepairOrderRequestId": 1,
// "CSR": "PBS",
// "CSRRef": "1ce12ac692564e94bda955d529ee911a",
// "Skill": "GEN",
RequestCode: "MISC",
RequestDescription: `VEHICLE REPAIRED AT BODYSHOP. PLEASE REFERENCE IMEX SHOP MANAGEMENT SYSTEM. ${socket.txEnvelope.story}`,
Status: "Completed",
// "TechRef": "00000000000000000000000000000000",
AllowedHours: 0,
EstimateLabour: 0,
EstimateParts: 0,
ComeBack: false,
AddedOperation: true,
PartLines: [],
PartRequestLines: [],
LabourLines: [],
SubletLines: [],
TimePunches: [],
Summary: {
Labour: 0,
Parts: 0,
OilGas: 0,
SubletTow: 0,
Misc: 0,
Environment: 0,
ShopSupplies: 0,
Freight: 0,
WarrantyDeductible: 0,
Discount: 0,
SubTotal: 0,
Tax1: 0,
Tax2: 0,
InvoiceTotal: 0,
CustomerDeductible: 0,
GrandTotal: 0,
LabourDiscount: 0,
PartDiscount: 0,
ServiceFeeTotal: 0,
OEMDiscount: 0
},
LineType: "RequestLine"
}
],
Memo: socket.txEnvelope.story
},
IsAsynchronous: false
// "UserRequest": "String",
// "UserRef": "00000000000000000000000000000000"
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, RepairOrderChangeResponse);
return RepairOrderChangeResponse;
} catch (error) {
WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
throw new Error(error);
}
}

View File

@@ -205,21 +205,49 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) {
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid);
let lines;
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
lines = Object.values(
bill.billlines.reduce((acc, il) => {
const { cost_center, actual_cost, quantity = 1 } = il;
const lines = bill.billlines.map((il) =>
generateBillLine(
il,
accounts,
bill.job.class,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bodyshop.md_responsibility_centers.costs,
bodyshop.accountingconfig,
bodyshop.region_config
)
);
if (!acc[cost_center]) {
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
}
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
return acc;
}, {})
).map((il) => {
il.actual_cost /= 100;
return generateBillLine(
il,
accounts,
bill.job.class,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bodyshop.md_responsibility_centers.costs,
bodyshop.accountingconfig,
bodyshop.region_config
);
});
} else {
lines = bill.billlines.map((il) =>
generateBillLine(
il,
accounts,
bill.job.class,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bodyshop.md_responsibility_centers.costs,
bodyshop.accountingconfig,
bodyshop.region_config
)
);
}
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
@@ -241,7 +269,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
Amount: Dinero({
amount: Math.round(
bill.billlines.reduce((acc, val) => {
return acc + (val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0);
return acc + (val.applicable_taxes?.federal ? val.actual_cost * val.quantity || 0 : 0);
}, 0) * 100
)
})

View File

@@ -46,6 +46,28 @@ exports.default = async (req, res) => {
};
const generateBill = (bill, bodyshop) => {
let lines;
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
lines = Object.values(
bill.billlines.reduce((acc, il) => {
const { cost_center, actual_cost, quantity = 1 } = il;
if (!acc[cost_center]) {
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
}
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
return acc;
}, {})
).map((il) => {
il.actual_cost /= 100;
return generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class);
});
} else {
lines = bill.billlines.map((il) => generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class));
}
const billQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
@@ -67,9 +89,7 @@ const generateBill = (bill, bodyshop) => {
}),
RefNumber: bill.invoice_number,
Memo: `RO ${bill.job.ro_number || ""}`,
ExpenseLineAdd: bill.billlines.map((il) =>
generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class)
)
ExpenseLineAdd: lines
}
}
}

441
server/data/carfax-rps.js Normal file
View File

@@ -0,0 +1,441 @@
const queries = require("../graphql-client/queries");
const moment = require("moment-timezone");
const logger = require("../utils/logger");
const fs = require("fs");
const client = require("../graphql-client/graphql-client").rpsClient;
const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail");
const crypto = require("crypto");
const { ftpSetup, uploadToS3 } = require("./carfax");
let Client = require("ssh2-sftp-client");
const AHDateFormat = "YYYY-MM-DD";
const NON_ASCII_REGEX = /[^\x20-\x7E]/g;
const S3_BUCKET_NAME = "rps-carfax-uploads";
const carfaxExportRps = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
return res.sendStatus(403);
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
return res.sendStatus(401);
}
// Send immediate response and continue processing.
res.status(202).json({
success: true,
message: "Processing request ...",
timestamp: new Date().toISOString()
});
try {
logger.log("CARFAX-RPS-start", "DEBUG", "api", null, null);
const allJSONResults = [];
const allErrors = [];
const { bodyshops } = await client.request(queries.GET_CARFAX_RPS_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload, ignoreDateFilter } = req.body; //YYYY-MM-DD
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("CARFAX-RPS-shopsToProcess-generated", "DEBUG", "api", null, null);
if (shopsToProcess.length === 0) {
logger.log("CARFAX-RPS-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors);
await sendServerEmail({
subject: `Project Mexico RPS Report ${moment().format("MM-DD-YY")}`,
text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allJSONResults.map((x) => ({
imexshopid: x.imexshopid,
filename: x.filename,
count: x.count,
result: x.result
})),
null,
2
)}`,
to: ["bradley.rhoades@convenient-brands.com"]
});
logger.log("CARFAX-RPS-end", "DEBUG", "api", null, null);
} catch (error) {
logger.log("CARFAX-RPS-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
}
};
async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) {
for (const bodyshop of shopsToProcess) {
const shopid = bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
const erroredJobs = [];
try {
logger.log("CARFAX-RPS-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_RPS_QUERY, {
bodyshopid: bodyshop.id,
...(ignoreDateFilter
? {}
: {
starttz: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"),
...(end && { endtz: moment(end).endOf("day") }),
start: start
? moment(start).startOf("day").format(AHDateFormat)
: moment().subtract(7, "days").startOf("day").format(AHDateFormat),
...(end && { endtz: moment(end).endOf("day").format(AHDateFormat) })
})
});
const carfaxObject = {
shopid: shopid,
shop_name: bodyshop.shopname,
job: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
};
if (erroredJobs.length > 0) {
logger.log("CARFAX-RPS-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.id))
});
}
const jsonObj = {
bodyshopid: bodyshop.id,
imexshopid: shopid,
json: JSON.stringify(carfaxObject, null, 2),
filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
count: carfaxObject.job.length
};
if (skipUpload) {
fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
uploadToS3(jsonObj, S3_BUCKET_NAME);
} else {
await uploadViaSFTP(jsonObj);
await sendMexicoBillingEmail({
subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
text: `Errors:\n${JSON.stringify(
erroredJobs.map((ej) => ({
jobid: ej.job?.id,
error: ej.error
})),
null,
2
)}\n\nUploaded:\n${JSON.stringify(
{
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
},
null,
2
)}`
});
}
allJSONResults.push({
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
});
logger.log("CARFAX-RPS-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("CARFAX-RPS-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: shopid,
CARFAXid: bodyshop.CARFAXid,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: shopid,
CARFAXid: bodyshop.CARFAXid,
errors: erroredJobs.map((ej) => ({
jobid: ej.job?.id,
error: ej.error
}))
});
}
}
}
async function uploadViaSFTP(jsonObj) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("CARFAX-RPS-sftp-connection-error", "ERROR", "api", jsonObj.bodyshopid, {
error: errors.message,
stack: errors.stack
})
);
try {
// Upload to S3 first.
uploadToS3(jsonObj, S3_BUCKET_NAME);
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
try {
jsonObj.result = await sftp.put(Buffer.from(jsonObj.json), `${jsonObj.filename}`);
logger.log("CARFAX-RPS-sftp-upload", "DEBUG", "api", jsonObj.bodyshopid, {
imexshopid: jsonObj.imexshopid,
filename: jsonObj.filename,
result: jsonObj.result
});
} catch (error) {
logger.log("CARFAX-RPS-sftp-upload-error", "ERROR", "api", jsonObj.bodyshopid, {
filename: jsonObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
} catch (error) {
logger.log("CARFAX-RPS-sftp-error", "ERROR", "api", jsonObj.bodyshopid, {
error: error.message,
stack: error.stack
});
throw error;
} finally {
sftp.end();
}
}
const CreateRepairOrderTag = (job, errorCallback) => {
try {
const subtotalEntry = job.totals.find((total) => total.TTL_TYPECD === "");
const subtotal = subtotalEntry ? subtotalEntry.T_AMT : 0;
const ret = {
ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"),
v_vin: job.v_vin || "",
v_year: job.v_model_yr
? parseInt(job.v_model_yr.match(/\d/g))
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
: ""
: "",
v_make: job.v_makedesc || "",
v_model: job.v_model || "",
date_estimated: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "",
data_opened: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "",
date_invoiced: [job.close_date, job.created_at].find((date) => date)
? moment([job.close_date, job.created_at].find((date) => date))
.tz("America/Winnipeg")
.format(AHDateFormat)
: "",
loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "",
ins_co_nm: job.ins_co_nm || "",
loss_desc: job.loss_desc || "",
theft_ind: job.theft_ind,
tloss_ind: job.tlos_ind,
subtotal: subtotal,
areaofdamage: {
impact1: generateAreaOfDamage(job.impact_1 || ""),
impact2: generateAreaOfDamage(job.impact_2 || "")
},
jobLines: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl)) : [generateNullDetailLine()]
};
return ret;
} catch (error) {
logger.log("CARFAX-RPS-job-data-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
errorCallback({ jobid: job.id, error });
}
};
const GenerateDetailLines = (line) => {
const ret = {
line_desc: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : null,
oem_partno: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : null,
alt_partno: line.alt_partno ? line.alt_partno.replace(NON_ASCII_REGEX, "") : null,
op_code_desc: generateOpCodeDescription(line.lbr_op),
lbr_ty: generateLaborType(line.mod_lbr_ty),
lbr_hrs: line.mod_lb_hrs || 0,
part_qty: line.part_qty || 0,
part_type: generatePartType(line.part_type),
act_price: line.act_price || 0
};
return ret;
};
const generateNullDetailLine = () => {
return {
line_desc: null,
oem_partno: null,
alt_partno: null,
lbr_ty: null,
part_qty: 0,
part_type: null,
act_price: 0
};
};
const generateAreaOfDamage = (loc) => {
const areaMap = {
"01": "Right Front Corner",
"02": "Right Front Side",
"03": "Right Side",
"04": "Right Rear Side",
"05": "Right Rear Corner",
"06": "Rear",
"07": "Left Rear Corner",
"08": "Left Rear Side",
"09": "Left Side",
10: "Left Front Side",
11: "Left Front Corner",
12: "Front",
13: "Rollover",
14: "Uknown",
15: "Total Loss",
16: "Non-Collision",
19: "All Over",
25: "Hood",
26: "Deck Lid",
27: "Roof",
28: "Undercarriage",
34: "All Over"
};
return areaMap[loc] || null;
};
const generateLaborType = (type) => {
const laborTypeMap = {
laa: "Aluminum",
lab: "Body",
lad: "Diagnostic",
lae: "Electrical",
laf: "Frame",
lag: "Glass",
lam: "Mechanical",
lar: "Refinish",
las: "Structural",
lau: "Other - LAU",
la1: "Other - LA1",
la2: "Other - LA2",
la3: "Other - LA3",
la4: "Other - LA4",
null: "Other",
mapa: "Paint Materials",
mash: "Shop Materials",
rates_subtotal: "Labor Total",
"timetickets.labels.shift": "Shift",
"timetickets.labels.amshift": "Morning Shift",
"timetickets.labels.ambreak": "Morning Break",
"timetickets.labels.pmshift": "Afternoon Shift",
"timetickets.labels.pmbreak": "Afternoon Break",
"timetickets.labels.lunch": "Lunch"
};
return laborTypeMap[type?.toLowerCase()] || null;
};
const generatePartType = (type) => {
const partTypeMap = {
paa: "Aftermarket",
pae: "Existing",
pag: "Glass",
pal: "LKQ",
pan: "OEM",
pao: "Other",
pas: "Sublet",
pasl: "Sublet",
ccc: "CC Cleaning",
ccd: "CC Damage Waiver",
ccdr: "CC Daily Rate",
ccf: "CC Refuel",
ccm: "CC Mileage",
prt_dsmk_total: "Line Item Adjustment"
};
return partTypeMap[type?.toLowerCase()] || null;
};
const generateOpCodeDescription = (type) => {
const opCodeMap = {
OP0: "REMOVE / REPLACE PARTIAL",
OP1: "REFINISH / REPAIR",
OP10: "REPAIR , PARTIAL",
OP100: "REPLACE PRE-PRICED",
OP101: "REMOVE/REPLACE RECYCLED PART",
OP103: "REMOVE / REPLACE PARTIAL",
OP104: "REMOVE / REPLACE PARTIAL LABOUR",
OP105: "!!ADJUST MANUALLY!!",
OP106: "REPAIR , PARTIAL",
OP107: "CHIPGUARD",
OP108: "MULTI TONE",
OP109: "REPLACE PRE-PRICED",
OP11: "REMOVE / REPLACE",
OP110: "REFINISH / REPAIR",
OP111: "REMOVE / REPLACE",
OP112: "REMOVE / REPLACE",
OP113: "REPLACE PRE-PRICED",
OP114: "REPLACE PRE-PRICED",
OP12: "REMOVE / REPLACE PARTIAL",
OP120: "REPAIR , PARTIAL",
OP13: "ADDITIONAL COSTS",
OP14: "ADDITIONAL OPERATIONS",
OP15: "BLEND",
OP16: "SUBLET",
OP17: "POLICY LIMIT ADJUSTMENT",
OP18: "APPEAR ALLOWANCE",
OP2: "REMOVE / INSTALL",
OP24: "CHIPGUARD",
OP25: "TWO TONE",
OP26: "PAINTLESS DENT REPAIR",
OP260: "SUBLET",
OP3: "ADDITIONAL LABOR",
OP4: "ALIGNMENT",
OP5: "OVERHAUL",
OP6: "REFINISH",
OP7: "INSPECT",
OP8: "CHECK / ADJUST",
OP9: "REPAIR"
};
return opCodeMap[type?.toUpperCase()] || null;
};
const errorCode = ({ count, filename, results }) => {
if (count === 0) return 1;
if (!filename) return 3;
const sftpErrorCode = results?.sftpError?.code;
if (sftpErrorCode && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(sftpErrorCode)) {
return 4;
}
if (sftpErrorCode) return 7;
return 0;
};
module.exports = {
default: carfaxExportRps,
ftpSetup
};

View File

@@ -37,12 +37,12 @@ const S3_BUCKET_NAME = InstanceManager({
const region = InstanceManager.InstanceRegion;
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
const uploadToS3 = (jsonObj) => {
const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => {
const webPath = isLocal
? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
: `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
: `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: jsonObj.filename, content: jsonObj.json })
uploadFileToS3({ bucketName: bucketName, key: jsonObj.filename, content: jsonObj.json })
.then(() => {
logger.log("CARFAX-s3-upload", "DEBUG", "api", jsonObj.bodyshopid, {
imexshopid: jsonObj.imexshopid,
@@ -61,7 +61,7 @@ const uploadToS3 = (jsonObj) => {
});
};
exports.default = async (req, res) => {
const carfaxExport = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
return res.sendStatus(403);
@@ -80,7 +80,7 @@ exports.default = async (req, res) => {
try {
logger.log("CARFAX-start", "DEBUG", "api", null, null);
const allXMLResults = [];
const allJSONResults = [];
const allErrors = [];
const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients.
@@ -96,12 +96,12 @@ exports.default = async (req, res) => {
return;
}
await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors);
await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors);
await sendServerEmail({
subject: `Project Mexico Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allXMLResults.map((x) => ({
text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allJSONResults.map((x) => ({
imexshopid: x.imexshopid,
filename: x.filename,
count: x.count,
@@ -109,7 +109,8 @@ exports.default = async (req, res) => {
})),
null,
2
)}`
)}`,
to: ["bradley.rhoades@convenient-brands.com"]
});
logger.log("CARFAX-end", "DEBUG", "api", null, null);
@@ -118,7 +119,7 @@ exports.default = async (req, res) => {
}
};
async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) {
async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) {
for (const bodyshop of shopsToProcess) {
const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
const erroredJobs = [];
@@ -195,7 +196,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
});
}
allXMLResults.push({
allJSONResults.push({
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
@@ -447,3 +448,9 @@ const errorCode = ({ count, filename, results }) => {
if (sftpErrorCode) return 7;
return 0;
};
module.exports = {
default: carfaxExport,
ftpSetup,
uploadToS3
};

View File

@@ -7,4 +7,5 @@ exports.usageReport = require("./usageReport").default;
exports.podium = require("./podium").default;
exports.emsUpload = require("./emsUpload").default;
exports.carfax = require("./carfax").default;
exports.carfaxRps = require("./carfax-rps").default;
exports.vehicletype = require("./vehicletype/vehicletype").default;

View File

@@ -44,8 +44,14 @@ const logEmail = async (req, email) => {
}
};
const sendServerEmail = async ({ subject, text }) => {
const sendServerEmail = async ({ subject, text, to = [] }) => {
if (process.env.NODE_ENV === undefined) return;
let sentTo = ["support@imexsystems.ca"];
if (to?.length) {
sentTo = [...sentTo, ...to];
}
try {
mailer.sendMail(
{
@@ -53,7 +59,7 @@ const sendServerEmail = async ({ subject, text }) => {
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>`
}),
to: ["support@thinkimex.com"],
to: sentTo,
subject: subject,
text: text,
ses: {
@@ -68,7 +74,7 @@ const sendServerEmail = async ({ subject, text }) => {
},
// eslint-disable-next-line no-unused-vars
(err, info) => {
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
logger.log("server-email-send", err ? "error" : "debug", null, null, {
message: err?.message,
stack: err?.stack
});
@@ -103,7 +109,7 @@ const sendMexicoBillingEmail = async ({ subject, text }) => {
},
// eslint-disable-next-line no-unused-vars
(err, info) => {
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
logger.log("server-email-send", err ? "error" : "debug", null, null, {
message: err?.message,
stack: err?.stack
});
@@ -258,7 +264,10 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
// eslint-disable-next-line no-unused-vars
(err, info) => {
// (message, type, user, record, meta
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
logger.log("server-email-send", err ? "error" : "debug", null, null, {
message: err?.message,
stack: err?.stack
});
}
);
} catch (error) {

View File

@@ -1,3 +1,5 @@
const logger = require("../utils/logger");
const GraphQLClient = require("graphql-request").GraphQLClient;
//New bug introduced with Graphql Request.
@@ -11,9 +13,24 @@ const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
}
});
const rpsClient =
process.env.RPS_GRAPHQL_ENDPOINT && process.env.RPS_HASURA_ADMIN_SECRET ?
new GraphQLClient(process.env.RPS_GRAPHQL_ENDPOINT, {
headers: {
"x-hasura-admin-secret": process.env.RPS_HASURA_ADMIN_SECRET
}
}) : null;
if (!rpsClient) {
//System log to disable RPS functions
logger.log(`RPS secrets are not set. Client is not configured.`, "WARN", "redis", "api", {
});
}
const unauthorizedClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);
module.exports = {
client,
rpsClient,
unauthorizedClient
};

View File

@@ -420,6 +420,8 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
v_make_desc
v_color
ca_customer_gst
scheduled_completion
actual_completion
vehicle {
v_trimcode
v_makecode
@@ -920,6 +922,41 @@ exports.CARFAX_QUERY = `query CARFAX_EXPORT($start: timestamptz, $bodyshopid: uu
}
}`;
exports.CARFAX_RPS_QUERY = `query CARFAX_RPS_EXPORT($starttz: timestamptz, $endtz: timestamptz,$start: date, $end: date, $bodyshopid: uuid!) {
bodyshops_by_pk(id: $bodyshopid) {
id
shopname
}
jobs(where: {_and: [{_or: [{close_date: {_gt: $start, _lte: $end}}, {created_at: {_gt: $starttz, _lte: $endtz}, close_date: {_is_null: true}}]}, {_not: {_and: [{close_date: {_is_null: true}}, {created_at: {_is_null: true}}]}}, {bodyshopid: {_eq: $bodyshopid}}, {v_vin: {_is_null: false}}]}) {
close_date
created_at
id
ins_co_nm
impact_1
impact_2
joblines {
act_price
alt_partno
line_desc
mod_lb_hrs
mod_lbr_ty
oem_partno
lbr_op
part_type
part_qty
}
loss_date
loss_desc
theft_ind
tlos_ind
totals
v_makedesc
v_model
v_model_yr
v_vin
}
}`;
exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
bodyshops_by_pk(id: $bodyshopid){
id
@@ -1870,6 +1907,13 @@ exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
}
}`;
exports.GET_CARFAX_RPS_SHOPS = `query GET_CARFAX_RPS_SHOPS {
bodyshops(where: {carfax_exclude: {_neq: "true"}}){
id
shopname
}
}`;
exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS {
bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){
id
@@ -2164,18 +2208,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
exports.INSERT_NEW_TRANSITION = (
includeOldTransition
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
insert_transitions_one(object: $newTransition) {
id
}
${
includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
${includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}`
: ""
: ""
}
}`;

View File

@@ -82,22 +82,35 @@ exports.partsScan = async function (req, res) {
criticalIds.add(jobline.id);
}
if (update_field && update_value) {
const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
lineId: jobline.id,
line: { [update_field]: update_value, manual_line: true }
});
const auditResult = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_AUDIT_TRAIL, {
auditObj: {
bodyshopid: data.jobs_by_pk.bodyshop.id,
jobid,
operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
type: "partscanupdate",
useremail: req.user.email
let actualUpdateField = update_field;
if (update_field === "part_number") {
// Determine which part number field to update based on the match
if (!jobline.oem_partno) {
actualUpdateField = "oem_partno";
} else {
if (regex) {
actualUpdateField = regex.test(jobline.oem_partno || "") ? "oem_partno" : "alt_partno";
} else {
actualUpdateField = jobline.oem_partno === value ? "oem_partno" : "alt_partno";
}
}
}
if (actualUpdateField) {
await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
lineId: jobline.id,
line: { [actualUpdateField]: update_value, manual_line: true }
});
}
await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_AUDIT_TRAIL, {
auditObj: {
bodyshopid: data.jobs_by_pk.bodyshop.id,
jobid,
operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
type: "partscanupdate",
useremail: req.user.email
}
});
}
//break; // No need to evaluate further rules for this jobline

View File

@@ -1,6 +1,6 @@
const express = require("express");
const router = express.Router();
const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax } = require("../data/data");
const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax, carfaxRps } = require("../data/data");
router.post("/ah", autohouse);
router.post("/cc", claimscorp);
@@ -9,5 +9,6 @@ router.post("/kaizen", kaizen);
router.post("/usagereport", usageReport);
router.post("/podium", podium);
router.post("/carfax", carfax);
router.post("/carfaxrps", carfaxRps);
module.exports = router;