feature/IO-3255-simplified-parts-management - Checkpoint

This commit is contained in:
Dave
2025-09-05 11:03:20 -04:00
parent 13cb68b0af
commit 838c24b3f1
3 changed files with 77 additions and 45 deletions

View File

@@ -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:

View File

@@ -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 <VehicleDamageEstimateAddRq>");
}
// 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 <ShopID> 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 });

View File

@@ -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 <VehicleDamageEstimateChgRq>");
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)