Further ESDP clean up.

This commit is contained in:
Patrick Fic
2025-12-11 10:14:44 -08:00
parent 26cd7762f3
commit d0d487abe8
9 changed files with 254 additions and 385 deletions

File diff suppressed because one or more lines are too long

View File

@@ -35,15 +35,15 @@ console.log(`ARTIFACT_SUFFIX set to: '${artifactSuffix}'`);
if (process.argv.length > 2) {
const command = process.argv[2];
const args = process.argv.slice(3);
console.log(`Executing: ${command} ${args.join(' ')}`);
const child = spawn(command, args, {
stdio: 'inherit',
env: { ...process.env, ARTIFACT_SUFFIX: artifactSuffix },
shell: true
});
child.on('close', (code) => {
process.exit(code);
});

View File

@@ -5,7 +5,7 @@ import eslintPluginReactHooks from "eslint-plugin-react-hooks";
import eslintPluginReactRefresh from "eslint-plugin-react-refresh";
export default tseslint.config(
{ignores: ["**/node_modules", "**/dist", "**/out"]},
{ ignores: ["**/node_modules", "**/dist", "**/out"] },
tseslint.configs.recommended,
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat["jsx-runtime"],
@@ -31,7 +31,7 @@ export default tseslint.config(
{
files: ["**/*.{js,mjs,ts,tsx,jsx,tsx}"],
rules: {
"prettier/prettier": ["error", {"endOfLine": "off"}]
"prettier/prettier": ["error", { "endOfLine": "auto" }]
}
},
eslintConfigPrettier,

View File

@@ -6,16 +6,9 @@ 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 { ScrubEstimate } from "../estimate-scrubber/estimate-scrubber";
import store from "../store/store";
import setAppProgressbar from "../util/setAppProgressBar";
import DecodeAD1 from "./decode-ad1";
import { DecodedAd1 } from "./decode-ad1.interface";
import DecodeAD2 from "./decode-ad2";
@@ -42,7 +35,6 @@ 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> {
@@ -142,39 +134,10 @@ async function ImportJob(filepath: string): Promise<void> {
};
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",
@@ -193,7 +156,8 @@ async function ImportJob(filepath: string): Promise<void> {
});
uploadNotification.show();
log.debug("Job inserted", insertRecordResult);
//Scrub the estimate
const scrubResults = ScrubEstimate({ job: jobObject });
UploadEmsToS3({
extensionlessFilePath,
@@ -216,7 +180,8 @@ async function ImportJob(filepath: string): Promise<void> {
export default ImportJob;
export interface RawJobDataObject
extends DecodedEnv,
extends
DecodedEnv,
DecodedAd1,
DecodedAD2,
DecodedVeh,

View File

@@ -0,0 +1,186 @@
import log from "electron-log";
import axios from "axios";
import path from "path";
import { BrowserWindow } from "electron";
import { promises as fsPromises } from "fs";
import { autoUpdater } from "electron-updater";
// Function to write job object to logs subfolder
async function writeJobToLogsFolder(job, fileName): Promise<string> {
try {
// Get the directory where electron-log stores its files
const logFilePath = log.transports.file.getFile().path;
const logsDir = path.dirname(logFilePath);
// Create a subfolder for job objects
const jobLogsDir = path.join(logsDir, "esjson");
// Ensure the directory exists
await fsPromises.mkdir(jobLogsDir, { recursive: true });
// Write the job object as JSON
const jobFilePath = path.join(jobLogsDir, `${fileName}.json`);
await fsPromises.writeFile(
jobFilePath,
JSON.stringify(job, null, 2),
"utf8",
);
log.debug(`Job object written to: ${jobFilePath}`);
console.log(`Job object written to: ${jobFilePath}`);
return jobFilePath;
} catch (error) {
log.error("Error writing job object to logs folder:", error);
throw error;
}
}
async function ScrubEstimate({ job }): Promise<string | undefined> {
//These are hard coded as they are not secure values and checking happens based on other values.
//No secret or private information is exposed.
const basicAuthUser = "Imex2";
const basicAuthpassword = "Patrick";
const currentChannel = autoUpdater.channel;
let estimateScrubberUrl;
switch (currentChannel) {
case "alpha":
estimateScrubberUrl = "https://4284-79287.el-alt.com"; //dev specific URL.
break;
case "beta":
estimateScrubberUrl = "https://4284-79073.el-alt.com"; //Beta specific URL.
break;
default:
estimateScrubberUrl = "https://insurtechtoolkit.com"; //Production route.
break;
}
log.log(`Estimate Scrubber URL: [${currentChannel} |`, estimateScrubberUrl);
const sendingEntityId = "87330f61-412b-4251-baaa-d026565b23c5";
try {
const esApiKey = job?.bodyshop?.es_api_key;
//Perform data manipulation on the job object
if (!job) {
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 = job.bodyshop.zip_post;
job.rf_ph1 = job.bodyshop.phone;
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()}`;
// Write job object to logs subfolder
try {
await writeJobToLogsFolder(job, 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);
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 resultPDFUrl = result?.data?.report_link;
const reportIssueUrl = `https://insurtechtoolkit.com/pcontactUs.aspx?apiKey=${esApiKey}&file=${fileName}.json`;
// log.log("Estimate Scrubber Result:", result.data, resultPDFUrl);
// const b = BrowserWindow.getAllWindows()[0];
// b.webContents.send(ipcTypes.app.toRenderer.scrubResults, {
// jobid: job.id,
// items: result.data?.identified_item,
// pdfUrl: resultPDFUrl,
// reportIssueUrl,
// });
// const pdfWindow = new BrowserWindow({
// webPreferences: {
// plugins: true, // Enable PDF viewing
// },
// });
// pdfWindow.loadURL(resultPDFUrl);
// pdfWindow.focus();
return resultPDFUrl;
} catch (error) {
log.error("Error while scrubbing estimate:", error, error.stack);
log.error("Error Response Data:", error.response?.data);
const mainWindow = BrowserWindow.getAllWindows()[0];
if (error.status === 400) {
mainWindow.webContents.send(ipcTypes.app.toRenderer.scrubError, {
message:
error.response?.data ||
"Error encountered sending estimate to Estimate Scrubber.",
});
} else if (error.status === 401) {
mainWindow.webContents.send(ipcTypes.app.toRenderer.scrubError, {
message:
"Authentication with Estimate Scrubber failed." ||
error.response?.data,
});
}
return "Error: Unable to scrub estimate.";
}
}
export { ScrubEstimate };

View File

@@ -1,67 +1,67 @@
export interface User {
stsTokenManager?: {
accessToken: string;
};
stsTokenManager?: {
accessToken: string;
};
}
export interface BodyShop {
shopname: string;
id: string;
shopname: string;
id: string;
}
export interface GraphQLResponse {
bodyshops_by_pk?: {
imexshopid: string;
shopname: string;
bodyshops_by_pk?: {
imexshopid: string;
shopname: string;
};
jobs?: Array<{
labhrs: any;
larhrs: any;
ro_number: string;
ownr_ln: string;
ownr_fn: string;
plate_no: string;
v_vin: string;
v_model_yr: string;
v_make_desc: string;
v_model_desc: string;
vehicle?: {
v_paint_codes?: {
paint_cd1: string;
};
};
jobs?: Array<{
labhrs: any;
larhrs: any;
ro_number: string;
ownr_ln: string;
ownr_fn: string;
plate_no: string;
v_vin: string;
v_model_yr: string;
v_make_desc: string;
v_model_desc: string;
vehicle?: {
v_paint_codes?: {
paint_cd1: string;
};
larhrs_aggregate?: {
aggregate?: {
sum?: {
mod_lb_hrs: number;
};
larhrs_aggregate?: {
aggregate?: {
sum?: {
mod_lb_hrs: number;
};
};
};
};
ins_co_nm: string;
est_ct_ln: string;
est_ct_fn: string;
job_totals?: {
rates?: {
mapa?: {
total?: {
amount: number;
};
};
ins_co_nm: string;
est_ct_ln: string;
est_ct_fn: string;
job_totals?: {
rates?: {
mapa?: {
total?: {
amount: number;
};
};
};
totals?: {
subtotal?: {
amount: number;
};
};
};
totals?: {
subtotal?: {
amount: number;
};
rate_mapa: number;
labhrs_aggregate?: {
aggregate?: {
sum?: {
mod_lb_hrs: number;
};
};
};
};
rate_mapa: number;
labhrs_aggregate?: {
aggregate?: {
sum?: {
mod_lb_hrs: number;
};
rate_lab: number;
}>;
}
};
};
rate_lab: number;
}>;
}

View File

@@ -27,16 +27,6 @@ const setSetting = async (
return Store.get(`settings.${key}`);
};
// Initialize paint scale input configs in store if not set
if (!Store.get("settings.paintScaleInputConfigs")) {
Store.set("settings.paintScaleInputConfigs", []);
}
// Initialize paint scale output configs in store if not set
if (!Store.get("settings.paintScaleOutputConfigs")) {
Store.set("settings.paintScaleOutputConfigs", []);
}
const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
const mainWindow = getMainWindow();
if (!mainWindow) {

View File

@@ -25,7 +25,7 @@ const ipcMainHandleAuthStateChanged = async (
checkForAppUpdatesContinuously();
};
async function setReleaseChannel() {
async function setReleaseChannel(): Promise<void> {
try {
//Need to query the currently active shop, and store the metadata as well.
//Also need to query the OP Codes for decoding reference.

View File

@@ -1,272 +0,0 @@
import log from "electron-log/main";
import path from "path";
import fs from "fs/promises";
import axios from "axios";
import { create } from "xmlbuilder2";
import { parseStringPromise } from "xml2js";
import store from "../../store/store";
import client, { getTokenFromRenderer } from "../../graphql/graphql-client";
import { PaintScaleConfig } from "../../../util/types/paintScale";
import dayjs from "dayjs";
import {
PPG_DATA_QUERY_TYPED,
PpgDataQueryResult,
PpgDataQueryVariables,
} from "../../graphql/queries";
export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
try {
log.info(
`Polling input directory for PPG config ${config.id}: ${config.path}`,
);
log.debug(
`Archive dir: ${path.join(config.path!, "archive")}, Error dir: ${path.join(config.path!, "error")}`,
);
// Ensure archive and error directories exist
const archiveDir = path.join(config.path!, "archive");
const errorDir = path.join(config.path!, "error");
try {
await fs.mkdir(archiveDir, { recursive: true });
await fs.mkdir(errorDir, { recursive: true });
log.debug(
`Archive and error directories ensured: ${archiveDir}, ${errorDir}`,
);
} catch (dirError) {
log.error(`Failed to create directories for ${config.path}:`, dirError);
throw dirError;
}
// Check for files
const files = await fs.readdir(config.path!);
log.debug(`Found ${files.length} files in ${config.path}:`, files);
for (const file of files) {
// Only process XML files
if (!file.toLowerCase().endsWith(".xml")) {
continue;
}
const filePath = path.join(config.path!, file);
try {
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
continue;
}
} catch (statError) {
log.warn(`Failed to stat file ${filePath}:`, statError);
continue;
}
log.debug(`Processing input file: ${filePath}`);
// Check file accessibility (e.g., not locked)
try {
await fs.access(filePath, fs.constants.R_OK);
} catch (error) {
log.warn(`File ${filePath} is inaccessible, skipping:`, error);
continue;
}
// Validate XML structure
let xmlContent: BlobPart;
try {
xmlContent = await fs.readFile(filePath, "utf8");
await parseStringPromise(xmlContent);
log.debug(`Successfully validated XML for ${filePath}`);
} catch (error) {
log.error(`Invalid XML in ${filePath}:`, error);
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
const originalFilename = path.basename(file, path.extname(file));
const errorPath = path.join(
errorDir,
`${originalFilename}-${timestamp}.xml`,
);
try {
await fs.rename(filePath, errorPath);
log.debug(`Moved invalid file to error: ${errorPath}`);
} catch (moveError) {
log.error(
`Failed to move invalid file to error directory ${errorPath}:`,
moveError,
);
}
continue;
}
// Get authentication token
let token: string | null;
try {
token = await getTokenFromRenderer();
if (!token) {
log.error(`No authentication token for file: ${filePath}`);
continue;
}
log.debug(
`Obtained authentication token for ${filePath}: ${token.slice(0, 10)}...`,
);
} catch (tokenError) {
log.error(
`Failed to obtain authentication token for ${filePath}:`,
tokenError,
);
continue;
}
// Upload file to API
const formData = new FormData();
formData.append("file", new Blob([xmlContent]), path.basename(filePath));
const shopId = (store.get("app.bodyshop") as any)?.shopname || "";
formData.append("shopId", shopId);
log.debug(`Shop ID: ${shopId}`);
const baseURL = store.get("app.isTest")
? import.meta.env.VITE_API_TEST_URL
: import.meta.env.VITE_API_URL;
const finalUrl = `${baseURL}/mixdata/upload`;
log.debug(`Uploading file to ${finalUrl}`);
try {
const response = await axios.post(finalUrl, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
timeout: 10000, // 10-second timeout
});
log.info(`Upload response for ${filePath}:`, {
status: response.status,
statusText: response.statusText,
data: response.data,
});
if (response.status === 200) {
log.info(`Successful upload of ${filePath}`);
// Move file to archive
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
const originalFilename = path.basename(file, path.extname(file));
const archivePath = path.join(
archiveDir,
`${originalFilename}-${timestamp}.xml`,
);
try {
await fs.access(archiveDir, fs.constants.W_OK); // Verify archiveDir is writable
await fs.rename(filePath, archivePath);
log.info(`Moved file to archive: ${archivePath}`);
} catch (moveError) {
log.error(
`Failed to move file to archive directory ${archivePath}:`,
moveError,
);
}
} else {
log.error(
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
{ responseData: response.data },
);
}
} catch (error: any) {
log.error(`Error uploading ${filePath}:`, {
message: error.message,
code: error.code,
response: error.response
? {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
}
: null,
});
}
}
} catch (error) {
log.error(`Error polling input directory ${config.path}:`, error);
}
}
// PPG Output Handler
export async function ppgOutputHandler(
config: PaintScaleConfig,
): Promise<void> {
try {
log.info(`Generating PPG output for config ${config.id}: ${config.path}`);
await fs.mkdir(config.path!, { recursive: true });
const variables: PpgDataQueryVariables = {
today: dayjs().toISOString(),
todayplus5: dayjs().add(5, "day").toISOString(),
shopid: (store.get("app.bodyshop") as any)?.id,
};
const response = await client.request<
PpgDataQueryResult,
PpgDataQueryVariables
>(PPG_DATA_QUERY_TYPED, variables);
const jobs = response.jobs ?? [];
const header = {
PPG: {
Header: {
Protocol: {
Message: "PaintShopInterface",
Name: "PPG",
Version: "1.5.0",
},
Transaction: {
TransactionID: "",
TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
},
Product: {
Name: import.meta.env.VITE_COMPANY === "IMEX",
Version: "",
},
},
DataInterface: {
ROData: {
ShopInfo: {
ShopID: response.bodyshops_by_pk?.imexshopid || "",
ShopName: response.bodyshops_by_pk?.shopname || "",
},
RepairOrders: {
ROCount: jobs.length.toString(),
RO: jobs.map((job) => ({
RONumber: job.ro_number || "",
ROStatus: "Open",
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
ROPainterNotes: "",
LicensePlateNum: job.plate_no || "",
VIN: job.v_vin || "",
ModelYear: job.v_model_yr || "",
MakeDesc: job.v_make_desc || "",
ModelName: job.v_model_desc || "",
OEMColorCode: job.vehicle?.v_paint_codes?.paint_cd1 || "",
RefinishLaborHours: job.larhrs?.aggregate?.sum?.mod_lb_hrs || 0,
InsuranceCompanyName: job.ins_co_nm || "",
EstimatorName: `${job.est_ct_ln || ""}, ${job.est_ct_fn || ""}`,
PaintMaterialsRevenue: (
(job.job_totals?.rates?.mapa?.total?.amount || 0) / 100
).toFixed(2),
PaintMaterialsRate: job.rate_mapa || 0,
BodyHours: job.labhrs?.aggregate?.sum?.mod_lb_hrs || 0,
BodyLaborRate: job.rate_lab || 0,
TotalCostOfRepairs: (
(job.job_totals?.totals?.subtotal?.amount || 0) / 100
).toFixed(2),
})),
},
},
},
},
};
const xml = create({ version: "1.0" }, header).end({ prettyPrint: true });
const outputPath = path.join(config.path!, `PPGPaint.xml`);
await fs.writeFile(outputPath, xml);
log.info(`Saved PPG output XML to ${outputPath}`);
} catch (error) {
log.error(`Error generating PPG output for config ${config.id}:`, error);
}
}