diff --git a/src/main/decoder/decode-env.interface.ts b/src/main/decoder/decode-env.interface.ts new file mode 100644 index 0000000..f120de1 --- /dev/null +++ b/src/main/decoder/decode-env.interface.ts @@ -0,0 +1,4 @@ +export interface DecodedEnv { + est_system: string; + estfile_id: string; +} diff --git a/src/main/decoder/decode-env.ts b/src/main/decoder/decode-env.ts new file mode 100644 index 0000000..3f4b305 --- /dev/null +++ b/src/main/decoder/decode-env.ts @@ -0,0 +1,42 @@ +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"; + +const DecodeEnv = async ( + extensionlessFilePath: string +): Promise => { + let dbf: DBFFile | null = null; + 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}`); + } + + 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", + ]) + ); + + //Apply business logic transfomrations. + + return rawEnvData; +}; +export default DecodeEnv; diff --git a/src/main/decoder/decode-pfm.ts b/src/main/decoder/decode-pfm.ts index 197cd13..35461ef 100644 --- a/src/main/decoder/decode-pfm.ts +++ b/src/main/decoder/decode-pfm.ts @@ -8,6 +8,7 @@ import { DecodedPfmLine, JobMaterialRateFields, } from "./decode-pfm.interface"; +import YNBoolConverter from "../../util/ynBoolConverter"; const DecodePfm = async ( extensionlessFilePath: string @@ -45,40 +46,42 @@ const DecodePfm = async ( }; const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => { - const singleLineData: DecodedPfmLine = deepLowerCaseKeys( - _.pick(record, [ - //TODO: Add typings for EMS File Formats. - "MATL_TYPE", - "CAL_CODE", - "CAL_DESC", - "CAL_MAXDLR", - "CAL_PRIP", + 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_SECP", + "MAT_CALP", + "CAL_PRETHR", //Mitchell here + "CAL_PSTTHR", + "CAL_THRAMT", - "CAL_LBRMIN", + "CAL_LBRMIN", - "CAL_LBRRTE", //Audatex puts it here - "CAL_OPCODE", + "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", - ]) + "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. @@ -89,15 +92,44 @@ const DecodePfm = async ( }); //Apply line by line adjustments. - const materialsLine: DecodedPfmLine | undefined = rawPfmData.find( + const mapaLine: DecodedPfmLine | undefined = rawPfmData.find( (line) => line.matl_type === "MAPA" ); - - if (materialsLine) { + if (mapaLine) { jobMaterialRates.rate_mapa = - materialsLine.cal_lbrrte || materialsLine.cal_prethr || 0; + 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` diff --git a/src/main/decoder/decode-pfo.ts b/src/main/decoder/decode-pfo.ts index 56f57f6..6637c2a 100644 --- a/src/main/decoder/decode-pfo.ts +++ b/src/main/decoder/decode-pfo.ts @@ -4,6 +4,7 @@ import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; import { DecodedPfo } from "./decode-pfo.interface"; +import YNBoolConverter from "../../util/ynBoolConverter"; const DecodePfo = async ( extensionlessFilePath: string @@ -27,36 +28,38 @@ const DecodePfo = async ( //PFO will always have only 1 row. //Commented lines have been cross referenced with existing partner fields. - const rawPfoData: DecodedPfo = 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", - ]) + const rawPfoData: DecodedPfo = 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. diff --git a/src/main/decoder/decode-pfp.interface.ts b/src/main/decoder/decode-pfp.interface.ts new file mode 100644 index 0000000..22cc866 --- /dev/null +++ b/src/main/decoder/decode-pfp.interface.ts @@ -0,0 +1,33 @@ +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 DecodedPfp { + PAA: DecodedPfpLine; + PAC: DecodedPfpLine; + PAL: DecodedPfpLine; + PAG: DecodedPfpLine; + PAM: DecodedPfpLine; + PAP: DecodedPfpLine; + PAN: DecodedPfpLine; + PAO: DecodedPfpLine; + PAR: DecodedPfpLine; + PAS: DecodedPfpLine; + PASL: DecodedPfpLine; + PAT: DecodedPfpLine; +} diff --git a/src/main/decoder/decode-pfp.ts b/src/main/decoder/decode-pfp.ts new file mode 100644 index 0000000..6ff89de --- /dev/null +++ b/src/main/decoder/decode-pfp.ts @@ -0,0 +1,71 @@ +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 } from "./decode-pfp.interface"; + +const DecodePfp = async ( + extensionlessFilePath: string +): Promise => { + let dbf: DBFFile | null = null; + 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}`); + } + + 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: DecodedPfp = rawPfpData.reduce( + (acc: DecodedPfp, line: DecodedPfpLine) => { + acc[line.prt_type] = line; + return acc; + }, + {} as DecodedPfp + ); + + return parsedPfpFile; +}; + +export default DecodePfp; diff --git a/src/main/decoder/decoder.ts b/src/main/decoder/decoder.ts index b9e8f45..d1a73bd 100644 --- a/src/main/decoder/decoder.ts +++ b/src/main/decoder/decoder.ts @@ -15,6 +15,8 @@ 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"; @@ -23,6 +25,8 @@ import DecodeTtl from "./decode-ttl"; import { DecodedTtl } from "./decode-ttl.interface"; import DecodeVeh from "./decode-veh"; import { DecodedVeh } from "./decode-veh.interface"; +import { DecodedEnv } from "./decode-env.interface"; +import DecodeEnv from "./decode-env"; async function ImportJob(filepath: string): Promise { const parsedFilePath = path.parse(filepath); @@ -35,6 +39,7 @@ async function ImportJob(filepath: string): Promise { try { //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. + const env: DecodedEnv = await DecodeEnv(extensionlessFilePath); const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath); const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath); const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath); @@ -46,19 +51,22 @@ async function ImportJob(filepath: string): Promise { const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object const stl: DecodedStl[] = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath); + const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath); - log.debug("EMS Object", { - ad1, - ad2, - veh, - lin, - pfh, - pfl, - pft, - pfm, - pfo, - stl, - ttl, + log.debug("Job Object", { + ...env, + ...ad1, + ...ad2, + ...veh, + joblines: { data: lin }, + ...pfh, + cieca_pfl: pfl, + cieca_pft: pft, + materials: pfm, + cieca_pfo: pfo, + ...stl, + ...ttl, + parts_tax_rates: pfp, }); } catch (error) { log.error("Error encountered while decoding job. ", errorTypeCheck(error)); diff --git a/src/util/ynBoolConverter.ts b/src/util/ynBoolConverter.ts new file mode 100644 index 0000000..2251f6d --- /dev/null +++ b/src/util/ynBoolConverter.ts @@ -0,0 +1,12 @@ +const YNBoolConverter = (original: T): T => { + Object.keys(original).forEach((key) => { + if (original[key] === "Y") { + original[key] = true; + } else if (original[key] === "N") { + original[key] = false; + } + }); + return original; +}; + +export default YNBoolConverter;