diff --git a/docker-compose.yml b/docker-compose.yml index 0bc96311c..ae8b35048 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - redis-node-1-data:/data - redis-lock:/redis-lock healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 10 @@ -39,7 +39,7 @@ services: - redis-node-2-data:/data - redis-lock:/redis-lock healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 10 @@ -57,7 +57,7 @@ services: - redis-node-3-data:/data - redis-lock:/redis-lock healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 10 @@ -85,7 +85,7 @@ services: ports: - "4566:4566" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"] + test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ] interval: 10s timeout: 5s retries: 5 @@ -118,6 +118,7 @@ services: aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1 " # Node App: The Main IMEX API node-app: diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index e53724b12..78c344387 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -2,6 +2,9 @@ const client = require("../../../graphql-client/graphql-client").client; const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); const opCodes = require("./lib/opCodes.json"); +// New imports for S3 XML archival +const { uploadFileToS3 } = require("../../../utils/s3"); +const InstanceMgr = require("../../../utils/instanceMgr").default; // GraphQL Queries and Mutations const { @@ -10,10 +13,28 @@ const { INSERT_OWNER, INSERT_JOB_WITH_LINES } = require("../partsManagement.queries"); +const { v4: uuidv4 } = require("uuid"); // Defaults const FALLBACK_DEFAULT_JOB_STATUS = "Open"; +const ESTIMATE_XML_BUCKET = + process.env?.NODE_ENV === "development" + ? "parts-estimates" // local/dev shared bucket name + : InstanceMgr({ + imex: `imex-webest-xml`, + rome: `rome-webest-xml` + }); + +const buildEstimateXmlKey = (rq) => { + const refClaimNum = rq.RefClaimNum; + const shopId = rq.ShopID; + + const ts = new Date().toISOString().replace(/:/g, "-"); + const safeClaim = (refClaimNum || "no-claim").toString().replace(/[^A-Za-z0-9_-]/g, "_"); + return `addRequest/${shopId}/${safeClaim}/${ts}-${uuidv4()}.xml`; +}; + /** * Fetches the default order status for a bodyshop. * @param {string} shopId - The bodyshop UUID. @@ -514,17 +535,10 @@ const insertOwner = async (ownerInput, logger) => { */ const vehicleDamageEstimateAddRq = async (req, res) => { const { logger } = req; - + const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : ""; try { - // Parse XML const payload = await parseXml(req.body, logger); const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq); - if (!rq) { - logger.log("parts-missing-root", "error"); - return res.status(400).send("Missing "); - } - - // Extract job data const { shopId, refClaimNum, @@ -544,36 +558,17 @@ const vehicleDamageEstimateAddRq = async (req, res) => { policy_no, ded_amt, driveable - // status, } = extractJobData(rq); - - if (!shopId) { - throw { status: 400, message: "Missing in XML" }; - } - - // Get default status const defaultStatus = await getDefaultJobStatus(shopId, logger); - - // Extract additional data const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo); const ownerData = extractOwnerData(rq, shopId); const estimatorData = extractEstimatorData(rq); - // const adjusterData = extractAdjusterData(rq); - // const repairFacilityData = extractRepairFacilityData(rq); const vehicleData = extractVehicleData(rq, shopId); const lossInfo = extractLossInfo(rq); const joblinesData = extractJobLines(rq); const insuranceData = extractInsuranceData(rq); - - // Derive clm_total: prefer RepairTotalsInfo SummaryTotals GRAND TOTAL; else sum from lines - // const grandTotal = extractGrandTotal(rq); - // const computedTotal = grandTotal ?? computeLinesTotal(joblinesData); - - // Find or create relationships const ownerid = await insertOwner(ownerData, logger); const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger); - - // Build job input const jobInput = { shopid: shopId, driveable, @@ -588,7 +583,7 @@ const vehicleDamageEstimateAddRq = async (req, res) => { parts_tax_rates, clm_no, status: defaultStatus, - clm_total: 0, // computedTotal || null, + clm_total: 0, policy_no, ded_amt, comment, @@ -598,14 +593,10 @@ const vehicleDamageEstimateAddRq = async (req, res) => { asgn_date, scheduled_in, scheduled_completion, - // Inline insurance/loss/contacts ...insuranceData, ...lossInfo, ...ownerData, ...estimatorData, - // ...adjusterData, - // ...repairFacilityData, - // Inline vehicle data v_vin: vehicleData.v_vin, v_model_yr: vehicleData.v_model_yr, v_model_desc: vehicleData.v_model_desc, @@ -616,10 +607,23 @@ const vehicleDamageEstimateAddRq = async (req, res) => { ...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }), joblines: { data: joblinesData } }; - - // Insert job const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput }); + // Upload AFTER job creation to include job id in filename + (async () => { + try { + const key = buildEstimateXmlKey(rq); + await uploadFileToS3({ + bucketName: ESTIMATE_XML_BUCKET, + key, + content: rawXml || "", + contentType: "application/xml" + }); + logger.log("parts-estimate-xml-uploaded", "info", shopId, newJob.id, { key, bytes: rawXml?.length || 0 }); + } catch (e) { + logger.log("parts-estimate-xml-upload-failed", "warn", shopId, null, { error: e?.message }); + } + })(); return res.status(200).json({ success: true, jobId: newJob.id }); } catch (err) { logger.log("parts-route-error", "error", null, null, { error: err }); diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js index c79340387..109e63995 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js @@ -5,6 +5,8 @@ const client = require("../../../graphql-client/graphql-client").client; const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); const opCodes = require("./lib/opCodes.json"); +const { uploadFileToS3 } = require("../../../utils/s3"); +const InstanceMgr = require("../../../utils/instanceMgr").default; const { GET_JOB_BY_ID, @@ -214,6 +216,22 @@ const extractDeletions = (deletions = {}) => { return Array.from(new Set(allSeqs)); }; +// S3 bucket + key builder (mirrors AddRq but with changeRequest prefix) +const ESTIMATE_XML_BUCKET = + process.env?.NODE_ENV === "development" + ? "parts-estimates" + : InstanceMgr({ + imex: `imex-webest-xml`, + rome: `rome-webest-xml` + }); + +const buildEstimateXmlKey = (rq) => { + const shopId = rq.ShopID; + const jobId = rq.JobID; + const ts = new Date().toISOString().replace(/:/g, "-"); + return `changeRequest/${shopId}/${jobId}/${ts}.xml`; +}; + /** * Handles VehicleDamageEstimateChgRq requests. * @param req @@ -222,19 +240,26 @@ const extractDeletions = (deletions = {}) => { */ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => { const { logger } = req; - + const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : ""; try { const payload = await parseXml(req.body, logger); const rq = normalizeXmlObject(payload.VehicleDamageEstimateChgRq); - if (!rq) return res.status(400).send("Missing "); - - const shopId = rq.ShopID; + const jobId = rq.JobID; - - if (!shopId || !jobId) return res.status(400).send("Missing ShopID or JobID"); + const shopId = rq.ShopID; + + // Fire-and-forget archival on valid request + (async () => { + try { + const key = buildEstimateXmlKey(rq); + await uploadFileToS3({ bucketName: ESTIMATE_XML_BUCKET, key, content: rawXml || "", contentType: "application/xml" }); + logger.log("parts-estimate-xml-uploaded", "info", jobId, null, { key, bytes: rawXml?.length || 0 }); + } catch (e) { + logger.log("parts-estimate-xml-upload-failed", "warn", jobId, null, { error: e?.message }); + } + })(); const job = await findJob(shopId, jobId, logger); - if (!job) return res.status(404).send("Job not found"); // --- Get updated lines and their unq_seq --- @@ -290,3 +315,5 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => { }; module.exports = partsManagementVehicleDamageEstimateChgRq; + +// Remove any duplicate S3 constants that might have been appended previously (none expected now)