feature/IO-3255-simplified-parts-management - Checkpoint
This commit is contained in:
@@ -21,7 +21,7 @@ services:
|
|||||||
- redis-node-1-data:/data
|
- redis-node-1-data:/data
|
||||||
- redis-lock:/redis-lock
|
- redis-lock:/redis-lock
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -39,7 +39,7 @@ services:
|
|||||||
- redis-node-2-data:/data
|
- redis-node-2-data:/data
|
||||||
- redis-lock:/redis-lock
|
- redis-lock:/redis-lock
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -57,7 +57,7 @@ services:
|
|||||||
- redis-node-3-data:/data
|
- redis-node-3-data:/data
|
||||||
- redis-lock:/redis-lock
|
- redis-lock:/redis-lock
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "4566:4566"
|
- "4566:4566"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
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 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-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 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: The Main IMEX API
|
||||||
node-app:
|
node-app:
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const client = require("../../../graphql-client/graphql-client").client;
|
|||||||
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||||
const opCodes = require("./lib/opCodes.json");
|
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
|
// GraphQL Queries and Mutations
|
||||||
const {
|
const {
|
||||||
@@ -10,10 +13,28 @@ const {
|
|||||||
INSERT_OWNER,
|
INSERT_OWNER,
|
||||||
INSERT_JOB_WITH_LINES
|
INSERT_JOB_WITH_LINES
|
||||||
} = require("../partsManagement.queries");
|
} = require("../partsManagement.queries");
|
||||||
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
const FALLBACK_DEFAULT_JOB_STATUS = "Open";
|
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.
|
* Fetches the default order status for a bodyshop.
|
||||||
* @param {string} shopId - The bodyshop UUID.
|
* @param {string} shopId - The bodyshop UUID.
|
||||||
@@ -514,17 +535,10 @@ const insertOwner = async (ownerInput, logger) => {
|
|||||||
*/
|
*/
|
||||||
const vehicleDamageEstimateAddRq = async (req, res) => {
|
const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
|
const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
|
||||||
try {
|
try {
|
||||||
// Parse XML
|
|
||||||
const payload = await parseXml(req.body, logger);
|
const payload = await parseXml(req.body, logger);
|
||||||
const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq);
|
const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq);
|
||||||
if (!rq) {
|
|
||||||
logger.log("parts-missing-root", "error");
|
|
||||||
return res.status(400).send("Missing <VehicleDamageEstimateAddRq>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract job data
|
|
||||||
const {
|
const {
|
||||||
shopId,
|
shopId,
|
||||||
refClaimNum,
|
refClaimNum,
|
||||||
@@ -544,36 +558,17 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
policy_no,
|
policy_no,
|
||||||
ded_amt,
|
ded_amt,
|
||||||
driveable
|
driveable
|
||||||
// status,
|
|
||||||
} = extractJobData(rq);
|
} = extractJobData(rq);
|
||||||
|
|
||||||
if (!shopId) {
|
|
||||||
throw { status: 400, message: "Missing <ShopID> in XML" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get default status
|
|
||||||
const defaultStatus = await getDefaultJobStatus(shopId, logger);
|
const defaultStatus = await getDefaultJobStatus(shopId, logger);
|
||||||
|
|
||||||
// Extract additional data
|
|
||||||
const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
||||||
const ownerData = extractOwnerData(rq, shopId);
|
const ownerData = extractOwnerData(rq, shopId);
|
||||||
const estimatorData = extractEstimatorData(rq);
|
const estimatorData = extractEstimatorData(rq);
|
||||||
// const adjusterData = extractAdjusterData(rq);
|
|
||||||
// const repairFacilityData = extractRepairFacilityData(rq);
|
|
||||||
const vehicleData = extractVehicleData(rq, shopId);
|
const vehicleData = extractVehicleData(rq, shopId);
|
||||||
const lossInfo = extractLossInfo(rq);
|
const lossInfo = extractLossInfo(rq);
|
||||||
const joblinesData = extractJobLines(rq);
|
const joblinesData = extractJobLines(rq);
|
||||||
const insuranceData = extractInsuranceData(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 ownerid = await insertOwner(ownerData, logger);
|
||||||
const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger);
|
const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger);
|
||||||
|
|
||||||
// Build job input
|
|
||||||
const jobInput = {
|
const jobInput = {
|
||||||
shopid: shopId,
|
shopid: shopId,
|
||||||
driveable,
|
driveable,
|
||||||
@@ -588,7 +583,7 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
parts_tax_rates,
|
parts_tax_rates,
|
||||||
clm_no,
|
clm_no,
|
||||||
status: defaultStatus,
|
status: defaultStatus,
|
||||||
clm_total: 0, // computedTotal || null,
|
clm_total: 0,
|
||||||
policy_no,
|
policy_no,
|
||||||
ded_amt,
|
ded_amt,
|
||||||
comment,
|
comment,
|
||||||
@@ -598,14 +593,10 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
asgn_date,
|
asgn_date,
|
||||||
scheduled_in,
|
scheduled_in,
|
||||||
scheduled_completion,
|
scheduled_completion,
|
||||||
// Inline insurance/loss/contacts
|
|
||||||
...insuranceData,
|
...insuranceData,
|
||||||
...lossInfo,
|
...lossInfo,
|
||||||
...ownerData,
|
...ownerData,
|
||||||
...estimatorData,
|
...estimatorData,
|
||||||
// ...adjusterData,
|
|
||||||
// ...repairFacilityData,
|
|
||||||
// Inline vehicle data
|
|
||||||
v_vin: vehicleData.v_vin,
|
v_vin: vehicleData.v_vin,
|
||||||
v_model_yr: vehicleData.v_model_yr,
|
v_model_yr: vehicleData.v_model_yr,
|
||||||
v_model_desc: vehicleData.v_model_desc,
|
v_model_desc: vehicleData.v_model_desc,
|
||||||
@@ -616,10 +607,23 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }),
|
...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }),
|
||||||
joblines: { data: joblinesData }
|
joblines: { data: joblinesData }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert job
|
|
||||||
const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput });
|
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 });
|
return res.status(200).json({ success: true, jobId: newJob.id });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("parts-route-error", "error", null, null, { error: err });
|
logger.log("parts-route-error", "error", null, null, { error: err });
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const client = require("../../../graphql-client/graphql-client").client;
|
|||||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||||
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||||
const opCodes = require("./lib/opCodes.json");
|
const opCodes = require("./lib/opCodes.json");
|
||||||
|
const { uploadFileToS3 } = require("../../../utils/s3");
|
||||||
|
const InstanceMgr = require("../../../utils/instanceMgr").default;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
GET_JOB_BY_ID,
|
GET_JOB_BY_ID,
|
||||||
@@ -214,6 +216,22 @@ const extractDeletions = (deletions = {}) => {
|
|||||||
return Array.from(new Set(allSeqs));
|
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.
|
* Handles VehicleDamageEstimateChgRq requests.
|
||||||
* @param req
|
* @param req
|
||||||
@@ -222,19 +240,26 @@ const extractDeletions = (deletions = {}) => {
|
|||||||
*/
|
*/
|
||||||
const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
|
const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
|
||||||
try {
|
try {
|
||||||
const payload = await parseXml(req.body, logger);
|
const payload = await parseXml(req.body, logger);
|
||||||
const rq = normalizeXmlObject(payload.VehicleDamageEstimateChgRq);
|
const rq = normalizeXmlObject(payload.VehicleDamageEstimateChgRq);
|
||||||
if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>");
|
|
||||||
|
|
||||||
const shopId = rq.ShopID;
|
|
||||||
const jobId = rq.JobID;
|
const jobId = rq.JobID;
|
||||||
|
const shopId = rq.ShopID;
|
||||||
if (!shopId || !jobId) return res.status(400).send("Missing ShopID or JobID");
|
|
||||||
|
// 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);
|
const job = await findJob(shopId, jobId, logger);
|
||||||
|
|
||||||
if (!job) return res.status(404).send("Job not found");
|
if (!job) return res.status(404).send("Job not found");
|
||||||
|
|
||||||
// --- Get updated lines and their unq_seq ---
|
// --- Get updated lines and their unq_seq ---
|
||||||
@@ -290,3 +315,5 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = partsManagementVehicleDamageEstimateChgRq;
|
module.exports = partsManagementVehicleDamageEstimateChgRq;
|
||||||
|
|
||||||
|
// Remove any duplicate S3 constants that might have been appended previously (none expected now)
|
||||||
|
|||||||
Reference in New Issue
Block a user