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

This commit is contained in:
Dave Richer
2025-06-23 14:00:58 -04:00
parent b0283f827e
commit 4b83330db9
5 changed files with 193 additions and 71 deletions

View File

@@ -4,6 +4,9 @@
const xml2js = require("xml2js");
const client = require("../../graphql-client/graphql-client").client;
// Defaults
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; // Default status if not found in bodyshop
// GraphQL statements
const INSERT_JOB_WITH_LINES = `
mutation InsertJob($job: jobs_insert_input!) {
@@ -14,6 +17,30 @@ const INSERT_JOB_WITH_LINES = `
}
`;
const GET_BODYSHOP_STATUS = `
query GetBodyshopStatus($id: uuid!) {
bodyshops_by_pk(id: $id) {
md_order_statuses
}
}
`;
const INSERT_OWNER = `
mutation InsertOwner($owner: owners_insert_input!) {
insert_owners_one(object: $owner) {
id
}
}
`;
// Do they call the add call first, future ones will be updates, we need to upcycle. Or we need to send a new add request, we treat it as an upsert.
/**
* Handles the VehicleDamageEstimateAddRq XML request from parts management.
* @param req
* @param res
* @returns {Promise<*>}
*/
const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
const { logger } = req;
const xml = req.body;
@@ -23,9 +50,11 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
try {
payload = await xml2js.parseStringPromise(xml, {
explicitArray: false,
tagNameProcessors: [xml2js.processors.stripPrefix]
tagNameProcessors: [xml2js.processors.stripPrefix],
attrNameProcessors: [xml2js.processors.stripPrefix]
// ignoreAttrs: false,
// xmlns: false
});
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");
@@ -43,6 +72,14 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
if (!shopId) throw { status: 400, message: "Missing <ShopID> in XML" };
const { RefClaimNum } = rq;
let defaultStatus = FALLBACK_DEFAULT_ORDER_STATUS;
try {
const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId });
defaultStatus = bodyshop_by_pk?.md_order_statuses?.default_open || defaultStatus;
} catch (err) {
logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err });
}
// ── DOCUMENT INFO ──────────────────────────────────────────────────────────
const doc = rq.DocumentInfo || {};
const comment = doc.Comment || null;
@@ -55,6 +92,68 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
const category = doc.DocumentType || null;
const classType = doc.DocumentStatus || null;
// ── PARTS TAX RATES STRUCTURE ───────────────────────────────────────────────
// Known rate types that map to your parts_tax_rates keys
const knownPartRateTypes = [
"PAA",
"PAC",
"PAG",
"PAL",
"PAM",
"PAN",
"PAO",
"PAP",
"PAR",
"PAS",
"PASL",
"CCC",
"CCD",
"CCF",
"CCM",
"CCDR"
];
const profile = rq.ProfileInfo || {};
const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}];
const parts_tax_rates = {};
for (const code of knownPartRateTypes) {
const rateInfo = rateInfos.find((r) => (r?.RateType || "").toUpperCase() === code);
if (!rateInfo) {
parts_tax_rates[code] = {};
continue;
}
const taxInfo = rateInfo.TaxInfo;
const taxTier = taxInfo?.TaxTierInfo;
// Try to find Percentage first
let percentage = parseFloat(taxTier?.Percentage ?? "NaN");
if (isNaN(percentage)) {
// fallback to RateTierInfo.Rate if that's where it might be
const tierRate = Array.isArray(rateInfo.RateTierInfo)
? rateInfo.RateTierInfo[0]?.Rate
: rateInfo.RateTierInfo?.Rate;
percentage = parseFloat(tierRate ?? "NaN");
}
// Still no tax rate? fallback to null object
if (isNaN(percentage)) {
parts_tax_rates[code] = {};
continue;
}
parts_tax_rates[code] = {
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: percentage / 100
};
}
// ── EVENT INFO ──────────────────────────────────────────────────────────────
const ev = rq.EventInfo || {};
const asgn = ev.AssignmentEvent || {};
@@ -106,8 +205,7 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
const est_ct_fn = est_fn;
const est_ct_ln = est_ln;
// TODO: SHould be the estimator insurance company name est_co_name
const est_aff = rq.AdminInfo?.Estimator?.Affiliation || null;
const est_co_nm = rq.AdminInfo?.Estimator?.Affiliation || null;
const estComms = Array.isArray(estParty.ContactInfo?.Communications)
? estParty.ContactInfo.Communications
@@ -193,9 +291,34 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
notes: line.LineMemo || null
}));
const ownerInput = {
shopid: shopId,
ownr_fn,
ownr_ln,
ownr_co_nm,
ownr_addr1,
ownr_addr2,
ownr_city,
ownr_st,
ownr_zip,
ownr_ctry,
ownr_ph1,
ownr_ph2,
ownr_ea
};
let ownerid = null;
try {
const { insert_owners_one } = await client.request(INSERT_OWNER, { owner: ownerInput });
ownerid = insert_owners_one?.id;
} catch (err) {
logger.log("parts-owner-insert-failed", "warn", null, null, { error: err });
}
// ── BUILD & INSERT THE JOB ──────────────────────────────────────────────────
const jobInput = {
shopid: shopId,
ownerid,
ro_number: RefClaimNum,
// IDs & CIECA metadata
@@ -205,10 +328,12 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
category,
class: classType,
// tax
parts_tax_rates,
// claim & policy
clm_no,
// default job: bodyshop.md_status.default_open
status: status || "OPEN",
status: status || defaultStatus,
clm_total: cieca_ttl,
policy_no,
ded_amt,
@@ -238,7 +363,7 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
ownr_ea,
// estimator
// est_co_id: est_aff,
est_co_nm,
est_ct_fn,
est_ct_ln,
est_ea,
@@ -253,16 +378,13 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => {
servicing_dealer,
servicing_dealer_contact,
// stash any extra CIECA stuff we didnt map above
production_vars: {},
// nested relationships
vehicle: { data: vehicleData },
joblines: { data: joblinesData }
};
logger.log("parts-insert-job", "debug", null, null, { jobInput });
const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput });
logger.log("parts-job-created", "info", newJob.id, null);
return res.status(200).json({ success: true, jobId: newJob.id });