Files
imexrps/electron/decoder/decoder.js
2026-03-25 10:30:31 -07:00

681 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { DBFFile } = require("dbffile");
const path = require("path");
const _ = require("lodash");
const log = require("electron-log");
const { store } = require("../electron-store");
const { BrowserWindow } = require("electron");
const ipcTypes = require("../../src/ipc.types.commonjs");
const { NewNotification } = require("../notification-wrapper/notification-wrapper");
const { WhichRulesetToApply } = require("./constants");
const { claimsClerk } = require("../claims-clerk/claims-clerk");
//const Nucleus = require("nucleus-nodejs");
async function ImportJob(filepath) {
const parsedFilePath = path.parse(filepath);
let extensionlessFilePath = path.join(parsedFilePath.dir, parsedFilePath.name);
const decodedJob = await DecodeAd1File(extensionlessFilePath);
const b = BrowserWindow.getAllWindows()[0];
b.webContents.send(ipcTypes.default.estimate.toRenderer.getCloseDate, {
filepath,
clm_no: decodedJob.CLM_NO
});
}
async function ImportJobWithCloseDate(filepath, close_date) {
const newJob = await DecodeEstimate(filepath, false, close_date);
const b = BrowserWindow.getAllWindows()[0];
if (newJob && !newJob.ERROR) {
b.webContents.send(ipcTypes.default.estimate.toRenderer.estimateDecodeSuccess, newJob);
log.info(`Sent job for upload. ${newJob.clm_no}`);
NewNotification({
title: "Job Uploaded",
body: "A new job has been uploaded."
});
} else {
log.info(`Ignored job. ${newJob.ERROR}`);
// Nucleus.track("IGNORE_JOB", { reason: newJob.ERROR });
NewNotification({
title: "Job Ignored",
body: newJob.ERROR
});
}
}
exports.ImportJobWithCloseDate = ImportJobWithCloseDate;
async function DecodeEstimate(filePath, includeFilePathInReturnJob = false, close_date = null) {
const parsedFilePath = path.parse(filePath);
let extensionlessFilePath = path.join(parsedFilePath.dir, parsedFilePath.name);
const job = {
...(await DecodeAd1File(extensionlessFilePath)),
...(await DecodeVehFile(extensionlessFilePath)),
...(await DecodeTtlFile(extensionlessFilePath)),
...(await DecodeLinFile(extensionlessFilePath, close_date)),
...(await DecodePflFile(extensionlessFilePath)),
...(await DecodePfmFile(extensionlessFilePath)),
...(await DecodePfhFile(extensionlessFilePath)),
...(await DecodeStlFile(extensionlessFilePath)),
...(includeFilePathInReturnJob ? { filePath } : {})
};
const ad2 = await DecodeAd2File(extensionlessFilePath);
job.rates = [...(job.rates || []), ...(job.mat_rates || [])];
delete job.mat_rates;
job.insp_date = ad2.INSP_DATE;
if (job.OWNR_FN === "" || !job.OWNR_FN) job.OWNR_FN = ad2.CLMT_FN;
if (job.OWNR_LN === "" || !job.OWNR_LN) job.OWNR_LN = ad2.CLMT_LN;
if (job.OWNR_CO_NM) job.OWNR_LN = `${job.OWNR_LN} ${job.OWNR_CO_NM}`;
delete job.OWNR_CO_NM;
const accepted_ins_co = store.get("accepted_ins_co");
let returnValue;
//Removed as a part of RPS-40.
// if (job.V_MILEAGE <= 20000) {
// returnValue = { ERROR: "Vehicle mileage is less than 20,000kms." };
// } else
const ins_rule_set = store.get("ins_rule_set");
if (!accepted_ins_co.includes(job.INS_CO_NM)) {
returnValue = {
ERROR: `Insurance Company Name is not valid for RPS. (${job.INS_CO_NM || "No name set"})`
};
} else if (!job.CLM_NO) {
log.info("Job ignored. No claim #. ");
returnValue = {
ERROR: `An unique claim number must be set for all jobs sent to RPS.`
};
} else if (ins_rule_set === "MPI" && !(job.CLM_NO.match("-01$") || job.CLM_NO.match("-99$"))) {
// 2024-05-10 Only -01 and -99 apply. Refactor logic.
log.info("Job ignored. This is not a -01 or -99 claim. Claim #. " + job.CLM_NO);
returnValue = {
ERROR: "Job ignored. This is not a -01 or -99 claim. Claim #. " + job.CLM_NO
};
}
// else if (job.LOSS_TYPE.toLowerCase() === "h") {
// log.info("Job ignored. This is a hail damage claim. " + job.CLM_NO);
// returnValue = {
// ERROR: "Job ignored. This is a hail damage claim. " + job.CLM_NO,
// };
// }
else {
delete job.LOSS_TYPE;
returnValue = _.transform(job, function (result, val, key) {
result[key.toLowerCase()] = val;
});
}
return returnValue;
}
async function DecodeAd1File(extensionlessFilePath) {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}A.AD1`);
} catch (error) {
log.error("Error opening AD1 File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.AD1`);
dbf && log.log("Found AD1 file using regular CIECA Id.");
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], [
// "INS_CO_ID",
"INS_CO_NM",
// "INS_ADDR1",
// "INS_ADDR2",
// "INS_CITY",
// "INS_ST",
// "INS_ZIP",
// "INS_CTRY",
// "INS_EA",
// "POLICY_NO",
"DED_AMT",
// "DED_STATUS",
// "ASGN_NO",
//"ASGN_DATE",
// "ASGN_TYPE",
"CLM_NO",
// "CLM_OFC_ID",
// "CLM_OFC_NM",
// "CLM_ADDR1",
// "CLM_ADDR2",
// "CLM_CITY",
// "CLM_ST",
// "CLM_ZIP",
// "CLM_CTRY",
// "CLM_PH1",
// "CLM_PH1X",
// "CLM_PH2",
// "CLM_PH2X",
// "CLM_FAX",
// "CLM_FAXX",
// "CLM_CT_LN",
// "CLM_CT_FN",
// "CLM_TITLE",
// "CLM_CT_PH",
// "CLM_CT_PHX",
// "CLM_EA",
// "PAYEE_NMS",
// "PAY_TYPE",
// "PAY_DATE",
// "PAY_CHKNM",
// "PAY_AMT",
// "AGT_CO_ID",
// "AGT_CO_NM",
// "AGT_ADDR1",
// "AGT_ADDR2",
// "AGT_CITY",
// "AGT_ST",
// "AGT_ZIP",
// "AGT_CTRY",
// "AGT_PH1",
// "AGT_PH1X",
// "AGT_PH2",
// "AGT_PH2X",
// "AGT_FAX",
// "AGT_FAXX",
// "AGT_CT_LN",
// "AGT_CT_FN",
// "AGT_CT_PH",
// "AGT_CT_PHX",
// "AGT_EA",
// "AGT_LIC_NO",
"LOSS_DATE",
"LOSS_TYPE",
"LOSS_DESC",
"THEFT_IND",
// "CAT_NO",
"TLOS_IND",
// "CUST_PR",
// "INSD_LN",
// "INSD_FN",
// "INSD_TITLE",
// "INSD_CO_NM",
// "INSD_ADDR1",
// "INSD_ADDR2",
// "INSD_CITY",
// "INSD_ST",
// "INSD_ZIP",
// "INSD_CTRY",
// "INSD_PH1",
// "INSD_PH1X",
// "INSD_PH2",
// "INSD_PH2X",
// "INSD_FAX",
// "INSD_FAXX",
// "INSD_EA",
"OWNR_LN",
"OWNR_FN",
// "OWNR_TITLE",
"OWNR_CO_NM",
"OWNR_ADDR1",
// "OWNR_ADDR2",
"OWNR_CITY",
// "OWNR_ST",
// "OWNR_ZIP",
// "OWNR_CTRY",
"OWNR_PH1"
// "OWNR_PH1X",
// "OWNR_PH2",
// "OWNR_PH2X",
// "OWNR_FAX",
// "OWNR_FAXX",
// "OWNR_EA",
// "INS_PH1",
// "INS_PH1X",
// "INS_PH2",
// "INS_PH2X",
// "INS_FAX",
// "INS_FAXX",
// "INS_CT_LN",
// "INS_CT_FN",
// "INS_TITLE",
// "INS_CT_PH",
// "INS_CT_PHX",
// "LOSS_CAT",
]);
}
}
async function DecodeAd2File(extensionlessFilePath) {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}B.AD2`);
} catch (error) {
log.error("Error opening AD2 File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.AD2`);
dbf && log.log("Found AD2 file using regular CIECA Id.");
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], ["CLMT_LN", "CLMT_FN", "INSP_DATE"]);
}
}
async function DecodePfhFile(extensionlessFilePath) {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
} catch (error) {
log.error("Error opening PFH File.", error);
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], ["ID_PRO_NAM"]);
}
}
async function DecodeVehFile(extensionlessFilePath) {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
} catch (error) {
log.error("Error opening VEH File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.VEH`);
dbf && log.log("Found VEH file using regular CIECA Id.");
} finally {
if (!dbf) return {};
let records = await dbf.readRecords(1);
return _.pick(records[0], [
"IMPACT_1",
"IMPACT_2",
// "DB_V_CODE",
// "PLATE_NO",
// "PLATE_ST",
"V_VIN",
// "V_COND",
// "V_PROD_DT",
"V_MODEL_YR",
// "V_MAKECODE",
"V_MAKEDESC",
"V_MODEL",
"V_TYPE",
"V_MILEAGE",
// "V_BSTYLE",
// "V_TRIMCODE",
// "TRIM_COLOR",
// "V_MLDGCODE",
// "V_ENGINE",
// "V_COLOR",
// "V_TONE",
"V_STAGE"
// "PAINT_CD1",
// "PAINT_CD2",
// "PAINT_CD3",
]);
}
}
async function DecodeTtlFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.TTL`);
let records = await dbf.readRecords(1);
return { clm_total: records[0]["G_TTL_AMT"], supp_amt: records[0]["SUPP_AMT"], g_bett_amt: records[0]["G_BETT_AMT"] };
}
async function DecodeLinFile(extensionlessFilePath, close_date) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.LIN`);
let records = await dbf.readRecords();
let joblines = records.map((record) => {
return _.transform(
_.pick(record, [
"LINE_NO",
"LINE_IND",
"LINE_REF",
// "TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
// "PART_DESCJ",
"PRT_DSMK_M",
"OEM_PARTNO",
"PRICE_INC",
// "ALT_PART_I",
"TAX_PART",
"DB_PRICE",
"ACT_PRICE",
"PART_QTY",
"PRICE_J",
"GLASS_FLAG",
"CERT_PART",
// "ALT_CO_ID",
"ALT_PARTNO",
// "ALT_OVERRD",
// "ALT_PARTM",
"PRT_DSMK_P",
"MOD_LBR_TY",
"DB_HRS",
"MOD_LB_HRS",
"LBR_INC",
"LBR_OP",
"LBR_HRS_J",
// "LBR_TYP_J",
"LBR_OP_J",
// "PAINT_STG",
// "PAINT_TONE",
// "LBR_TAX",
"LBR_AMT",
"MISC_AMT"
// "MISC_SUBLT",
// "MISC_TAX",
// "BETT_TYPE",
// "BETT_PCTG",
// "BETT_AMT",
// "BETT_TAX",
]),
function (result, val, key) {
//Required because unq_seq gets pulled as a numeric instaed of a string.
if (key === "UNQ_SEQ") {
result[key.toLowerCase()] = val.toString();
return;
}
result[key.toLowerCase()] = val;
return;
}
);
});
//Apply ruleset.
const ins_rule_set = store.get("ins_rule_set");
joblines.map((jobline) => {
jobline.ignore = false;
switch (ins_rule_set) {
case "MPI":
case "SGI":
const rulesetToApply = WhichRulesetToApply(close_date);
switch (rulesetToApply) {
case "V1":
jobline = V1Ruleset(jobline, joblines);
break;
case "V2":
jobline = V2Ruleset(jobline, joblines);
break;
case "V3":
jobline = V3Ruleset(jobline, joblines);
break;
default:
jobline = V3Ruleset(jobline, joblines);
break;
}
break;
// case "SGI":
// log.info("Using SGI ruleset. Line will be automatically counted until rules are added.");
// break;
default:
log.info("Using default ruleset (MPI).");
break;
}
//2025-05-27 Commenting out to prepare release without claims clerk.
// jobline.alerts = claimsClerk({ jobline, joblines });
return jobline;
});
//Check for discounts that need to be ignored after the fact.
return { joblines: { data: joblines } };
}
async function DecodePflFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.PFL`);
let records = await dbf.readRecords();
let pflLines = records.map((record) => {
return _.pick(record, ["LBR_TYPE", "LBR_DESC", "LBR_RATE"]);
});
//Check for discounts that need to be ignored after the fact.
return { rates: pflLines };
}
async function DecodeStlFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.STL`);
let records = await dbf.readRecords();
let pflLines = records.map((record) => {
return _.pick(record, ["TTL_TYPECD", "T_AMT", "T_HRS", "NT_HRS"]);
});
//Check for discounts that need to be ignored after the fact.
return { totals: pflLines };
}
async function DecodePfmFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.PFM`);
let records = await dbf.readRecords();
let pflLines = records.map((record) => {
return _.pick(record, ["MATL_TYPE", "CAL_PRETHR"]);
});
//Check for discounts that need to be ignored after the fact.
return { mat_rates: pflLines };
}
exports.DecodeEstimate = DecodeEstimate;
exports.ImportJob = ImportJob;
function V1Ruleset(jobline, joblines) {
//These set of MPI rules are valid until April 1, 2023.
//Wheel Repair Pricing PRS-82
//Verified on 08/24/21
if (
jobline.part_type === "PAN" &&
jobline.line_desc.toLowerCase().includes("wheel") &&
Math.abs(jobline.prt_dsmk_m) === Math.round((jobline.act_price / 2) * 100) / 100
) {
log.info(`Jobline '${jobline.line_desc}' ignored due to wheel repair.`);
jobline.ignore = true;
}
//RPS-39 - OEM ON OEM SAVINGS
//Verified on 08/24/21
if (jobline.part_type === "PAN" && jobline.db_price !== jobline.act_price && jobline.db_price !== 0) {
jobline.db_price = jobline.act_price;
}
//Remove all $0DB line items 02/17/2022.
if (
(jobline.part_type === "PAA" || jobline.part_type === "PAL" || jobline.part_type === "PAN") &&
jobline.db_price === 0
) {
jobline.ignore = true;
}
//05/20
//Well have to apply a rule that will not count any A/M, Reman or new part price that is manually changed to $0.00.
//Only recycled parts that are changed to $0.00 can be counted towards RPS.
// This is separate from $0.00 DB prices that need to be updated to the manually entered price.
//Verified on 08/24/21
if (jobline.part_type !== "PAL" && jobline.act_price === 0 && jobline.price_j) {
log.info(`Jobline '${jobline.line_desc}' ignored because it was manually changed to 0..`);
jobline.ignore = true;
}
//09/2021 Detect NAGS lines using RegEx for the Part Number
if (jobline.line_desc.toLowerCase().includes("glass") && jobline.oem_partno.match(`[A-Z]{2}[0-9]{5,6}[A-Z]{3}`)) {
console.log(jobline.line_desc, "NAGS Line ignored");
jobline.ignore = true;
}
//Logic Based Exclusions.
//Verified on 08/24/21
if (
!jobline.part_type ||
jobline.db_ref.startsWith("900") ||
jobline.line_desc.toLowerCase().startsWith("urethane") ||
jobline.line_desc.toLowerCase().startsWith("w/shield adhesive") ||
//jobline.line_desc.toLowerCase().includes("wheel") || Removed as a part of RPS-41
(jobline.line_desc.toLowerCase().includes("tire") &&
!jobline.line_desc.toLowerCase().includes("sensor") &&
!jobline.line_desc.toLowerCase().includes("label")) ||
jobline.line_desc.toLowerCase().startsWith("hazardous") ||
jobline.line_desc.toLowerCase().startsWith("detail") ||
jobline.line_desc.toLowerCase().startsWith("clean") ||
// jobline.part_type.toUpperCase() === "PAG" ||Removed for RPS-43.
jobline.part_type.toUpperCase() === "PAS" ||
jobline.part_type.toUpperCase() === "PASL" ||
jobline.part_type.toUpperCase() === "PAE"
//jobline.glass_flag === true //Removed for RPS-43.
) {
jobline.ignore = true;
}
//Check to see if this is a discount line i.e. a 900511
if (jobline.db_ref === "900511" && jobline.prt_dsmk_p !== 50) {
jobline.ignore = false;
//Calculate the discount to be added as a negative.
jobline.act_price = jobline.prt_dsmk_m;
//Check to see if the parent line has a discount.
const parentLine = joblines.find((r) => parseInt(r.unq_seq) === jobline.line_ref);
if (parentLine && parentLine.ignore) {
jobline.ignore = true;
}
}
//RPS-42 Dynamic Inclusion or Exclusion of Lines based on RPS-EXCLUDE or RPS.
if (jobline.oem_partno.toLowerCase().includes("/rps-exclude")) {
jobline.ignore = true;
} else if (jobline.oem_partno.toLowerCase().includes("/rps")) {
jobline.ignore = false;
}
return jobline;
}
function V2Ruleset(jobline, joblines) {
//This is the rules psot 04/01/2023. They are a further restrictive set, and therefore
//V1 rules are called first, and then run through this filter as well.
V1Ruleset(jobline, joblines);
//Remove any glass related items.
if (jobline.mod_lbr_ty?.toUpperCase() === "LAG") {
jobline.ignore = true;
}
//ADAS Part Line
if (
AdasDescriptions.some((d) => {
const ret = jobline.line_desc.toLowerCase().includes(d);
return ret;
})
) {
jobline.ignore = true;
}
//Additional glass line exclusions
if (v2GlassLines.some((d) => jobline.line_desc.toLowerCase().includes(d))) {
jobline.ignore = true;
}
return jobline;
}
function V3Ruleset(jobline, joblines) {
//This is the rules psot 09/01/2023. They appear to be a copy of V2 rules. They have been duplicated for structural sake.
V2Ruleset(jobline, joblines);
return jobline;
}
const AdasDescriptions = [
"seat belt",
"air bag bolt",
"air bag cable reel",
"air bag center sensor",
"air bag clock",
"air bag coil",
"air bag control",
"air bag diagnostic",
"air bag driver",
"air bag ecs",
"air bag harness",
"air bag impact",
"air bag label",
"air bag module",
"air bag nut",
"air bag rollover",
"air bag rotary coupler",
"air bag safing sensor",
"air bag satellite sensor",
"air bag screw",
"air bag seat",
"air bag warning",
"air bag ocs",
"air bag opds",
"air bag sensor",
"air bag occupant",
"air bag pass",
"air bag system air bag terminal",
"air bag side",
"side air bag",
"air bag weight",
"air bag steering",
"air bag decal",
"air bag switch",
"air bag pressure",
"air bag pig",
"air bag whiplash",
"air bag spiral",
"air bag restraint",
"air bag wire harness",
"drivers knee air bag",
"drivers seat air bag",
"inform label air bag",
"pass air bag wire",
"air bag extension",
"air bag caution",
"connector air bag",
"curtain air bag",
"air bag sdm",
"air bag cover",
"air bag srs",
"air bag light sensor",
"knee air bag",
"air bag contact",
"air bag crash",
"air bag lwr",
"air bag pad",
"air bag suspension",
"air bag spacer",
"back air bag",
"air bag reel",
"air bag discriminating",
"air bag curtain",
"air bag initiator",
"air bag positive",
"engine wiring harness for air bag",
"air bag wiring",
"air bag combination",
"air bag connector",
"air bag plug",
"rear air bag",
"air bag clip",
"air bag gas",
"air bag bracket",
"suspension air bag",
"passenger air bag"
];
const v2GlassLines = [
"frt sunroof assembly",
"frt sunroof glass assembly",
"frt sunroof glass panel",
"frt sunroof panel",
"frt sunroof sliding panel",
"rear sunroof glass assembly",
"rear sunroof glass panel",
"sunroof assembly",
"sunroof glass assembly",
"sunroof glass panel",
"sunroof sliding panel"
];