Files
bodyshop/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js

357 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const xml2js = require("xml2js");
const client = require("../../graphql-client/graphql-client").client;
// GraphQL statements
const INSERT_JOB_WITH_LINES = `
mutation InsertJob($job: jobs_insert_input!) {
insert_jobs_one(object: $job) {
id
joblines { id unq_seq }
}
}
`;
const INSERT_PARTS_ORDERS = `
mutation InsertPartsOrders($po: [parts_orders_insert_input!]!) {
insert_parts_orders(objects: $po) {
returning { id order_number }
}
}
`;
/**
* Handles incoming VehicleDamageEstimateAddRq XML,
* parses every known field, inserts a Job + nested JobLines,
* then any PartsOrders (grouped per SupplierRefNum).
*/
const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
const { logger } = req;
const xml = req.body;
let payload;
try {
payload = await xml2js.parseStringPromise(xml, {
explicitArray: false,
tagNameProcessors: [xml2js.processors.stripPrefix]
});
logger.log("parts-xml-parse", "debug", null, null, { success: true });
} catch (err) {
logger.log("parts-xml-parse-error", "error", null, null, { error: err });
return res.status(400).send("Invalid XML");
}
const rq = payload.VehicleDamageEstimateAddRq;
if (!rq) {
logger.log("parts-missing-root", "error");
return res.status(400).send("Missing <VehicleDamageEstimateAddRq>");
}
try {
//
// ── SHOP ID ───────────────────────────────────────────────────────────────────
//
// pulled directly from <ShopID> in your XML
//
const shopId = rq.ShopID || rq.shopId;
if (!shopId) {
throw { status: 400, message: "Missing <ShopID> in XML" };
}
//
// ── DOCUMENT INFO ─────────────────────────────────────────────────────────────
//
const { RqUID, RefClaimNum } = rq;
const doc = rq.DocumentInfo || {};
const comment = doc.Comment || null;
const transmitDate = doc.TransmitDateTime || null;
// capture all <DocumentVer> entries
const docVers = doc.DocumentVer ? (Array.isArray(doc.DocumentVer) ? doc.DocumentVer : [doc.DocumentVer]) : [];
const documentVersions = docVers.map((dv) => ({
code: dv.DocumentVerCode,
num: dv.DocumentVerNum
}));
// pull out any OtherReferenceInfo (RO Number + Job UUID)
const otherRefs = doc.ReferenceInfo?.OtherReferenceInfo
? Array.isArray(doc.ReferenceInfo.OtherReferenceInfo)
? doc.ReferenceInfo.OtherReferenceInfo
: [doc.ReferenceInfo.OtherReferenceInfo]
: [];
const originalRoNumber = otherRefs.find((r) => r.OtherReferenceName === "RO Number")?.OtherRefNum;
const originalJobUuid = otherRefs.find((r) => r.OtherReferenceName === "Job UUID")?.OtherRefNum;
//
// ── EVENT INFO ────────────────────────────────────────────────────────────────
//
const ev = rq.EventInfo || {};
const assignEv = ev.AssignmentEvent || {};
const assignmentEvent = {
number: assignEv.AssignmentNumber,
type: assignEv.AssignmentType,
date: assignEv.AssignmentDate,
createdAt: assignEv.CreateDateTime
};
const repairEv = ev.RepairEvent || {};
const scheduled_completion = repairEv.TargetCompletionDateTime || null;
const scheduled_in = repairEv.RequestedPickUpDateTime || null;
//
// ── CLAIM INFO ────────────────────────────────────────────────────────────────
//
const ci = rq.ClaimInfo || {};
const clm_no = ci.ClaimNum;
const ClaimStatus = ci.ClaimStatus || null;
const policy_no = ci.PolicyInfo?.PolicyNum || null;
const ded_amt = parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0);
// if your XML ever has a `<Cieca_ttl>` you'd parse it here
const clm_total = parseFloat(ci.Cieca_ttl || 0);
//
// ── OWNER ─────────────────────────────────────────────────────────────────────
//
const ownerParty = rq.AdminInfo?.Owner?.Party || {};
const ownerName = ownerParty.PersonInfo?.PersonName || {};
const ownerOrg = ownerParty.OrgInfo || {};
const ownerAddr = ownerParty.PersonInfo?.Communications?.Address || {};
const ownerComms = ownerParty.ContactInfo?.Communications
? Array.isArray(ownerParty.ContactInfo.Communications)
? ownerParty.ContactInfo.Communications
: [ownerParty.ContactInfo.Communications]
: [];
let ownerPhone = null,
ownerEmail = null;
ownerComms.forEach((c) => {
if (c.CommQualifier === "CP") ownerPhone = c.CommPhone;
if (c.CommQualifier === "EM") ownerEmail = c.CommEmail;
});
const ownerPrefContact = ownerParty.PreferredContactMethod || null;
//
// ── VEHICLE INFO ──────────────────────────────────────────────────────────────
//
const vin = rq.VehicleInfo?.VINInfo?.VINNum || null;
const plate_no = rq.VehicleInfo?.License?.LicensePlateNum || null;
const plate_st = rq.VehicleInfo?.License?.LicensePlateStateProvince || null;
const desc = rq.VehicleInfo?.VehicleDesc || {};
const v_model_yr = desc.ModelYear || null;
const v_make_desc = desc.MakeDesc || null;
const v_model_desc = desc.ModelName || null;
const body_style = desc.BodyStyle || null;
const engine_desc = desc.EngineDesc || null;
const production_date = desc.ProductionDate || null;
const sub_model_desc = desc.SubModelDesc || null;
const fuel_type = desc.FuelType || null;
const v_color = rq.VehicleInfo?.Paint?.Exterior?.ColorName || null;
const drivable = rq.VehicleInfo?.Condition?.DrivableInd === "Y";
//
// ── PROFILE & RATES ───────────────────────────────────────────────────────────
//
const rateInfos = rq.ProfileInfo?.RateInfo
? Array.isArray(rq.ProfileInfo.RateInfo)
? rq.ProfileInfo.RateInfo
: [rq.ProfileInfo.RateInfo]
: [];
const rates = {}; // main { rate_lab, rate_laf, … }
const rateTier = {}; // e.g. { MA2S: [ {tier, pct}, … ] }
const materialCalc = {}; // e.g. { LAR: { CalcMethodCode, CalcMaxHours }, … }
rateInfos.forEach((r) => {
if (!r || !r.RateType) return;
const t = r.RateType;
// main perunit rate
if (r.Rate) rates[`rate_${t.toLowerCase()}`] = parseFloat(r.Rate) || 0;
// any tier settings
if (r.RateTierInfo) {
const tiers = Array.isArray(r.RateTierInfo) ? r.RateTierInfo : [r.RateTierInfo];
rateTier[t] = tiers.map((ti) => ({
tier: ti.TierNum,
pct: parseFloat(ti.Percentage) || 0
}));
}
// any materialcalc limits
if (r.MaterialCalcSettings) {
materialCalc[t] = r.MaterialCalcSettings;
}
});
//
// ── DAMAGE LINES → joblinesData ─────────────────────────────────────────────
//
const damageLines = Array.isArray(rq.DamageLineInfo) ? rq.DamageLineInfo : [rq.DamageLineInfo];
const joblinesData = damageLines.map((line) => ({
line_no: parseInt(line.LineNum, 10),
unq_seq: parseInt(line.UniqueSequenceNum, 10),
manual_line: line.ManualLineInd === "1",
automated_entry: line.AutomatedEntry === "1",
desc_judgment_ind: line.DescJudgmentInd === "1",
status: line.LineStatusCode || null,
line_desc: line.LineDesc || null,
// parts
part_type: line.PartInfo.PartType || null,
part_qty: parseInt(line.PartInfo.Quantity || 0, 10),
db_price: parseFloat(line.PartInfo.PartPrice || 0),
act_price: parseFloat(line.PartInfo.PartPrice || 0),
oem_partno: line.PartInfo.OEMPartNum || null,
// non-OEM block
non_oem_part_num: line.PartInfo?.NonOEM?.NonOEMPartNum || null,
non_oem_part_price: parseFloat(line.PartInfo?.NonOEM?.NonOEMPartPrice || 0),
supplier_ref_num: line.PartInfo?.NonOEM?.SupplierRefNum || null,
part_selected_ind: line.PartInfo?.NonOEM?.PartSelectedInd === "1",
after_market_usage: line.PartInfo.AfterMarketUsage || null,
certification_type: line.PartInfo.CertificationType || null,
tax_part: line.PartInfo.TaxableInd === "1",
glass_flag: line.PartInfo.GlassPartInd === "1",
price_j: line.PriceJudgmentInd === "1",
price_inc: line.PriceInclInd === "1",
order_by_application_ind: String(line.PartInfo.OrderByApplicationInd).toLowerCase() === "true",
// labor
mod_lbr_ty: line.LaborInfo?.LaborType || null,
mod_lb_hrs: parseFloat(line.LaborInfo?.LaborHours || 0),
lbr_op: line.LaborInfo?.LaborOperation || null,
lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0),
// linkage & memo
parent_line_no: line.ParentLineNum ? parseInt(line.ParentLineNum, 10) : null,
notes: line.LineMemo || null
}));
//
// ── BUILD & INSERT THE JOB ───────────────────────────────────────────────────
//
const jobInput = {
shopid: shopId,
// identifiers
ro_number: RefClaimNum,
original_ro_number: originalRoNumber,
original_job_uuid: originalJobUuid,
// claim & policy
clm_no,
status: ClaimStatus,
clm_total,
policy_no,
ded_amt,
// timestamps & comments
comment,
date_exported: transmitDate,
// owner
ownr_fn: ownerName.FirstName || null,
ownr_ln: ownerName.LastName || null,
ownr_co_nm: ownerOrg.CompanyName || null,
ownr_addr1: ownerAddr.Address1 || null,
ownr_city: ownerAddr.City || null,
ownr_st: ownerAddr.StateProvince || null,
ownr_zip: ownerAddr.PostalCode || null,
ownr_country: ownerAddr.Country || null,
ownr_ph1: ownerPhone,
ownr_ea: ownerEmail,
ownr_pref_contact: ownerPrefContact,
// vehicle
v_vin: vin,
plate_no,
plate_st,
v_model_yr,
v_make_desc,
v_model_desc,
v_color,
body_style,
engine_desc,
production_date,
sub_model_desc,
fuel_type,
drivable,
// labor & material rates
...rates,
// everything extra in one JSON column
production_vars: {
rqUid: RqUID,
documentVersions,
assignmentEvent,
scheduled_completion,
scheduled_in,
rateTier,
materialCalc
},
// nested joblines
joblines: { data: joblinesData }
};
logger.log("parts-insert-job", "debug", null, null, { jobInput });
const jobResp = await client.request(INSERT_JOB_WITH_LINES, {
job: jobInput
});
const newJob = jobResp.insert_jobs_one;
const jobId = newJob.id;
logger.log("parts-job-created", "info", jobId, null);
//
// ── BUILD & INSERT PARTS ORDERS ────────────────────────────────────────────
//
// group lines by their SupplierRefNum
const insertedMap = newJob.joblines.reduce((m, ln) => {
m[ln.unq_seq] = ln.id;
return m;
}, {});
const poGroups = {};
damageLines.forEach((line) => {
const pt = line.PartInfo.PartType;
const qty = parseInt(line.PartInfo.Quantity || 0, 10);
if (["PAN", "PAC", "PAM", "PAA"].includes(pt) && qty > 0) {
const vendorid = line.PartInfo?.NonOEM?.SupplierRefNum || process.env.DEFAULT_VENDOR_ID;
if (!poGroups[vendorid]) poGroups[vendorid] = [];
poGroups[vendorid].push({
line,
unq_seq: parseInt(line.UniqueSequenceNum, 10)
});
}
});
const partsOrders = Object.entries(poGroups).map(([vendorid, entries]) => ({
jobid: jobId,
vendorid,
order_number: `${clm_no}-${entries[0].line.LineNum}`,
parts_order_lines: {
data: entries.map(({ line, unq_seq }) => ({
job_line_id: insertedMap[unq_seq],
part_type: line.PartInfo.PartType,
quantity: parseInt(line.PartInfo.Quantity || 0, 10),
act_price: parseFloat(line.PartInfo.PartPrice || 0),
db_price: parseFloat(line.PartInfo.PartPrice || 0),
line_desc: line.LineDesc,
non_oem_part_num: line.PartInfo?.NonOEM?.NonOEMPartNum || null,
non_oem_part_price: parseFloat(line.PartInfo?.NonOEM?.NonOEMPartPrice || 0),
part_selected_ind: line.PartInfo?.NonOEM?.PartSelectedInd === "1"
}))
}
}));
if (partsOrders.length) {
logger.log("parts-insert-orders", "debug", null, null, {
partsOrders
});
await client.request(INSERT_PARTS_ORDERS, { po: partsOrders });
logger.log("parts-orders-created", "info", jobId, null);
}
return res.status(200).json({ success: true, jobId });
} catch (err) {
logger.log("parts-route-error", "error", null, null, { error: err });
return res.status(err.status || 500).json({ error: err.message || "Internal error" });
}
};
module.exports = partsManagementVehicleDamageEstimateAddRq;