From b29600158a6725bc4599df56b2b92054f4f46163 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 16 Dec 2025 15:05:14 -0800 Subject: [PATCH] Basic scrubbing is functional. --- src/main/decoder/decode-env.ts | 2 +- src/main/decoder/decode-lin.ts | 5 +- src/main/decoder/decode-pfh.interface.ts | 1 + src/main/decoder/decode-pfh.ts | 2 +- src/main/decoder/decode-pfm.ts | 2 +- src/main/decoder/decoder.ts | 2 +- .../es-job-object.interface.ts | 246 +++++++++++++++++ src/main/estimate-scrubber/es-transformer.ts | 249 ++++++++++++++++++ .../estimate-scrubber/estimate-scrubber.ts | 120 +++------ 9 files changed, 546 insertions(+), 83 deletions(-) create mode 100644 src/main/estimate-scrubber/es-job-object.interface.ts create mode 100644 src/main/estimate-scrubber/es-transformer.ts diff --git a/src/main/decoder/decode-env.ts b/src/main/decoder/decode-env.ts index de26726..758c361 100644 --- a/src/main/decoder/decode-env.ts +++ b/src/main/decoder/decode-env.ts @@ -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", ]), ); diff --git a/src/main/decoder/decode-lin.ts b/src/main/decoder/decode-lin.ts index a5b2c1b..cd0443c 100644 --- a/src/main/decoder/decode-lin.ts +++ b/src/main/decoder/decode-lin.ts @@ -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; }); diff --git a/src/main/decoder/decode-pfh.interface.ts b/src/main/decoder/decode-pfh.interface.ts index 043c08f..9260aaa 100644 --- a/src/main/decoder/decode-pfh.interface.ts +++ b/src/main/decoder/decode-pfh.interface.ts @@ -1,4 +1,5 @@ export interface DecodedPfh { + id_pro_nam: string; tax_prethr: number; tax_thr_amt?: number; tax_pstthr?: number; diff --git a/src/main/decoder/decode-pfh.ts b/src/main/decoder/decode-pfh.ts index 0a9c724..6b7b356 100644 --- a/src/main/decoder/decode-pfh.ts +++ b/src/main/decoder/decode-pfh.ts @@ -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", diff --git a/src/main/decoder/decode-pfm.ts b/src/main/decoder/decode-pfm.ts index cc840db..8d65b64 100644 --- a/src/main/decoder/decode-pfm.ts +++ b/src/main/decoder/decode-pfm.ts @@ -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. }; }; diff --git a/src/main/decoder/decoder.ts b/src/main/decoder/decoder.ts index c9943ee..6c0c19d 100644 --- a/src/main/decoder/decoder.ts +++ b/src/main/decoder/decoder.ts @@ -128,7 +128,7 @@ async function ImportJob(filepath: string): Promise { 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, }; diff --git a/src/main/estimate-scrubber/es-job-object.interface.ts b/src/main/estimate-scrubber/es-job-object.interface.ts new file mode 100644 index 0000000..734f23d --- /dev/null +++ b/src/main/estimate-scrubber/es-job-object.interface.ts @@ -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 } + >; +} diff --git a/src/main/estimate-scrubber/es-transformer.ts b/src/main/estimate-scrubber/es-transformer.ts new file mode 100644 index 0000000..3048b76 --- /dev/null +++ b/src/main/estimate-scrubber/es-transformer.ts @@ -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; diff --git a/src/main/estimate-scrubber/estimate-scrubber.ts b/src/main/estimate-scrubber/estimate-scrubber.ts index 329d589..fbf586d 100644 --- a/src/main/estimate-scrubber/estimate-scrubber.ts +++ b/src/main/estimate-scrubber/estimate-scrubber.ts @@ -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 { @@ -34,13 +37,21 @@ async function writeJobToLogsFolder(job, fileName): Promise { throw error; } } -async function ScrubEstimate({ job }): Promise { +async function ScrubEstimate({ + job, +}: { + job: RawJobDataObject; +}): Promise { //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 { } 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 { // 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);