diff --git a/package-lock.json b/package-lock.json index b920d09..519fa83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "electron-updater": "^6.6.2", "node-cron": "^3.0.3", "winax": "^3.6.2", + "xml2js": "^0.6.2", "xmlbuilder2": "^3.1.1" }, "devDependencies": { @@ -37,6 +38,7 @@ "@types/node-cron": "^3.0.11", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.2", + "@types/xml2js": "^0.4.14", "@vitejs/plugin-react": "^4.3.4", "antd": "^5.24.6", "chokidar": "^4.0.3", @@ -4374,13 +4376,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -4646,6 +4641,16 @@ "license": "MIT", "optional": true }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -12948,13 +12953,12 @@ } }, "node_modules/react-router": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz", - "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", + "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", "dev": true, "license": "MIT", "dependencies": { - "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" @@ -15255,6 +15259,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 3f6fe04..004c373 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "electron-updater": "^6.6.2", "node-cron": "^3.0.3", "winax": "^3.6.2", + "xml2js": "^0.6.2", "xmlbuilder2": "^3.1.1" }, "devDependencies": { @@ -52,6 +53,7 @@ "@types/node-cron": "^3.0.11", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.2", + "@types/xml2js": "^0.4.14", "@vitejs/plugin-react": "^4.3.4", "antd": "^5.24.6", "chokidar": "^4.0.3", diff --git a/src/main/ipc/paintScaleHandlers/PPG.ts b/src/main/ipc/paintScaleHandlers/PPG.ts index 32dad2a..c95ef44 100644 --- a/src/main/ipc/paintScaleHandlers/PPG.ts +++ b/src/main/ipc/paintScaleHandlers/PPG.ts @@ -3,47 +3,85 @@ 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 from "../../graphql/graphql-client"; -import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale"; +import { PaintScaleConfig } from "../../../util/types/paintScale"; // PPG Input Handler export async function ppgInputHandler(config: PaintScaleConfig): Promise { try { - log.info(`Polling input directory for PPG config ${config.id}: ${config.path}`); + log.info( + `Polling input directory for PPG config ${config.id}: ${config.path}`, + ); - // Ensure archive directory exists + // Ensure archive and error directories exist const archiveDir = path.join(config.path!, "archive"); + const errorDir = path.join(config.path!, "error"); await fs.mkdir(archiveDir, { recursive: true }); + await fs.mkdir(errorDir, { recursive: true }); // Check for files const files = await fs.readdir(config.path!); for (const file of files) { - if (!file.toLowerCase().endsWith(".xml")) continue; + // Only process XML files + if (!file.toLowerCase().endsWith(".xml")) { + continue; + } const filePath = path.join(config.path!, file); const stats = await fs.stat(filePath); - if (stats.isFile()) { - log.debug(`Processing input file: ${filePath}`); + if (!stats.isFile()) { + continue; + } - // Get authentication token - const token = (store.get("user") as any)?.stsTokenManager?.accessToken; - if (!token) { - log.error(`No authentication token for file: ${filePath}`); - continue; - } + log.debug(`Processing input file: ${filePath}`); - const formData = new FormData(); - formData.append("file", new Blob([await fs.readFile(filePath)]), path.basename(filePath)); - formData.append("shopId", (store.get("app.bodyshop") as any)?.shopname || ""); + // 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; + } - const baseURL = store.get("app.isTest") - ? import.meta.env.VITE_API_TEST_URL - : import.meta.env.VITE_API_URL; - const finalUrl = `${baseURL}/mixdata/upload`; + // Validate XML structure + let xmlContent : BlobPart; - log.debug(`Uploading file to ${finalUrl}`); + try { + xmlContent = await fs.readFile(filePath, "utf8"); + await parseStringPromise(xmlContent); + } catch (error) { + log.error(`Invalid XML in ${filePath}:`, error); + const errorPath = path.join(errorDir, path.basename(filePath)); + await fs.rename(filePath, errorPath); + log.debug(`Moved invalid file to error: ${errorPath}`); + continue; + } + // Get authentication token + const token = (store.get("user") as any)?.stsTokenManager?.accessToken; + if (!token) { + log.error(`No authentication token for file: ${filePath}`); + continue; + } + + // Upload file to API + const formData = new FormData(); + formData.append("file", new Blob([xmlContent]), path.basename(filePath)); + formData.append( + "shopId", + (store.get("app.bodyshop") as any)?.shopname || "", + ); + + 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}`, @@ -58,8 +96,13 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise { await fs.rename(filePath, archivePath); log.debug(`Moved file to archive: ${archivePath}`); } else { - log.error(`Failed to upload ${filePath}: ${response.statusText}`); + log.error( + `Failed to upload ${filePath}: ${response.status} ${response.statusText}`, + response.data, + ); } + } catch (error) { + log.error(`Error uploading ${filePath}:`, error); } } } catch (error) { @@ -68,7 +111,9 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise { } // PPG Output Handler -export async function ppgOutputHandler(config: PaintScaleConfig): Promise { +export async function ppgOutputHandler( + config: PaintScaleConfig, +): Promise { try { log.info(`Generating PPG output for config ${config.id}: ${config.path}`); @@ -150,7 +195,10 @@ export async function ppgOutputHandler(config: PaintScaleConfig): Promise }, Transaction: { TransactionID: "", - TransactionDate: new Date().toISOString().replace("T", " ").substring(0, 19), + TransactionDate: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19), }, Product: { Name: "ImEX Online", @@ -179,11 +227,15 @@ export async function ppgOutputHandler(config: PaintScaleConfig): Promise 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), + 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), + TotalCostOfRepairs: ( + (job.job_totals?.totals?.subtotal?.amount || 0) / 100 + ).toFixed(2), })), }, },