Basic scrubbing is functional.

This commit is contained in:
Patrick Fic
2025-12-16 15:05:14 -08:00
parent d75f8a3155
commit b29600158a
9 changed files with 546 additions and 83 deletions

View File

@@ -54,7 +54,7 @@ const DecodeEnv = async (
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
//"EST_SYSTEM",
"EST_SYSTEM",
"ESTFILE_ID",
]),
);

View File

@@ -7,6 +7,7 @@ import store from "../store/store";
import { DecodedLin, DecodedLinLine } from "./decode-lin.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
import OpCodeData from "../util/opCodes";
const DecodeLin = async (
extensionlessFilePath: string,
@@ -107,7 +108,9 @@ const DecodeLin = async (
]),
);
//Apply line by line adjustments.
singleLineData.op_code_desc = opCodeData[singleLineData.lbr_op]?.desc;
singleLineData.op_code_desc = singleLineData.lbr_op
? OpCodeData[singleLineData.lbr_op]?.desc
: undefined;
return singleLineData;
});

View File

@@ -1,4 +1,5 @@
export interface DecodedPfh {
id_pro_nam: string;
tax_prethr: number;
tax_thr_amt?: number;
tax_pstthr?: number;

View File

@@ -54,7 +54,7 @@ const DecodePfh = async (
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
//"ID_PRO_NAM", //Remove
"ID_PRO_NAM", //Remove
"TAX_PRETHR",
"TAX_THRAMT",
"TAX_PSTTHR",

View File

@@ -162,7 +162,7 @@ const DecodePfm = async (
MASH: mashLine,
MAPA: mapaLine, //TODO: Need to verify if more fields are to come in here.
},
//cieca_pfm: rawPfmData, //TODO: Not currently captured. This may have valu in the future.
cieca_pfm: rawPfmData, //TODO: Not currently captured. This may have valu in the future.
};
};

View File

@@ -128,7 +128,7 @@ async function ImportJob(filepath: string): Promise<void> {
vehicle_info: `${jobObject.v_model_yr} ${jobObject.v_make_desc} ${jobObject.v_model_desc}`,
clm_no: jobObject.clm_no,
clm_amt: jobObject.clm_total,
// source_system: jobObject.source_system, //TODO: Add back source system if needed.
source_system: jobObject.source_system, //TODO: Add back source system if needed.
issupplement: false,
jobid: null,
};

View File

@@ -0,0 +1,246 @@
import { DecodedLinLine } from "../decoder/decode-lin.interface";
import { RawJobDataObject } from "../decoder/decoder";
//This should be a copy of RawJobObject with fields removed.
export interface ESJobObject extends Omit<
RawJobDataObject,
// Agent fields
| "cat_no"
| "ciecaid"
| "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"
// Adjustment fields
| "adj_g_disc"
| "adj_strdis"
| "adj_towdis"
// Assignment fields
| "asgn_date"
| "asgn_no"
| "asgn_type"
// Claim fields
| "clm_addr1"
| "clm_addr2"
| "clm_city"
| "clm_ct_fn"
| "clm_ct_ln"
| "clm_ct_ph"
| "clm_ct_phx"
| "clm_ctry"
| "clm_ea"
| "clm_fax"
| "clm_faxx"
| "clm_ofc_id"
| "clm_ofc_nm"
| "clm_ph1"
| "clm_ph1x"
| "clm_ph2"
| "clm_ph2x"
| "clm_st"
| "clm_title"
| "clm_zip"
| "clm_total"
// Misc fields
| "cust_pr"
| "date_estimated"
| "ded_status"
| "depreciation_taxes"
// Estimator fields
| "est_addr1"
| "est_addr2"
| "est_city"
| "est_co_nm"
| "est_ct_fn"
| "est_ct_ln"
| "est_ctry"
| "est_ea"
| "est_ph1"
| "est_st"
| "est_zip"
| "federal_tax_rate"
// Insurance fields
| "ins_addr1"
| "ins_addr2"
| "ins_city"
| "ins_co_id"
| "ins_ct_fn"
| "ins_ct_ln"
| "ins_ct_ph"
| "ins_ct_phx"
| "ins_ctry"
| "ins_ea"
| "ins_fax"
| "ins_faxx"
| "ins_ph1"
| "ins_ph1x"
| "ins_ph2"
| "ins_ph2x"
| "ins_st"
| "ins_title"
| "ins_zip"
| "insd_fax"
| "insd_faxx"
// Loss fields
| "kmin"
| "loss_cat"
| "loss_type"
// Owner fields
| "ownr_addr2"
| "ownr_co_nm"
| "ownr_ctry"
| "ownr_ea"
| "ownr_ph2"
| "ownr_st"
| "ownr_title"
| "ownr_zip"
// Payment fields
| "pay_amt"
| "pay_chknm"
| "pay_date"
| "pay_type"
| "payee_nms"
// Vehicle fields
| "plate_no"
| "plate_st"
| "policy_no"
// Rate fields
| "rate_la1"
| "rate_la2"
| "rate_la3"
| "rate_la4"
| "rate_laa"
| "rate_lab"
| "rate_lad"
| "rate_lae"
| "rate_laf"
| "rate_lag"
| "rate_lam"
| "rate_lar"
| "rate_las"
| "rate_lau"
| "rate_ma2s"
| "rate_ma2t"
| "rate_ma3s"
| "rate_mabl"
| "rate_macs"
| "rate_mahw"
| "rate_mapa"
| "rate_mash"
// Tax fields
| "tax_lbr_rt"
| "tax_levies_rt"
| "tax_paint_mat_rt"
| "tax_predis"
| "tax_prethr"
| "tax_pstthr"
| "tax_shop_mat_rt"
| "tax_str_rt"
| "tax_sub_rt"
| "tax_thramt"
| "tax_tow_rt"
// Additional vehicle fields
| "theft_ind"
| "v_color"
| "tlos_ind"
| "v_make_desc"
| "v_model_desc"
| "shopid"
| "est_system"
// Object fields
| "owner"
| "vehicle"
| "bodyshop"
| "area_of_damage"
| "joblines"
| "bodyshop"
// CIECA fields
| "cieca_pft"
| "cieca_pfl"
| "cieca_pfm"
| "cieca_pfo"
| "cieca_stl"
| "cieca_ttl"
| "parts_tax_rates"
| "materials"
> {
// Fields added by the transformer
impact_1?: string;
impact_2?: string;
close_date: string | null;
created_at: string;
id: string;
group: string;
group_verified: boolean;
updated_at: string;
v_age: number;
v_type: string;
v_makedesc?: string;
v_model?: string;
supp_amt: number;
ro_number: string | null;
requires_reimport: boolean;
v_mileage: string;
id_pro_nam?: string;
g_ttl_amt: number;
source_system: string;
rf_ph1: string;
rf_zip: string;
association_switch: string;
sending_entity_id: string;
sending_entity_accept_terms_of_use: boolean;
// Transformed arrays
joblines: Omit<
DecodedLinLine,
| "lbr_tax"
| "lbr_typ_j"
| "line_ref"
| "misc_sublt"
| "misc_tax"
| "prt_dsmk_m"
| "prt_dsmk_p"
| "tran_code"
| "unq_seq"
| "alt_co_id"
| "alt_overrd"
| "alt_part_i"
| "alt_partm"
| "bett_type"
| "bett_pctg"
| "bett_amt"
| "bett_tax"
| "op_code_desc"
| "paint_stg"
| "paint_tone"
>[];
totals: Array<{
nt_hrs?: number;
t_amt?: number;
t_hrs?: number;
ttl_typecd?: string;
}>;
rates: Array<
| { cal_prethr?: number; mat_type?: string }
| { lbr_desc?: string; lbr_rate?: number; lbr_type?: string }
>;
}

View File

@@ -0,0 +1,249 @@
import { RawJobDataObject } from "../decoder/decoder";
import { ESJobObject } from "./es-job-object.interface";
import _ from "lodash";
const sendingEntityId = "87330f61-412b-4251-baaa-d026565b23c5";
function TransformJobForEstimateScrubber(job: RawJobDataObject): ESJobObject {
//Take the job object and strip off everything we don't need.
const omittedJob = _.omit(job, [
"cat_no",
"ciecaid",
"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",
"adj_g_disc",
"adj_strdis",
"adj_towdis",
"asgn_date",
"asgn_no",
"asgn_type",
"clm_addr1",
"clm_addr2",
"clm_city",
"clm_ct_fn",
"clm_ct_ln",
"clm_ct_ph",
"clm_ct_phx",
"clm_ctry",
"clm_ea",
"clm_fax",
"clm_faxx",
"clm_ofc_id",
"clm_ofc_nm",
"clm_ph1",
"clm_ph1x",
"clm_ph2",
"clm_ph2x",
"clm_st",
"clm_title",
"clm_zip",
"cust_pr",
"date_estimated",
"ded_status",
"depreciation_taxes",
"est_addr1",
"est_addr2",
"est_city",
"est_co_nm",
"est_ct_fn",
"est_ct_ln",
"est_ctry",
"est_ea",
"est_ph1",
"est_st",
"est_zip",
"federal_tax_rate",
"ins_addr1",
"ins_addr2",
"ins_city",
"ins_co_id",
"ins_ct_fn",
"ins_ct_ln",
"ins_ct_ph",
"ins_ct_phx",
"ins_ctry",
"ins_ea",
"ins_fax",
"ins_faxx",
"ins_ph1",
"ins_ph1x",
"ins_ph2",
"ins_ph2x",
"ins_st",
"ins_title",
"ins_zip",
"insd_fax",
"insd_faxx",
"kmin",
"loss_cat",
"loss_type",
"ownr_addr2",
"ownr_co_nm",
"ownr_ctry",
"ownr_ea",
"ownr_ph2",
"ownr_st",
"ownr_title",
"ownr_zip",
"pay_amt",
"pay_chknm",
"pay_date",
"pay_type",
"payee_nms",
"plate_no",
"plate_st",
"policy_no",
"rate_la1",
"rate_la2",
"rate_la3",
"rate_la4",
"rate_laa",
"rate_lab",
"rate_lad",
"rate_lae",
"rate_laf",
"rate_lag",
"rate_lam",
"rate_lar",
"rate_las",
"rate_lau",
"rate_ma2s",
"rate_ma2t",
"rate_ma3s",
"rate_mabl",
"rate_macs",
"rate_mahw",
"rate_mapa",
"rate_mash",
"tax_lbr_rt",
"tax_levies_rt",
"tax_paint_mat_rt",
"tax_predis",
"tax_prethr",
"tax_pstthr",
"tax_shop_mat_rt",
"tax_str_rt",
"tax_sub_rt",
"tax_thramt",
"tax_tow_rt",
"theft_ind",
"v_color",
"tlos_ind",
"v_make_desc",
"v_model_desc",
"shopid",
"est_system",
//Object fields
"owner",
"vehicle",
"bodyshop",
"area_of_damage",
"joblines",
"clm_total",
"bodyshop",
//Cieca Fields
"cieca_pft",
"cieca_pfl",
"cieca_pfm",
"cieca_pfo",
"cieca_stl",
"cieca_ttl",
"parts_tax_rates",
"materials",
]);
//Apply the ES specific transformations needed.
const rates = [
...(job.cieca_pfm?.map((pfm) => ({
cal_prethr: pfm.cal_prethr,
mat_type: pfm.matl_type, //Rename required - presumed typo in ES API.
})) || []),
...(Object.keys(job.cieca_pfl).map((key) =>
_.pick(job.cieca_pfl[key], ["lbr_desc", "lbr_rate", "lbr_type"]),
) || []),
];
const supp_amt = job.cieca_ttl?.data?.supp_amt || 0;
const totals =
job.cieca_stl?.data.map((ttl) =>
_.pick(ttl, ["nt_hrs", "t_amt", "t_hrs", "ttl_typecd"]),
) || [];
//Add the ES Objects.
return {
//TODO: Remove hard coded values
...omittedJob,
impact_1: job.area_of_damage?.impact1,
impact_2: job.area_of_damage?.impact2,
close_date: null,
created_at: "2025-12-11T18:09:14.085604+00:00",
id: "c478593b-58a2-4bab-b7c7-2df19a257b2b",
group: "Group 10",
group_verified: false,
updated_at: "2025-12-11T18:09:14.085604+00:00",
v_age: 20, //Needed? RPS calc.
v_type: "PC", // need to get from API.
v_makedesc: job.v_make_desc,
v_model: job.v_model_desc,
rates,
supp_amt,
totals,
ro_number: null,
requires_reimport: false,
v_mileage: job.kmin?.toString() || "",
sending_entity_id: sendingEntityId,
sending_entity_accept_terms_of_use: true,
association_switch: "ATAM", //TODO: This needs to be updated ,
rf_ph1: "2043792253", //TODO - is this needed anymore?
rf_zip: "R0G 1Z0", //TODO - is this needed anymore?
g_ttl_amt: job.clm_total,
source_system: job.est_system || "M", //TODO - pull this.
joblines: job.joblines?.data?.map((line) => ({
..._.omit(line, [
"lbr_tax",
"lbr_typ_j",
"line_ref",
"misc_sublt",
"misc_tax",
"prt_dsmk_m",
"prt_dsmk_p",
"tran_code",
"unq_seq",
"alt_co_id",
"alt_overrd",
"alt_part_i",
"alt_partm",
"bett_type",
"bett_pctg",
"bett_amt",
"bett_tax",
"op_code_desc",
"paint_stg",
"paint_tone",
]),
})),
// Remove or modify sensitive fields here}
};
}
export default TransformJobForEstimateScrubber;

View File

@@ -1,9 +1,12 @@
import log from "electron-log";
import axios from "axios";
import path from "path";
import { BrowserWindow } from "electron";
import { promises as fsPromises } from "fs";
import log from "electron-log";
import { autoUpdater } from "electron-updater";
import { promises as fsPromises } from "fs";
import path from "path";
import { RawJobDataObject } from "../decoder/decoder";
import store from "../store/store";
import TransformJobForEstimateScrubber from "./es-transformer";
// Function to write job object to logs subfolder
async function writeJobToLogsFolder(job, fileName): Promise<string> {
@@ -34,13 +37,21 @@ async function writeJobToLogsFolder(job, fileName): Promise<string> {
throw error;
}
}
async function ScrubEstimate({ job }): Promise<string | undefined> {
async function ScrubEstimate({
job,
}: {
job: RawJobDataObject;
}): Promise<string | undefined> {
//These are hard coded as they are not secure values and checking happens based on other values.
//No secret or private information is exposed.
//Scrub Estimate Transformer. Original schema kept to keep data in line with ImEX standards.
const transformedJob = TransformJobForEstimateScrubber(job);
const basicAuthUser = "Imex2";
const basicAuthpassword = "Patrick";
const currentChannel = autoUpdater.channel;
let estimateScrubberUrl;
let estimateScrubberUrl: string;
switch (currentChannel) {
case "alpha":
estimateScrubberUrl = "https://4284-79287.el-alt.com"; //dev specific URL.
@@ -54,92 +65,46 @@ async function ScrubEstimate({ job }): Promise<string | undefined> {
}
log.log(`Estimate Scrubber URL: [${currentChannel} |`, estimateScrubberUrl);
const sendingEntityId = "87330f61-412b-4251-baaa-d026565b23c5";
try {
const esApiKey = job?.bodyshop?.es_api_key;
const esApiKey = store.get("settings.esApiKey") as string;
//Perform data manipulation on the job object
if (!job) {
if (!transformedJob) {
console.error("No job provided to ScrubEstimate");
return;
}
//Set shop metrics
job.sending_entity_id = sendingEntityId;
job.sending_entity_accept_terms_of_use = true;
job.association_switch = "ATAM";
job.rf_zip = "R0G 1Z0" || job.bodyshop?.zip_post; //TODO: REPLACE WITH BODYSHOP ZIP IF AVAILABLE
job.rf_ph1 = "2043792253" || job.bodyshop?.phone; //TODO: REPLACE WITH BODYSHOP PHONE IF AVAILABLE
job.g_ttl_amt = job.clm_total;
job.source_system = "M"; //Requested by Steven.
job.v_mileage = job.v_mileage?.toString() || ""; //Requested by Steven to be a string.
delete job.clm_total;
delete job.bodyshop; //Bodyshop has to be passed through the object as we don't have access to the store here.
//Adjust the rates field to be MAT_TYPE instead of MATL_TYPE
if (job.rates && Array.isArray(job.rates)) {
job.rates.forEach((rate) => {
if (rate.MATL_TYPE) {
rate.MAT_TYPE = rate.MATL_TYPE;
delete rate.MATL_TYPE;
}
});
}
//Lower case the rates & totals
if (job.rates && Array.isArray(job.rates)) {
job.rates = job.rates.map((rate) => {
const lowercasedRate = {};
for (const [key, value] of Object.entries(rate)) {
lowercasedRate[key.toLowerCase()] = value;
}
return lowercasedRate;
});
}
if (job.totals && Array.isArray(job.totals)) {
job.totals = job.totals.map((total) => {
const lowercasedTotal = {};
for (const [key, value] of Object.entries(total)) {
lowercasedTotal[key.toLowerCase()] = value;
}
return lowercasedTotal;
});
}
const fileName = `${esApiKey}-${job.clm_no}-${Date.now()}`;
const fileName = `${esApiKey}-${transformedJob.clm_no}-${Date.now()}`;
// Write job object to logs subfolder
try {
await writeJobToLogsFolder(job, fileName);
await writeJobToLogsFolder(transformedJob, fileName);
} catch (error) {
log.error("Failed to write job to logs folder:", error);
// Continue with the rest of the function even if this fails
}
const formData = new FormData();
const jsonString = JSON.stringify(job);
const jsonString = JSON.stringify(transformedJob);
formData.append(
"file",
new Blob([jsonString], { type: "application/json" }),
`${fileName}.json`,
);
// const result = await axios.post(
// `${estimateScrubberUrl}/api/sendems`,
// formData,
// {
// auth: {
// username: basicAuthUser,
// password: basicAuthpassword,
// },
// headers: {
// ...(formData.getHeaders ? formData.getHeaders() : {}),
// APIkey: esApiKey,
// },
// },
// );
const result = null;
const result = await axios.post(
`${estimateScrubberUrl}/api/sendems`,
formData,
{
auth: {
username: basicAuthUser,
password: basicAuthpassword,
},
headers: {
APIkey: esApiKey,
},
},
);
const resultPDFUrl = result?.data?.report_link;
const reportIssueUrl = `https://insurtechtoolkit.com/pcontactUs.aspx?apiKey=${esApiKey}&file=${fileName}.json`;
@@ -152,15 +117,14 @@ async function ScrubEstimate({ job }): Promise<string | undefined> {
// reportIssueUrl,
// });
// const pdfWindow = new BrowserWindow({
const pdfWindow = new BrowserWindow({
webPreferences: {
plugins: true, // Enable PDF viewing
},
});
// webPreferences: {
// plugins: true, // Enable PDF viewing
// },
// });
// pdfWindow.loadURL(resultPDFUrl);
// pdfWindow.focus();
pdfWindow.loadURL(resultPDFUrl);
pdfWindow.focus();
return resultPDFUrl;
} catch (error) {
log.error("Error while scrubbing estimate:", error, error.stack);