Initial copy of shop partner app.

This commit is contained in:
Patrick Fic
2025-12-01 05:43:59 -08:00
commit 267ef714a7
193 changed files with 32199 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
import { UUID } from "crypto";
export interface DecodedAd1 {
// Insurance company information
ins_co_id?: string;
ins_co_nm?: string;
ins_addr1?: string;
ins_addr2?: string;
ins_city?: string;
ins_st?: string;
ins_zip?: string;
ins_ctry?: string;
ins_ea?: string;
ins_ph1?: string;
ins_ph1x?: string;
ins_ph2?: string;
ins_ph2x?: string;
ins_fax?: string;
ins_faxx?: string;
ins_ct_ln?: string;
ins_ct_fn?: string;
ins_title?: string;
ins_ct_ph?: string;
ins_ct_phx?: string;
// Policy information
policy_no?: string;
ded_amt?: string;
ded_status?: string;
asgn_no?: string;
asgn_date?: Date | string;
asgn_type?: string;
// Claim information
clm_no?: string;
clm_ofc_id?: string;
clm_ofc_nm?: string;
clm_addr1?: string;
clm_addr2?: string;
clm_city?: string;
clm_st?: string;
clm_zip?: string;
clm_ctry?: string;
clm_ph1?: string;
clm_ph1x?: string;
clm_ph2?: string;
clm_ph2x?: string;
clm_fax?: string;
clm_faxx?: string;
clm_ct_ln?: string;
clm_ct_fn?: string;
clm_title?: string;
clm_ct_ph?: string;
clm_ct_phx?: string;
clm_ea?: string;
// Payment information
payee_nms?: string;
pay_type?: string;
pay_date?: string;
pay_chknm?: string;
pay_amt?: string;
// Agent information
agt_co_id?: string;
agt_co_nm?: string;
agt_addr1?: string;
agt_addr2?: string;
agt_city?: string;
agt_st?: string;
agt_zip?: string;
agt_ctry?: string;
agt_ph1?: string;
agt_ph1x?: string;
agt_ph2?: string;
agt_ph2x?: string;
agt_fax?: string;
agt_faxx?: string;
agt_ct_ln?: string;
agt_ct_fn?: string;
agt_ct_ph?: string;
agt_ct_phx?: string;
agt_ea?: string;
agt_lic_no?: string;
// Loss information
loss_date?: string;
loss_type?: string;
loss_desc?: string;
theft_ind?: string;
cat_no?: string;
tlos_ind?: string;
cust_pr?: string;
loss_cat?: string;
// Insured information
insd_ln?: string;
insd_fn?: string;
insd_title?: string;
insd_co_nm?: string;
insd_addr1?: string;
insd_addr2?: string;
insd_city?: string;
insd_st?: string;
insd_zip?: string;
insd_ctry?: string;
insd_ph1?: string;
insd_ph2?: string;
insd_fax?: string;
insd_faxx?: string;
insd_ea?: string;
// Owner information
ownr_ln?: string;
ownr_fn?: string;
ownr_title?: string;
ownr_co_nm?: string;
ownr_addr1?: string;
ownr_addr2?: string;
ownr_city?: string;
ownr_st?: string;
ownr_zip?: string;
ownr_ctry?: string;
ownr_ph1?: string;
ownr_ph2?: string;
ownr_ea?: string;
// Owner data object - referenced in the code
owner: {
data: OwnerRecordInterface;
};
}
export interface OwnerRecordInterface {
ownr_ln?: string;
ownr_fn?: string;
ownr_title?: string;
ownr_co_nm?: string;
ownr_addr1?: string;
ownr_addr2?: string;
ownr_city?: string;
ownr_st?: string;
ownr_zip?: string;
ownr_ctry?: string;
ownr_ph1?: string;
ownr_ph2?: string;
ownr_ea?: string;
shopid: UUID;
}

View File

@@ -0,0 +1,237 @@
import { platform } from "@electron-toolkit/utils";
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import { DecodedAd1, OwnerRecordInterface } from "./decode-ad1.interface";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeAD1 = async (
extensionlessFilePath: string,
): Promise<DecodedAd1> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}A.AD1`);
} catch {
// log.debug("Error opening AD1 File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.AD1`);
// log.debug("Trying to find AD1 file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any AD1 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD1 files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["a.ad1", ".ad1"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any AD1 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD1 files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening AD1 File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD1 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawAd1Data: DecodedAd1 = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"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",
]),
);
//Copy specific logic for manipulation.
//If ownr_ph1 is missing, use ownr_ph2
if (rawAd1Data.asgn_date) {
const newAsgnDate = new Date(rawAd1Data.asgn_date);
rawAd1Data.asgn_date = newAsgnDate.toISOString().split("T")[0];
}
if (!rawAd1Data.ownr_ph1 || _.isEmpty(rawAd1Data.ownr_ph1)) {
rawAd1Data.ownr_ph1 = rawAd1Data.ownr_ph2;
}
if (rawAd1Data.clm_no === "") {
rawAd1Data.clm_no = undefined;
}
let ownerRecord: OwnerRecordInterface;
//Check if the owner information is there. If not, use the insured information as a fallback.
if (
_.isEmpty(rawAd1Data.ownr_ln) &&
_.isEmpty(rawAd1Data.ownr_fn) &&
_.isEmpty(rawAd1Data.ownr_co_nm)
) {
//They're all empty. Using the insured information as a fallback.
// Build up the owner record to insert it alongside the job.
//TODO: Verify that this should be the insured, and not the claimant.
ownerRecord = {
ownr_ln: rawAd1Data.insd_ln,
ownr_fn: rawAd1Data.insd_fn,
ownr_title: rawAd1Data.insd_title,
ownr_co_nm: rawAd1Data.insd_co_nm,
ownr_addr1: rawAd1Data.insd_addr1,
ownr_addr2: rawAd1Data.insd_addr2,
ownr_city: rawAd1Data.insd_city,
ownr_st: rawAd1Data.insd_st,
ownr_zip: rawAd1Data.insd_zip,
ownr_ctry: rawAd1Data.insd_ctry,
ownr_ph1: rawAd1Data.insd_ph1,
ownr_ph2: rawAd1Data.insd_ph2,
ownr_ea: rawAd1Data.insd_ea,
shopid: store.get("app.bodyshop.id"),
};
} else {
//Use the owner information.
ownerRecord = {
ownr_ln: rawAd1Data.ownr_ln,
ownr_fn: rawAd1Data.ownr_fn,
ownr_title: rawAd1Data.ownr_title,
ownr_co_nm: rawAd1Data.ownr_co_nm,
ownr_addr1: rawAd1Data.ownr_addr1,
ownr_addr2: rawAd1Data.ownr_addr2,
ownr_city: rawAd1Data.ownr_city,
ownr_st: rawAd1Data.ownr_st,
ownr_zip: rawAd1Data.ownr_zip,
ownr_ctry: rawAd1Data.ownr_ctry,
ownr_ph1: rawAd1Data.ownr_ph1,
ownr_ph2: rawAd1Data.ownr_ph2,
ownr_ea: rawAd1Data.ownr_ea,
shopid: store.get("app.bodyshop.id"),
};
}
return { ...rawAd1Data, owner: { data: ownerRecord } };
};
export default DecodeAD1;

View File

@@ -0,0 +1,29 @@
export interface DecodedAD2 {
clmt_ln?: string;
clmt_fn?: string;
clmt_title?: string;
clmt_co_nm?: string;
clmt_addr1?: string;
clmt_addr2?: string;
clmt_city?: string;
clmt_st?: string;
clmt_zip?: string;
clmt_ctry?: string;
clmt_ph1?: string;
clmt_ph2?: string;
clmt_ea?: string;
est_co_id?: string;
est_co_nm?: string;
est_addr1?: string;
est_addr2?: string;
est_city?: string;
est_st?: string;
est_zip?: string;
est_ctry?: string;
est_ph1?: string;
est_ct_ln?: string;
est_ct_fn?: string;
est_ea?: string;
date_estimated?: Date; // This is transformed from insp_date
insp_date?: Date; // This exists initially but gets deleted
}

View File

@@ -0,0 +1,170 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import { DecodedAD2 } from "./decode-ad2.interface";
import { platform } from "@electron-toolkit/utils";
import errorTypeCheck from "../../util/errorTypeCheck";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeAD2 = async (
extensionlessFilePath: string,
): Promise<DecodedAD2> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}B.AD2`);
} catch {
dbf = await DBFFile.open(`${extensionlessFilePath}.AD2`);
}
if (!dbf) {
log.error(`Could not find any AD2 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD2 files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["b.ad2", ".ad2"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any AD2 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD2 files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening AD2 File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawAd2Data: DecodedAD2 = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"CLMT_LN", //TODO: This claimant info shouldnt be passed back. Just for the owner info.
"CLMT_FN",
"CLMT_TITLE",
"CLMT_CO_NM",
"CLMT_ADDR1",
"CLMT_ADDR2",
"CLMT_CITY",
"CLMT_ST",
"CLMT_ZIP",
"CLMT_CTRY",
"CLMT_PH1",
//"CLMT_PH1X",
"CLMT_PH2",
//"CLMT_PH2X",
//"CLMT_FAX",
//"CLMT_FAXX",
"CLMT_EA",
//"EST_CO_ID",
"EST_CO_NM",
"EST_ADDR1",
"EST_ADDR2",
"EST_CITY",
"EST_ST",
"EST_ZIP",
"EST_CTRY",
"EST_PH1",
//"EST_PH1X",
//"EST_PH2",
//"EST_PH2X",
//"EST_FAX",
//"EST_FAXX",
"EST_CT_LN",
"EST_CT_FN",
"EST_EA",
//"EST_LIC_NO",
//"EST_FILENO",
//"INSP_CT_LN",
//"INSP_CT_FN",
//"INSP_ADDR1",
//"INSP_ADDR2",
//"INSP_CITY",
//"INSP_ST",
//"INSP_ZIP",
//"INSP_CTRY",
//"INSP_PH1",
//"INSP_PH1X",
//"INSP_PH2",
//"INSP_PH2X",
//"INSP_FAX",
//"INSP_FAXX",
//"INSP_EA",
//"INSP_CODE",
//"INSP_DESC",
"INSP_DATE", //RENAME TO date_estimated
//"INSP_TIME",
//"RF_CO_ID",
//"RF_CO_NM",
//"RF_ADDR1",
//"RF_ADDR2",
//"RF_CITY",
//"RF_ST",
//"RF_ZIP",
//"RF_CTRY",
//"RF_PH1",
//"RF_PH1X",
//"RF_PH2",
//"RF_PH2X",
//"RF_FAX",
//"RF_FAXX",
//"RF_CT_LN",
//"RF_CT_FN",
//"RF_EA",
//"RF_TAX_ID",
//"RF_LIC_NO",
//"RF_BAR_NO",
//"RO_IN_DATE",
//"RO_IN_TIME",
//"TAR_DATE",
//"TAR_TIME",
//"RO_CMPDATE",
//"RO_CMPTIME",
//"DATE_OUT",
//"TIME_OUT",
//"RF_ESTIMTR",
//"MKTG_TYPE",
//"MKTG_SRC",
//"LOC_NM",
//"LOC_ADDR1",
//"LOC_ADDR2",
//"LOC_CITY",
//"LOC_ST",
//"LOC_ZIP",
//"LOC_CTRY",
//"LOC_PH1",
//"LOC_PH1X",
//"LOC_PH2",
//"LOC_PH2X",
//"LOC_FAX",
//"LOC_FAXX",
//"LOC_CT_LN",
//"LOC_CT_FN",
//"LOC_TITLE",
//"LOC_PH",
//"LOC_PHX",
//"LOC_EA",
]),
);
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
rawAd2Data.date_estimated = rawAd2Data.insp_date;
delete rawAd2Data.insp_date;
return rawAd2Data;
};
export default DecodeAD2;

View File

@@ -0,0 +1,5 @@
export interface DecodedEnv {
est_system?: string;
estfile_id?: string;
ciecaid?: string;
}

View File

@@ -0,0 +1,68 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedEnv } from "./decode-env.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeEnv = async (
extensionlessFilePath: string,
): Promise<DecodedEnv> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.ENV`);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any ENV files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".env"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any ENV files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//TODO: Determine if there's any value to capture the whole ENV file.
const rawEnvData: DecodedEnv = deepLowerCaseKeys(
_.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",
"ESTFILE_ID",
]),
);
rawEnvData.ciecaid = rawEnvData.estfile_id;
delete rawEnvData.estfile_id;
//Apply business logic transfomrations.
return rawEnvData;
};
export default DecodeEnv;

View File

@@ -0,0 +1,54 @@
export interface DecodedLinLine {
line_no?: string;
line_ind?: string;
line_ref?: string;
tran_code?: string;
db_ref?: string;
unq_seq?: string;
//who_pays?: string;
line_desc?: string;
part_type?: string;
//part_desc_j?: boolean;
glass_flag?: boolean;
oem_partno?: string;
price_inc?: boolean;
alt_part_i?: boolean;
tax_part?: boolean;
db_price?: number;
act_price?: number;
price_j?: boolean;
cert_part?: boolean;
part_qty?: number;
alt_co_id?: string;
alt_partno?: string;
alt_overrd?: boolean;
alt_partm?: string;
prt_dsmk_p?: string;
prt_dsmk_m?: string;
mod_lbr_ty?: string;
db_hrs?: number;
mod_lb_hrs?: number;
lbr_inc?: boolean;
lbr_op?: string;
lbr_hrs_j?: boolean;
lbr_typ_j?: boolean;
lbr_op_j?: boolean;
paint_stg?: string;
paint_tone?: string;
lbr_tax?: boolean;
lbr_amt?: number;
misc_amt?: number;
misc_sublt?: string;
misc_tax?: boolean;
bett_type?: string;
bett_pctg?: string | number;
bett_amt?: number;
bett_tax?: boolean;
op_code_desc?: string;
}
export interface DecodedLin {
joblines: {
data: DecodedLinLine[];
};
}

View File

@@ -0,0 +1,120 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import { DecodedLin, DecodedLinLine } from "./decode-lin.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeLin = async (
extensionlessFilePath: string,
): Promise<DecodedLin> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.LIN`);
} catch (error) {
//LIN File only has 1 location.
log.error("Error opening LIN File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any LIN files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any LIN files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["lin"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any LIN files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any LIN files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening LIN File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const opCodeData = store.get("app.masterdata.opcodes"); //TODO: Type the op codes
const rawLinData: DecodedLinLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedLinLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"LINE_NO",
"LINE_IND",
"LINE_REF",
"TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
//TODO: Believe this was previously broken in partner. Need to confirm.
// system == "M" ? "PART_DESCJ" : "PART_DES_J",
//"PART_DESC_J",
//End Check
"GLASS_FLAG",
"OEM_PARTNO",
"PRICE_INC",
"ALT_PART_I",
"TAX_PART",
"DB_PRICE",
"ACT_PRICE",
"PRICE_J",
"CERT_PART",
"PART_QTY",
"ALT_CO_ID",
"ALT_PARTNO",
"ALT_OVERRD",
"ALT_PARTM",
"PRT_DSMK_P",
"PRT_DSMK_M",
"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",
]),
);
//Apply line by line adjustments.
singleLineData.op_code_desc = opCodeData[singleLineData.lbr_op]?.desc;
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { joblines: { data: rawLinData } };
};
export default DecodeLin;

View File

@@ -0,0 +1,15 @@
export interface DecodedPfh {
tax_prethr: number;
tax_thr_amt?: number;
tax_pstthr?: number;
tax_tow_rt: number;
tax_str_rt: number;
tax_sub_rt: number;
tax_lbr_rt: number;
federal_tax_rate: number;
adj_g_disc?: number;
adj_towdis?: number;
adj_strdis?: number;
tax_predis?: number;
tax_gst_rt?: number;
}

View File

@@ -0,0 +1,94 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfh } from "./decode-pfh.interface";
import { platform } from "os";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfh = async (
extensionlessFilePath: string,
): Promise<DecodedPfh> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
log.log("Trying to find PFH file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any PFH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFH files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfh"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFH files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfhData: DecodedPfh = deepLowerCaseKeys(
_.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
"TAX_PRETHR",
"TAX_THRAMT",
"TAX_PSTTHR",
//"TAX_TOW_IN", //Remove
"TAX_TOW_RT",
//"TAX_STR_IN", //Remove
"TAX_STR_RT",
//"TAX_SUB_IN", //Remove
"TAX_SUB_RT",
//"TAX_BTR_IN", //Remove
"TAX_LBR_RT",
"TAX_GST_RT",
//"TAX_GST_IN", //Remove
"ADJ_G_DISC",
"ADJ_TOWDIS",
"ADJ_STRDIS",
//"ADJ_BTR_IN", //Remove
"TAX_PREDIS",
]),
);
//Apply business logic transfomrations.
//Standardize some of the numbers and divide by 100.
rawPfhData.tax_prethr = (rawPfhData.tax_prethr ?? 0) / 100;
rawPfhData.tax_pstthr = (rawPfhData.tax_pstthr ?? 0) / 100;
rawPfhData.tax_tow_rt = (rawPfhData.tax_tow_rt ?? 0) / 100;
rawPfhData.tax_str_rt = (rawPfhData.tax_str_rt ?? 0) / 100;
rawPfhData.tax_sub_rt = (rawPfhData.tax_sub_rt ?? 0) / 100;
rawPfhData.tax_lbr_rt = (rawPfhData.tax_lbr_rt ?? 0) / 100;
rawPfhData.federal_tax_rate = (rawPfhData.tax_gst_rt ?? 0) / 100;
delete rawPfhData.tax_gst_rt;
return rawPfhData;
};
export default DecodePfh;

View File

@@ -0,0 +1,57 @@
//TODO: Clean up this interface. A bit messy as we store the data in very different ways.
export interface DecodedPflLine {
lbr_type: string;
lbr_desc: string;
lbr_rate: number;
lbr_tax_in: boolean;
lbr_taxp: number;
lbr_adjP: number;
lbr_tx_ty1: string;
lbr_tx_in1: boolean;
lbr_tx_ty2: string;
lbr_tx_in2: boolean;
lbr_tx_ty3: string;
lbr_tx_in3: boolean;
lbr_tx_ty4: string;
lbr_tx_in4: boolean;
lbr_tx_ty5: string;
lbr_tx_in5: boolean;
}
export interface JobLaborRateFields {
rate_laa: number;
rate_lab: number;
rate_lad: number;
rate_las: number;
rate_lar: number;
rate_lae: number;
rate_lag: number;
rate_laf: number;
rate_lam: number;
rate_lau: number;
rate_la1: number;
rate_la2: number;
rate_la3: number;
rate_la4: number;
}
export interface CiecaPfl {
LAA?: DecodedPflLine;
LAB?: DecodedPflLine;
LAD?: DecodedPflLine;
LAS?: DecodedPflLine;
LAR?: DecodedPflLine;
LAE?: DecodedPflLine;
LAG?: DecodedPflLine;
LAF?: DecodedPflLine;
LAM?: DecodedPflLine;
LAU?: DecodedPflLine;
LA1?: DecodedPflLine;
LA2?: DecodedPflLine;
LA3?: DecodedPflLine;
LA4?: DecodedPflLine;
}
export interface DecodedPfl extends JobLaborRateFields {
cieca_pfl: CiecaPfl;
}

View File

@@ -0,0 +1,122 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
DecodedPfl,
JobLaborRateFields,
DecodedPflLine,
} from "./decode-pfl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfl = async (
extensionlessFilePath: string,
): Promise<DecodedPfl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFL`);
} catch (error) {
//PFL File only has 1 location.
log.error("Error opening PFL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const jobLaborRates: JobLaborRateFields = {
rate_laa: 0,
rate_lab: 0,
rate_lad: 0,
rate_las: 0,
rate_lar: 0,
rate_lae: 0,
rate_lag: 0,
rate_laf: 0,
rate_lam: 0,
rate_lau: 0,
rate_la1: 0,
rate_la2: 0,
rate_la3: 0,
rate_la4: 0,
};
const rawPflData: DecodedPflLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPflLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"LBR_TYPE",
"LBR_DESC",
"LBR_RATE",
"LBR_TAX_IN",
"LBR_TAXP",
"LBR_ADJP",
"LBR_TX_TY1",
"LBR_TX_IN1",
"LBR_TX_TY2",
"LBR_TX_IN2",
"LBR_TX_TY3",
"LBR_TX_IN3",
"LBR_TX_TY4",
"LBR_TX_IN4",
"LBR_TX_TY5",
"LBR_TX_IN5",
]),
);
//Apply line by line adjustments.
//Set the job.rate_<CIECA_TYPE> field based on the value.
jobLaborRates[`rate_${singleLineData.lbr_type.toLowerCase()}`] =
singleLineData.lbr_rate;
//For Mitchell, Alum is stored under LA3 instead of LAA. Shift it back over.
//The old partner had a check for this, but it always was true. Matching that logic.
if (singleLineData.lbr_type === "LA3") {
jobLaborRates[`rate_laa`] = singleLineData.lbr_rate;
}
//Also capture the whole object.
//This is segmented because the whole object was not previously captured for ImEX as it wasn't needed.
//Rome needs the whole object to accurately calculate the tax rates.
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
const pflObj = _.keyBy(rawPflData, "lbr_type");
return { ...jobLaborRates, cieca_pfl: pflObj };
};
export default DecodePfl;

View File

@@ -0,0 +1,50 @@
export interface DecodedPfmLine {
matl_type?: string;
cal_code?: number;
cal_desc?: string;
cal_maxdlr?: number;
cal_prip?: number;
cal_secp?: number;
mat_calp?: number;
cal_prethr?: number;
cal_pstthr?: number;
cal_thramt?: number;
cal_lbrmin?: number;
cal_lbrrte?: number;
cal_opcode?: string;
tax_ind?: boolean;
mat_taxp?: number;
mat_adjp?: number;
mat_tx_ty1?: string;
mat_tx_in1?: boolean;
mat_tx_ty2?: string;
mat_tx_in2?: boolean;
mat_tx_ty3?: string;
mat_tx_in3?: boolean;
mat_tx_ty4?: string;
mat_tx_in4?: boolean;
mat_tx_ty5?: string;
mat_tx_in5?: boolean;
}
export interface JobMaterialRateFields {
rate_mapa: number;
tax_paint_mat_rt: number;
rate_mash: number;
tax_shop_mat_rt: number;
rate_mahw: number;
tax_levies_rt: number;
rate_ma2s: number;
rate_ma2t: number;
rate_ma3s: number;
rate_macs: number;
rate_mabl: number;
}
export interface DecodedPfm extends JobMaterialRateFields {
materials: {
MAPA?: DecodedPfmLine;
MASH?: DecodedPfmLine;
};
cieca_pfm?: DecodedPfmLine[];
}

View File

@@ -0,0 +1,169 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import {
DecodedPfm,
DecodedPfmLine,
JobMaterialRateFields,
} from "./decode-pfm.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfm = async (
extensionlessFilePath: string,
): Promise<DecodedPfm> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFM`);
} catch (error) {
//PFM File only has 1 location.
log.error("Error opening PFM File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFM files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFM files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfm"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFM files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFM files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFM File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const jobMaterialRates: JobMaterialRateFields = {
rate_mapa: 0,
tax_paint_mat_rt: 0,
rate_mash: 0,
tax_shop_mat_rt: 0,
rate_mahw: 0,
tax_levies_rt: 0,
rate_ma2s: 0,
rate_ma2t: 0,
rate_ma3s: 0,
rate_macs: 0,
rate_mabl: 0,
};
const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfmLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"MATL_TYPE",
"CAL_CODE",
"CAL_DESC",
"CAL_MAXDLR",
"CAL_PRIP",
"CAL_SECP",
"MAT_CALP",
"CAL_PRETHR", //Mitchell here
"CAL_PSTTHR",
"CAL_THRAMT",
"CAL_LBRMIN",
"CAL_LBRRTE", //Audatex puts it here
"CAL_OPCODE",
"TAX_IND",
"MAT_TAXP",
"MAT_ADJP",
"MAT_TX_TY1",
"MAT_TX_IN1",
"MAT_TX_TY2",
"MAT_TX_IN2",
"MAT_TX_TY3",
"MAT_TX_IN3",
"MAT_TX_TY4",
"MAT_TX_IN4",
"MAT_TX_TY5",
"MAT_TX_IN5",
]),
),
);
//Also capture the whole object.
//This is segmented because the whole object was not previously captured for ImEX as it wasn't needed.
//Rome needs the whole object to accurately calculate the tax rates.
return singleLineData;
});
//Apply line by line adjustments.
const mapaLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAPA",
);
if (mapaLine) {
jobMaterialRates.rate_mapa =
mapaLine.cal_lbrrte || mapaLine.cal_prethr || 0;
jobMaterialRates.tax_paint_mat_rt = (mapaLine.mat_taxp ?? 0) / 100;
}
const mashLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MASH",
);
if (mashLine) {
jobMaterialRates.rate_mash =
mashLine.cal_lbrrte || mashLine.cal_prethr || 0;
jobMaterialRates.tax_shop_mat_rt = (mashLine.mat_taxp ?? 0) / 100;
}
const mahwLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAHW",
);
if (mahwLine) {
jobMaterialRates.rate_mahw =
mahwLine.cal_lbrrte || mahwLine.cal_prethr || 0;
jobMaterialRates.tax_levies_rt = (mahwLine.mat_taxp ?? 0) / 100;
}
const additionalMaterials = ["MA2S", "MA2T", "MA3S", "MACS", "MABL"];
additionalMaterials.forEach((type) => {
const line: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === type,
);
if (line) {
jobMaterialRates[`rate_${type.toLowerCase()}`] =
line.cal_lbrrte || line.cal_prethr || 0;
}
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return {
...jobMaterialRates,
materials: {
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.
};
};
export default DecodePfm;

View File

@@ -0,0 +1,32 @@
export interface DecodedPfoLine {
tx_tow_ty?: string;
tow_t_ty1?: string;
tow_t_in1?: boolean;
tow_t_ty2?: string;
tow_t_in2?: boolean;
tow_t_ty3?: string;
tow_t_in3?: boolean;
tow_t_ty4?: string;
tow_t_in4?: boolean;
tow_t_ty5?: string;
tow_t_in5?: boolean;
tow_t_ty6?: string;
tow_t_in6?: boolean;
tx_stor_ty?: string;
stor_t_ty1?: string;
stor_t_in1?: boolean;
stor_t_ty2?: string;
stor_t_in2?: boolean;
stor_t_ty3?: string;
stor_t_in3?: boolean;
stor_t_ty4?: string;
stor_t_in4?: boolean;
stor_t_ty5?: string;
stor_t_in5?: boolean;
stor_t_ty6?: string;
stor_t_in6?: boolean;
}
export interface DecodedPfo {
cieca_pfo: DecodedPfoLine;
}

View File

@@ -0,0 +1,93 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import { DecodedPfo, DecodedPfoLine } from "./decode-pfo.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfo = async (
extensionlessFilePath: string,
): Promise<DecodedPfo> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFO`);
} catch (error) {
log.error("Error opening PFO File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.PFO`);
log.log("Trying to find PFO file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any PFO files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFO files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfo"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFO files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFO files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFO File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFO will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfoData: DecodedPfoLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TX_TOW_TY",
"TOW_T_TY1",
"TOW_T_IN1",
"TOW_T_TY2",
"TOW_T_IN2",
"TOW_T_TY3",
"TOW_T_IN3",
"TOW_T_TY4",
"TOW_T_IN4",
"TOW_T_TY5",
"TOW_T_IN5",
"TOW_T_TY6",
"TOW_T_IN6",
"TX_STOR_TY",
"STOR_T_TY1",
"STOR_T_IN1",
"STOR_T_TY2",
"STOR_T_IN2",
"STOR_T_TY3",
"STOR_T_IN3",
"STOR_T_TY4",
"STOR_T_IN4",
"STOR_T_TY5",
"STOR_T_IN5",
"STOR_T_TY6",
"STOR_T_IN6",
]),
),
);
//Apply business logic transfomrations.
return { cieca_pfo: rawPfoData };
};
export default DecodePfo;

View File

@@ -0,0 +1,37 @@
export interface DecodedPfpLine {
prt_type: string;
prt_tax_in: boolean;
prt_tax_rt: number;
prt_mkupp: number;
prt_mktyp: string;
prt_discp: number;
prt_tx_ty1: string;
prt_tx_in1: boolean;
prt_tx_ty2: string;
prt_tx_in2: boolean;
prt_tx_ty3: string;
prt_tx_in3: boolean;
prt_tx_ty4: string;
prt_tx_in4: boolean;
prt_tx_ty5: string;
prt_tx_in5: boolean;
}
export interface DecodedPfpLinesByType {
PAA: DecodedPfpLine;
PAC: DecodedPfpLine;
PAL: DecodedPfpLine;
PAG: DecodedPfpLine;
PAM: DecodedPfpLine;
PAP: DecodedPfpLine;
PAN: DecodedPfpLine;
PAO: DecodedPfpLine;
PAR: DecodedPfpLine;
PAS: DecodedPfpLine;
PASL: DecodedPfpLine;
PAT: DecodedPfpLine;
}
export interface DecodedPfp {
parts_tax_rates: DecodedPfpLinesByType;
}

View File

@@ -0,0 +1,98 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import {
DecodedPfp,
DecodedPfpLine,
DecodedPfpLinesByType,
} from "./decode-pfp.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfp = async (
extensionlessFilePath: string,
): Promise<DecodedPfp> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFP`);
} catch (error) {
//PFP File only has 1 location.
log.error("Error opening PFP File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFP files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfp"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFP files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFP File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfpData: DecodedPfpLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfpLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"PRT_TYPE",
"PRT_TAX_IN",
"PRT_TAX_RT",
"PRT_MKUPP",
"PRT_MKTYP",
"PRT_DISCP",
"PRT_TX_TY1",
"PRT_TX_IN1",
"PRT_TX_TY2",
"PRT_TX_IN2",
"PRT_TX_TY3",
"PRT_TX_IN3",
"PRT_TX_TY4",
"PRT_TX_IN4",
"PRT_TX_TY5",
"PRT_TX_IN5",
]),
);
singleLineData.prt_tax_rt = singleLineData.prt_tax_rt / 100;
return YNBoolConverter(singleLineData);
});
//Apply business logic transfomrations.
//Convert array of lines to a hash object.
const parsedPfpFile: DecodedPfpLinesByType = rawPfpData.reduce(
(acc: DecodedPfpLinesByType, line: DecodedPfpLine) => {
acc[line.prt_type] = line;
return acc;
},
{} as DecodedPfpLinesByType,
);
return { parts_tax_rates: parsedPfpFile };
};
export default DecodePfp;

View File

@@ -0,0 +1,147 @@
/**
* Interface representing decoded data from a PFT file
* Contains tax type information with up to 6 tax types and 5 tiers each
*/
export interface DecodedPftLine {
// Tax Type 1
tax_type1?: string;
ty1_tier1?: number;
ty1_thres1?: number;
ty1_rate1?: number;
ty1_sur1?: number;
ty1_tier2?: number;
ty1_thres2?: number;
ty1_rate2?: number;
ty1_sur2?: number;
ty1_tier3?: number;
ty1_thres3?: number;
ty1_rate3?: number;
ty1_sur3?: number;
ty1_tier4?: number;
ty1_thres4?: number;
ty1_rate4?: number;
ty1_sur4?: number;
ty1_tier5?: number;
ty1_thres5?: number;
ty1_rate5?: number;
ty1_sur5?: number;
// Tax Type 2
tax_type2?: string;
ty2_tier1?: number;
ty2_thres1?: number;
ty2_rate1?: number;
ty2_sur1?: number;
ty2_tier2?: number;
ty2_thres2?: number;
ty2_rate2?: number;
ty2_sur2?: number;
ty2_tier3?: number;
ty2_thres3?: number;
ty2_rate3?: number;
ty2_sur3?: number;
ty2_tier4?: number;
ty2_thres4?: number;
ty2_rate4?: number;
ty2_sur4?: number;
ty2_tier5?: number;
ty2_thres5?: number;
ty2_rate5?: number;
ty2_sur5?: number;
// Tax Type 3
tax_type3?: string;
ty3_tier1?: number;
ty3_thres1?: number;
ty3_rate1?: number;
ty3_sur1?: number;
ty3_tier2?: number;
ty3_thres2?: number;
ty3_rate2?: number;
ty3_sur2?: number;
ty3_tier3?: number;
ty3_thres3?: number;
ty3_rate3?: number;
ty3_sur3?: number;
ty3_tier4?: number;
ty3_thres4?: number;
ty3_rate4?: number;
ty3_sur4?: number;
ty3_tier5?: number;
ty3_thres5?: number;
ty3_rate5?: number;
ty3_sur5?: number;
// Tax Type 4
tax_type4?: string;
ty4_tier1?: number;
ty4_thres1?: number;
ty4_rate1?: number;
ty4_sur1?: number;
ty4_tier2?: number;
ty4_thres2?: number;
ty4_rate2?: number;
ty4_sur2?: number;
ty4_tier3?: number;
ty4_thres3?: number;
ty4_rate3?: number;
ty4_sur3?: number;
ty4_tier4?: number;
ty4_thres4?: number;
ty4_rate4?: number;
ty4_sur4?: number;
ty4_tier5?: number;
ty4_thres5?: number;
ty4_rate5?: number;
ty4_sur5?: number;
// Tax Type 5
tax_type5?: string;
ty5_tier1?: number;
ty5_thres1?: number;
ty5_rate1?: number;
ty5_sur1?: number;
ty5_tier2?: number;
ty5_thres2?: number;
ty5_rate2?: number;
ty5_sur2?: number;
ty5_tier3?: number;
ty5_thres3?: number;
ty5_rate3?: number;
ty5_sur3?: number;
ty5_tier4?: number;
ty5_thres4?: number;
ty5_rate4?: number;
ty5_sur4?: number;
ty5_tier5?: number;
ty5_thres5?: number;
ty5_rate5?: number;
ty5_sur5?: number;
// Tax Type 6
tax_type6?: string;
ty6_tier1?: number;
ty6_thres1?: number;
ty6_rate1?: number;
ty6_sur1?: number;
ty6_tier2?: number;
ty6_thres2?: number;
ty6_rate2?: number;
ty6_sur2?: number;
ty6_tier3?: number;
ty6_thres3?: number;
ty6_rate3?: number;
ty6_sur3?: number;
ty6_tier4?: number;
ty6_thres4?: number;
ty6_rate4?: number;
ty6_sur4?: number;
ty6_tier5?: number;
ty6_thres5?: number;
ty6_rate5?: number;
ty6_sur5?: number;
}
export interface DecodedPft {
cieca_pft: DecodedPftLine;
}

View File

@@ -0,0 +1,189 @@
import { platform } from "@electron-toolkit/utils";
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPft, DecodedPftLine } from "./decode-pft.interface";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePft = async (
extensionlessFilePath: string,
): Promise<DecodedPft> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFT`);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFT files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFT files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["pft"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFT files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFT files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFT File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFT will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPftData: DecodedPftLine = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TAX_TYPE1", //The below is is taken from a CCC estimate. Will require validation to ensure it is also accurate for Audatex/Mitchell
"TY1_TIER1",
"TY1_THRES1",
"TY1_RATE1",
"TY1_SUR1",
"TY1_TIER2",
"TY1_THRES2",
"TY1_RATE2",
"TY1_SUR2",
"TY1_TIER3",
"TY1_THRES3",
"TY1_RATE3",
"TY1_SUR3",
"TY1_TIER4",
"TY1_THRES4",
"TY1_RATE4",
"TY1_SUR4",
"TY1_TIER5",
"TY1_THRES5",
"TY1_RATE5",
"TY1_SUR5",
"TAX_TYPE2",
"TY2_TIER1",
"TY2_THRES1",
"TY2_RATE1",
"TY2_SUR1",
"TY2_TIER2",
"TY2_THRES2",
"TY2_RATE2",
"TY2_SUR2",
"TY2_TIER3",
"TY2_THRES3",
"TY2_RATE3",
"TY2_SUR3",
"TY2_TIER4",
"TY2_THRES4",
"TY2_RATE4",
"TY2_SUR4",
"TY2_TIER5",
"TY2_THRES5",
"TY2_RATE5",
"TY2_SUR5",
"TAX_TYPE3",
"TY3_TIER1",
"TY3_THRES1",
"TY3_RATE1",
"TY3_SUR1",
"TY3_TIER2",
"TY3_THRES2",
"TY3_RATE2",
"TY3_SUR2",
"TY3_TIER3",
"TY3_THRES3",
"TY3_RATE3",
"TY3_SUR3",
"TY3_TIER4",
"TY3_THRES4",
"TY3_RATE4",
"TY3_SUR4",
"TY3_TIER5",
"TY3_THRES5",
"TY3_RATE5",
"TY3_SUR5",
"TAX_TYPE4",
"TY4_TIER1",
"TY4_THRES1",
"TY4_RATE1",
"TY4_SUR1",
"TY4_TIER2",
"TY4_THRES2",
"TY4_RATE2",
"TY4_SUR2",
"TY4_TIER3",
"TY4_THRES3",
"TY4_RATE3",
"TY4_SUR3",
"TY4_TIER4",
"TY4_THRES4",
"TY4_RATE4",
"TY4_SUR4",
"TY4_TIER5",
"TY4_THRES5",
"TY4_RATE5",
"TY4_SUR5",
"TAX_TYPE5",
"TY5_TIER1",
"TY5_THRES1",
"TY5_RATE1",
"TY5_SUR1",
"TY5_TIER2",
"TY5_THRES2",
"TY5_RATE2",
"TY5_SUR2",
"TY5_TIER3",
"TY5_THRES3",
"TY5_RATE3",
"TY5_SUR3",
"TY5_TIER4",
"TY5_THRES4",
"TY5_RATE4",
"TY5_SUR4",
"TY5_TIER5",
"TY5_THRES5",
"TY5_RATE5",
"TY5_SUR5",
"TAX_TYPE6",
"TY6_TIER1",
"TY6_THRES1",
"TY6_RATE1",
"TY6_SUR1",
"TY6_TIER2",
"TY6_THRES2",
"TY6_RATE2",
"TY6_SUR2",
"TY6_TIER3",
"TY6_THRES3",
"TY6_RATE3",
"TY6_SUR3",
"TY6_TIER4",
"TY6_THRES4",
"TY6_RATE4",
"TY6_SUR4",
"TY6_TIER5",
"TY6_THRES5",
"TY6_RATE5",
"TY6_SUR5",
]),
);
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { cieca_pft: rawPftData };
};
export default DecodePft;

View File

@@ -0,0 +1,23 @@
export interface DecodedStlLine {
ttl_type?: string;
ttl_typecd?: string;
t_amt?: number;
t_hrs?: number;
t_addlbr?: number;
t_discamt?: number;
t_mkupamt?: number;
t_gdiscamt?: number;
tax_amt?: number;
nt_amt?: number;
nt_hrs?: number;
nt_addlbr?: number;
nt_disc?: number;
nt_mkup?: number;
nt_gdis?: number;
ttl_typamt?: number;
ttl_hrs?: number;
ttl_amt?: number;
}
export interface DecodedStl {
cieca_stl: { data: DecodedStlLine[] };
}

View File

@@ -0,0 +1,85 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedStl, DecodedStlLine } from "./decode-stl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeStl = async (
extensionlessFilePath: string,
): Promise<DecodedStl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.STL`);
} catch (error) {
log.error("Error opening STL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any STL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any STL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["stl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any STL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any STL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening STL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawStlData: DecodedStlLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedStlLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"TTL_TYPE",
"TTL_TYPECD",
"T_AMT",
"T_HRS",
"T_ADDLBR",
"T_DISCAMT",
"T_MKUPAMT",
"T_GDISCAMT",
"TAX_AMT",
"NT_AMT",
"NT_HRS",
"NT_ADDLBR",
"NT_DISC",
"NT_MKUP",
"NT_GDIS",
"TTL_TYPAMT",
"TTL_HRS",
"TTL_AMT",
]),
);
//Apply line by line adjustments.
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { cieca_stl: { data: rawStlData } };
};
export default DecodeStl;

View File

@@ -0,0 +1,22 @@
export interface DecodedTtl {
clm_total: number;
depreciation_taxes: number;
cieca_ttl: { data: DecodedTtlLine };
}
export interface DecodedTtlLine {
g_ttl_amt?: number;
g_bett_amt?: number;
g_rpd_amt?: number;
g_ded_amt?: number;
g_cust_amt?: number;
g_aa_amt?: number;
n_ttl_amt?: number;
prev_net?: number;
supp_amt?: number;
n_supp_amt?: number;
g_upd_amt?: number;
g_ttl_disc?: number;
g_tax?: number;
gst_amt?: number;
}

View File

@@ -0,0 +1,80 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedTtl, DecodedTtlLine } from "./decode-ttl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeTtl = async (
extensionlessFilePath: string,
): Promise<DecodedTtl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.TTL`);
} catch (error) {
log.error("Error opening TTL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any TTL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any TTL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["ttl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any TTL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any TTL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening TTL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFT will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawTtlData: DecodedTtlLine = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"G_TTL_AMT",
"G_BETT_AMT",
"G_RPD_AMT",
"G_DED_AMT",
"G_CUST_AMT",
"G_AA_AMT",
"N_TTL_AMT",
"PREV_NET",
"SUPP_AMT",
"N_SUPP_AMT", //Previously commented. Possible issue.
"G_UPD_AMT",
"G_TTL_DISC",
"G_TAX",
"GST_AMT",
]),
);
//Apply business logic transfomrations.
return {
clm_total: rawTtlData.g_ttl_amt || 0,
depreciation_taxes: rawTtlData.g_bett_amt || 0, //TODO: Find where this needs to be filled from
cieca_ttl: { data: rawTtlData },
};
};
export default DecodeTtl;

View File

@@ -0,0 +1,64 @@
import { UUID } from "crypto";
export interface DecodedVeh {
// Basic vehicle information
plate_no?: string;
plate_st?: string;
v_vin?: string;
v_model_yr?: string;
v_make_desc?: string;
v_model_desc?: string;
v_color?: string;
kmin?: number;
area_of_damage?: {
impact1?: string;
impact2?: string;
};
// Complete vehicle data object
vehicle?: { data: VehicleRecordInterface };
}
export interface VehicleRecordInterface {
// Area of damage information
area_of_damage?: {
impact1?: string;
impact2?: string;
};
// Paint code information
v_paint_codes: {
paint_cd1: string;
paint_cd2: string;
paint_cd3: string;
};
// Vehicle information from DBF file
db_v_code?: string;
plate_no?: string;
plate_st?: string;
v_vin?: string;
v_cond: string;
v_prod_dt?: Date;
v_model_yr: string;
v_makecode: string;
v_make_desc?: string;
v_model?: string;
v_model_desc?: string;
v_type: string;
v_bstyle?: string;
v_trimcode?: string;
trim_color?: string;
v_mldgcode?: string;
v_engine?: string;
v_mileage?: number; //TODO: This can sometimes come in as UNK.
v_color?: string;
v_tone?: string;
v_stage?: string;
shopid: UUID;
//These are removed during business logic processing.
v_makedesc?: string;
impact_1?: string;
impact_2?: string;
paint_cd1?: string;
paint_cd2?: string;
paint_cd3?: string;
}

View File

@@ -0,0 +1,145 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import { DecodedVeh, VehicleRecordInterface } from "./decode-veh.interface";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import typeCaster from "../../util/typeCaster";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeVeh = async (
extensionlessFilePath: string,
): Promise<DecodedVeh> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
} catch (error) {
log.error("Error opening VEH File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.VEH`);
log.log("Found VEH file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any VEH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any VEH files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["v.veh", ".veh"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any VEH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any VEH files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath, { readMode: "loose" });
} catch (error) {
log.error("Error opening VEH File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
//typeCaster is required as the previous partner sent some of these values toString, and the database was made accordingly rather than keeping their original type.
//Alternative is to change the database schema to match the original type.
const rawVehData: VehicleRecordInterface = typeCaster(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"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_BSTYLE",
"V_TRIMCODE",
"TRIM_COLOR",
"V_MLDGCODE",
"V_ENGINE",
"V_MILEAGE",
"V_COLOR",
"V_TONE",
"V_STAGE",
"PAINT_CD1",
"PAINT_CD2",
"PAINT_CD3",
]),
),
{
v_tone: "string",
v_stage: "string",
},
);
//Apply business logic transfomrations.
//An old error where the column had an extra underscore.
rawVehData.v_make_desc = rawVehData.v_makedesc || rawVehData.v_makecode; //Fallback for US.
delete rawVehData.v_makedesc;
//An old error where the column had an extra underscore.
rawVehData.v_model_desc = rawVehData.v_model;
delete rawVehData.v_model;
//Consolidate Area of Damage.
const area_of_damage = {
impact1: rawVehData.impact_1 ?? "",
impact2: rawVehData.impact_2 ?? "",
};
delete rawVehData.impact_1;
delete rawVehData.impact_2;
const kmin = rawVehData.v_mileage ?? 0;
delete rawVehData.v_mileage;
//Consolidate Paint Code information.
rawVehData.v_paint_codes = {
paint_cd1: rawVehData.paint_cd1 ?? "",
paint_cd2: rawVehData.paint_cd2 ?? "",
paint_cd3: rawVehData.paint_cd3 ?? "",
};
delete rawVehData.paint_cd1;
delete rawVehData.paint_cd2;
delete rawVehData.paint_cd3;
rawVehData.shopid = store.get("app.bodyshop.id");
//Aggregate the vehicle data to be stamped onto the job record.
const jobVehicleData: DecodedVeh = {
plate_no: rawVehData.plate_no,
plate_st: rawVehData.plate_st,
v_vin: rawVehData.v_vin,
v_model_yr: rawVehData.v_model_yr,
v_make_desc: rawVehData.v_make_desc,
v_model_desc: rawVehData.v_model_desc,
v_color: rawVehData.v_color,
kmin: kmin,
area_of_damage: area_of_damage,
vehicle: {
data: rawVehData,
},
};
return jobVehicleData;
};
export default DecodeVeh;

View File

@@ -0,0 +1,47 @@
import log from "electron-log/main";
import fs from "fs";
import path from "path";
const findFileCaseInsensitive = async (
extensionlessFilePath: string,
extensions: string[],
): Promise<string | null> => {
const directory: string = path.dirname(extensionlessFilePath);
try {
const matchingFiles = fs.readdirSync(directory).filter((file: string) => {
return (
extensions.some((ext) =>
file.toLowerCase().endsWith(ext.toLowerCase()),
) &&
path
.basename(file, path.extname(file))
.toLowerCase()
.startsWith(path.basename(extensionlessFilePath).toLowerCase())
);
});
const files: string[] = [];
matchingFiles.forEach((file) => {
const fullPath = path.join(directory, file);
files.push(fullPath);
});
// Return the first matching file if needed
if (files.length > 0) {
return files[0];
}
} catch (error) {
log.error(`Failed to read directory ${directory}:`, error);
throw error;
}
return null;
};
const getFilePathWithoutExtension = (filePath: string): string => {
return path.join(
path.dirname(filePath),
path.basename(filePath, path.extname(filePath)),
);
};
export { findFileCaseInsensitive };

431
src/main/decoder/decoder.ts Normal file
View File

@@ -0,0 +1,431 @@
import { platform } from "@electron-toolkit/utils";
import { UUID } from "crypto";
import { Notification, shell } from "electron";
import log from "electron-log/main";
import fs from "fs";
import _ from "lodash";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import client from "../graphql/graphql-client";
import {
INSERT_AVAILABLE_JOB_TYPED,
InsertAvailableJobResult,
QUERY_JOB_BY_CLM_NO_TYPED,
QUERY_VEHICLE_BY_VIN_TYPED,
QueryJobByClmNoResult,
VehicleQueryResult,
} from "../graphql/queries";
import store from "../store/store";
import DecodeAD1 from "./decode-ad1";
import { DecodedAd1 } from "./decode-ad1.interface";
import DecodeAD2 from "./decode-ad2";
import { DecodedAD2 } from "./decode-ad2.interface";
import DecodeEnv from "./decode-env";
import { DecodedEnv } from "./decode-env.interface";
import DecodeLin from "./decode-lin";
import { DecodedLin } from "./decode-lin.interface";
import DecodePfh from "./decode-pfh";
import { DecodedPfh } from "./decode-pfh.interface";
import DecodePfl from "./decode-pfl";
import { DecodedPfl } from "./decode-pfl.interface";
import DecodePfm from "./decode-pfm";
import { DecodedPfm } from "./decode-pfm.interface";
import DecodePfo from "./decode-pfo";
import { DecodedPfo } from "./decode-pfo.interface";
import DecodePfp from "./decode-pfp";
import { DecodedPfp } from "./decode-pfp.interface";
import DecodePft from "./decode-pft";
import { DecodedPft } from "./decode-pft.interface";
import DecodeStl from "./decode-stl";
import { DecodedStl } from "./decode-stl.interface";
import DecodeTtl from "./decode-ttl";
import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface";
import setAppProgressbar from "../util/setAppProgressBar";
import UploadEmsToS3 from "./emsbackup";
async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name,
);
log.debug("Importing Job", extensionlessFilePath);
try {
await WaitForAllFiles(extensionlessFilePath, requiredExtensions);
//The below all end up returning parts of the job object.
//Some of them return additional info - e.g. owner or vehicle record data at both the job and corresponding table level.
setAppProgressbar(0.1);
const env: DecodedEnv = await DecodeEnv(extensionlessFilePath);
setAppProgressbar(0.15);
const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath);
setAppProgressbar(0.2);
const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath);
setAppProgressbar(0.25);
const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath);
setAppProgressbar(0.3);
const lin: DecodedLin = await DecodeLin(extensionlessFilePath);
setAppProgressbar(0.35);
const pfh: DecodedPfh = await DecodePfh(extensionlessFilePath);
setAppProgressbar(0.4);
const pfl: DecodedPfl = await DecodePfl(extensionlessFilePath);
setAppProgressbar(0.45);
const pft: DecodedPft = await DecodePft(extensionlessFilePath);
setAppProgressbar(0.5);
const pfm: DecodedPfm = await DecodePfm(extensionlessFilePath);
setAppProgressbar(0.55);
const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object
setAppProgressbar(0.6);
const stl: DecodedStl = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object
setAppProgressbar(0.65);
const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath);
setAppProgressbar(0.7);
const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath);
setAppProgressbar(0.75);
const jobObjectUncleaned: RawJobDataObject = {
...env,
...ad1,
...ad2,
...veh,
...lin,
...pfh,
...pfl,
...pft,
...pfm,
...pfo,
...stl,
...ttl,
...pfp,
shopid: store.get("app.bodyshop.id") as UUID,
};
// Replace owner information with claimant information if necessary
const jobObject = ReplaceOwnerInfoWithClaimant(jobObjectUncleaned);
setAppProgressbar(0.8);
if (import.meta.env.DEV) {
// Save jobObject to a timestamped JSON file
const timestamp = new Date()
.toISOString()
.replace(/:/g, "-")
.replace(/\..+/, "");
const fileName = `job_${timestamp}_${parsedFilePath.name}.json`;
const logsDir = path.join(process.cwd(), "logs");
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const filePath = path.join(logsDir, fileName);
fs.writeFileSync(filePath, JSON.stringify(jobObject, null, 2), "utf8");
log.info(`Job data saved to: ${filePath}`);
}
const newAvailableJob: AvailableJobSchema = {
uploaded_by: store.get("user.email"),
bodyshopid: store.get("app.bodyshop.id"),
cieca_id: jobObject.ciecaid,
est_data: jobObject,
ownr_name: `${jobObject.ownr_fn} ${jobObject.ownr_ln} ${jobObject.ownr_co_nm}`,
ins_co_nm: jobObject.ins_co_nm,
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.
issupplement: false,
jobid: null,
};
setAppProgressbar(0.85);
const existingVehicleRecord: VehicleQueryResult = await client.request(
QUERY_VEHICLE_BY_VIN_TYPED,
{
vin: jobObject.v_vin,
},
);
if (existingVehicleRecord.vehicles.length > 0) {
delete newAvailableJob.est_data.vehicle;
newAvailableJob.est_data.vehicleid = existingVehicleRecord.vehicles[0].id;
}
console.log("Available Job record to upload;", newAvailableJob);
setAppProgressbar(0.95);
if (jobObject.clm_no) {
const existingJobRecord: QueryJobByClmNoResult = await client.request(
QUERY_JOB_BY_CLM_NO_TYPED,
{ clm_no: jobObject.clm_no },
);
if (existingJobRecord.jobs.length > 0) {
newAvailableJob.issupplement = true;
newAvailableJob.jobid = existingJobRecord.jobs[0].id;
}
}
const insertRecordResult: InsertAvailableJobResult = await client.request(
INSERT_AVAILABLE_JOB_TYPED,
{
jobInput: [newAvailableJob],
},
);
setAppProgressbar(-1);
const uploadNotification = new Notification({
title: "Job Imported",
//subtitle: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}`,
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`,
actions: [{ text: "View Job", type: "button" }],
});
uploadNotification.on("click", () => {
shell.openExternal(
`${
store.get("app.isTest")
? import.meta.env.VITE_FE_URL_TEST
: import.meta.env.VITE_FE_URL
}/manage/available`,
);
});
uploadNotification.show();
log.debug("Job inserted", insertRecordResult);
UploadEmsToS3({
extensionlessFilePath,
bodyshopid: newAvailableJob.bodyshopid,
ciecaid: jobObject.ciecaid ?? "",
clm_no: jobObject.clm_no ?? "",
ownr_ln: jobObject.ownr_ln ?? "",
});
} catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
const uploadNotificationFailure = new Notification({
title: "Job Upload Failure",
body: errorTypeCheck(error).message, //TODO: Remove after debug.
});
uploadNotificationFailure.show();
}
}
export default ImportJob;
export interface RawJobDataObject
extends DecodedEnv,
DecodedAd1,
DecodedAD2,
DecodedVeh,
DecodedLin,
DecodedPfh,
DecodedPfl,
DecodedPft,
DecodedPfm,
DecodedPfo,
DecodedStl,
DecodedTtl,
DecodedPfp {
vehicleid?: UUID;
shopid: UUID;
}
export interface AvailableJobSchema {
uploaded_by: string;
bodyshopid: UUID;
cieca_id?: string;
est_data: RawJobDataObject;
ownr_name: string;
ins_co_nm?: string;
vehicle_info: string;
clm_no?: string;
clm_amt: number;
source_system?: string | null;
issupplement: boolean;
jobid: UUID | null;
}
async function WaitForAllFiles(
baseFilePath: string,
requiredExtensions: string[],
maxRetries: number = 5,
backoffMs: number = 1000,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
//Get all files in directory if Mac.
let filesInDir: string[] = [];
if (platform.isMacOS) {
const dir: string = path.dirname(baseFilePath);
filesInDir = fs.readdirSync(dir).map((file) => file.toLowerCase());
}
const missingFiles = requiredExtensions.filter((ext) => {
const filePath: string = `${baseFilePath}.${ext}`;
const filePathA: string = `${baseFilePath}A.${ext}`;
const filePathB: string = `${baseFilePath}B.${ext}`;
const filePathV: string = `${baseFilePath}V.${ext}`;
if (!platform.isWindows) {
// Case-insensitive check for macOS/Linux
const baseName: string = path.basename(baseFilePath);
return !(
filesInDir.includes(`${baseName}.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}A.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}B.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}V.${ext}`.toLowerCase())
);
} else {
// Case-sensitive check for other platforms
return !(
fs.existsSync(filePath) ||
fs.existsSync(filePathA) ||
fs.existsSync(filePathB) ||
fs.existsSync(filePathV)
);
}
});
if (missingFiles.length === 0) {
return; // All files are present
}
log.debug(
`Attempt ${attempt}: Missing files: ${missingFiles.join(", ")}. Retrying in ${backoffMs}ms...`,
);
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, backoffMs));
backoffMs *= 2; // Exponential backoff
} else {
throw new Error(
`The set of files is not valid. Missing files for CIECA ID ${baseFilePath}: ${missingFiles.join(", ")}`,
);
}
}
}
const requiredExtensions = [
"env",
"ad1",
"ad2",
"veh",
"lin",
"pfh",
"pfl",
"pft",
"pfm",
"pfo",
"stl",
"ttl",
"pfp",
];
export function ReplaceOwnerInfoWithClaimant<
T extends Partial<
Pick<
RawJobDataObject,
| "ownr_ln"
| "ownr_fn"
| "ownr_co_nm"
| "ownr_title"
| "ownr_co_nm"
| "ownr_addr1"
| "ownr_addr2"
| "ownr_city"
| "ownr_st"
| "ownr_zip"
| "ownr_ctry"
| "ownr_ph1"
| "ownr_ph2"
| "ownr_ea"
| "clmt_ln"
| "clmt_fn"
| "clmt_title"
| "clmt_co_nm"
| "clmt_addr1"
| "clmt_addr2"
| "clmt_city"
| "clmt_st"
| "clmt_zip"
| "clmt_ctry"
| "clmt_ph1"
| "clmt_ph2"
| "clmt_ea"
| "insd_ln"
| "insd_fn"
| "insd_title"
| "insd_co_nm"
| "insd_addr1"
| "insd_addr2"
| "insd_city"
| "insd_st"
| "insd_zip"
| "insd_ctry"
| "insd_ph1"
| "insd_ph2"
| "insd_ea"
| "owner"
>
>,
>(jobObject: T): T {
// Promote claimant data first if owner identity is entirely missing; otherwise fallback to insured data.
const identityKeys = ["ln", "fn", "co_nm"] as const; // keys used to determine presence
const copyKeys = [
"ln",
"fn",
"title",
"co_nm",
"addr1",
"addr2",
"city",
"st",
"zip",
"ctry",
"ph1",
"ph2",
"ea",
] as const; // full set of fields to copy/delete
const ownerMissing = identityKeys.every((k) =>
_.isEmpty((jobObject as any)[`ownr_${k}`]),
);
const claimantHasSome = identityKeys.some(
(k) => !_.isEmpty((jobObject as any)[`clmt_${k}`]),
);
const claimantMissing = identityKeys.every((k) =>
_.isEmpty((jobObject as any)[`clmt_${k}`]),
);
const { owner } = jobObject as any; // destructure for optional nested updates
// Copy helper inline (no extra function as requested)
const promote = (sourcePrefix: "clmt" | "insd"): void => {
copyKeys.forEach((suffix) => {
(jobObject as any)[`ownr_${suffix}`] = (jobObject as any)[
`${sourcePrefix}_${suffix}`
];
if (owner?.data) {
owner.data[`ownr_${suffix}`] = (jobObject as any)[
`${sourcePrefix}_${suffix}`
];
}
});
};
if (ownerMissing && claimantHasSome) {
promote("clmt");
} else if (ownerMissing && claimantMissing) {
promote("insd");
}
// Delete the claimant info as it's not needed.
copyKeys.forEach((suffix) => delete (jobObject as any)[`clmt_${suffix}`]);
// Delete the insured info as it's not needed.
copyKeys.forEach((suffix) => delete (jobObject as any)[`insd_${suffix}`]);
return jobObject;
}

View File

@@ -0,0 +1,104 @@
import axios from "axios";
import archiver from "archiver";
import errorTypeCheck from "../../util/errorTypeCheck";
import { UUID } from "crypto";
import fs from "fs";
import path from "path";
import stream from "stream";
import { getTokenFromRenderer } from "../graphql/graphql-client";
import store from "../store/store";
async function UploadEmsToS3({
extensionlessFilePath,
bodyshopid,
clm_no,
ciecaid,
ownr_ln,
}: {
extensionlessFilePath: string;
bodyshopid: UUID;
clm_no: string;
ciecaid: string;
ownr_ln: string;
}): Promise<boolean> {
// This function is a placeholder for the actual upload logic
try {
const directory = path.dirname(extensionlessFilePath);
const baseFilename = path.basename(extensionlessFilePath);
// Find all files in the directory that start with the base filename
const filesToZip = fs
.readdirSync(directory)
.filter((file) => file.startsWith(baseFilename))
.map((file) => path.join(directory, file));
if (filesToZip.length === 0) {
console.error("No files found to zip.");
return false;
}
// Create a zip archive in memory
const archive = archiver("zip", { zlib: { level: 9 } });
const zipBuffer = await new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
const writableStream = new stream.Writable({
write(chunk, _encoding, callback) {
buffers.push(chunk);
callback();
},
});
writableStream.on("finish", () => resolve(Buffer.concat(buffers)));
writableStream.on("error", reject);
archive.pipe(writableStream);
// Append files to the archive
filesToZip.forEach((file) => {
archive.file(file, { name: path.basename(file) });
});
archive.finalize();
});
// Get the presigned URL from the server
const presignedUrlResponse = await axios.post(
`${
store.get("app.isTest")
? import.meta.env.VITE_API_TEST_URL
: import.meta.env.VITE_API_URL
}/emsupload`,
{
bodyshopid,
ciecaid,
clm_no,
ownr_ln,
},
{
headers: {
Authorization: `Bearer ${await getTokenFromRenderer()}`,
},
},
);
const presignedUrl = presignedUrlResponse.data?.presignedUrl;
if (!presignedUrl) {
console.error("Failed to retrieve presigned URL.");
return false;
}
// Upload the zip file to S3 using the presigned URL
await axios.put(presignedUrl, zipBuffer, {
headers: {
"Content-Type": "application/zip",
},
});
} catch (error) {
console.error("Error uploading EMS to S3:", errorTypeCheck(error));
return false;
}
return true; // Return true if the upload is successful
}
export default UploadEmsToS3;

View File

@@ -0,0 +1,57 @@
import path from "path";
import { GetAllEnvFiles } from "../watcher/watcher";
import DecodeAD1 from "./decode-ad1";
import DecodeAD2 from "./decode-ad2";
import DecodeEnv from "./decode-env";
import DecodeVeh from "./decode-veh";
import { ReplaceOwnerInfoWithClaimant } from "./decoder";
const folderScan = async (): Promise<FolderScanResult[]> => {
//Get all ENV files for watched paths.
const allEnvFiles = GetAllEnvFiles();
//Run a simplified decode on them
const returnedFiles: FolderScanResult[] = [];
for (const filepath of allEnvFiles) {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name,
);
const rawJob = {
...(await DecodeEnv(extensionlessFilePath)),
...(await DecodeAD1(extensionlessFilePath)),
...(await DecodeAD2(extensionlessFilePath)),
...(await DecodeVeh(extensionlessFilePath)),
};
const job = ReplaceOwnerInfoWithClaimant(rawJob);
const scanResult: FolderScanResult = {
id: job.ciecaid,
filepath: filepath,
cieca_id: job.ciecaid,
clm_no: job.clm_no,
owner: `${job.ownr_fn} ${job.ownr_ln} ${job.ownr_co_nm}`.trim(),
vehicle:
`${job.vehicle?.data.v_model_yr} ${job.vehicle?.data.v_make_desc} ${job.vehicle?.data.v_model_desc}`.trim(),
ins_co_nm: job.ins_co_nm,
};
returnedFiles.push(scanResult);
}
//Build up the object and return it
return returnedFiles;
};
export interface FolderScanResult {
id?: string;
filepath: string;
cieca_id?: string;
clm_no?: string;
owner: string;
ins_co_nm?: string;
vehicle: string;
}
export default folderScan;