feature/IO-3255-simplified-parts-management - Checkpoint
This commit is contained in:
@@ -23,17 +23,21 @@ const KNOWN_PART_RATE_TYPES = [
|
||||
* @returns {object} The parts tax rates object.
|
||||
*/
|
||||
|
||||
//PF: Major validation would be required on this - EMS files are inconsistent with things like 5% being passed as 5.0 or .05.
|
||||
//PF: Is this data being sent by them now?
|
||||
//TODO: Major validation would be required on this - EMS files are inconsistent with things like 5% being passed as 5.0 or .05.
|
||||
const extractPartsTaxRates = (profile = {}) => {
|
||||
const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}];
|
||||
const partsTaxRates = {};
|
||||
|
||||
/**
|
||||
* In this context, r.RateType._ accesses the property named _ on the RateType object.
|
||||
* This pattern is common when handling data parsed from XML, where element values are stored under the _ key. So,
|
||||
* _ aligns to the actual value/content of the RateType field when RateType is an object (not a string).
|
||||
*/
|
||||
for (const r of rateInfos) {
|
||||
const rateTypeRaw =
|
||||
typeof r?.RateType === "string"
|
||||
? r.RateType
|
||||
: typeof r?.RateType === "object" && r?.RateType._ //PF: what does _ align to?
|
||||
: typeof r?.RateType === "object" && r?.RateType._
|
||||
? r.RateType._
|
||||
: "";
|
||||
const rateType = (rateTypeRaw || "").toUpperCase();
|
||||
|
||||
@@ -83,46 +83,46 @@ const deleteJobsByIds = async (jobIds) => {
|
||||
*/
|
||||
const partsManagementDeprovisioning = async (req, res) => {
|
||||
const { logger } = req;
|
||||
const p = req.body; //Same as other file, can p be renamed to be more descriptive?
|
||||
const body = req.body;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!p.shopId) {
|
||||
if (!body.shopId) {
|
||||
throw { status: 400, message: "shopId is required." };
|
||||
}
|
||||
|
||||
// Fetch bodyshop and check external_shop_id
|
||||
const shopResp = await client.request(GET_BODYSHOP, { id: p.shopId });
|
||||
const shopResp = await client.request(GET_BODYSHOP, { id: body.shopId });
|
||||
const shop = shopResp.bodyshops_by_pk;
|
||||
if (!shop) {
|
||||
throw { status: 404, message: `Bodyshop with id ${p.shopId} not found.` };
|
||||
throw { status: 404, message: `Bodyshop with id ${body.shopId} not found.` };
|
||||
}
|
||||
if (!shop.external_shop_id) {
|
||||
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
||||
}
|
||||
|
||||
logger.log("admin-delete-shop", "debug", null, null, {
|
||||
shopId: p.shopId,
|
||||
shopId: body.shopId,
|
||||
shopname: shop.shopname,
|
||||
ioadmin: true
|
||||
});
|
||||
|
||||
// Get vendors
|
||||
const vendorsResp = await client.request(GET_VENDORS, { shopId: p.shopId });
|
||||
const vendorsResp = await client.request(GET_VENDORS, { shopId: body.shopId });
|
||||
const deletedVendors = vendorsResp.vendors.map((v) => v.name);
|
||||
|
||||
// Get associated users
|
||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: p.shopId });
|
||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: body.shopId });
|
||||
const associatedUsers = assocResp.associations.map((assoc) => ({
|
||||
authId: assoc.user.authid,
|
||||
email: assoc.user.email
|
||||
}));
|
||||
|
||||
// Delete associations for the shop
|
||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: p.shopId });
|
||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: body.shopId });
|
||||
const associationsDeleted = assocDeleteResp.delete_associations.affected_rows;
|
||||
|
||||
// For each user, check if they have remaining associations; if not, delete user and Firebase account
|
||||
@@ -138,23 +138,23 @@ const partsManagementDeprovisioning = async (req, res) => {
|
||||
}
|
||||
|
||||
// Get all job ids for this shop, then delete joblines and jobs (joblines first)
|
||||
const jobIds = await getJobIdsForShop(p.shopId);
|
||||
const jobIds = await getJobIdsForShop(body.shopId);
|
||||
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
||||
const jobsDeleted = await deleteJobsByIds(jobIds);
|
||||
|
||||
// Delete any audit trail entries tied to this bodyshop to avoid FK violations
|
||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: p.shopId });
|
||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: body.shopId });
|
||||
const auditDeleted = auditResp.delete_audit_trail.affected_rows;
|
||||
|
||||
// Delete vendors
|
||||
await deleteVendorsByShop(p.shopId);
|
||||
await deleteVendorsByShop(body.shopId);
|
||||
|
||||
// Delete shop
|
||||
await deleteBodyshop(p.shopId);
|
||||
await deleteBodyshop(body.shopId);
|
||||
|
||||
// Summary log
|
||||
logger.log("admin-delete-shop-summary", "info", null, null, {
|
||||
shopId: p.shopId,
|
||||
shopId: body.shopId,
|
||||
shopname: shop.shopname,
|
||||
associationsDeleted,
|
||||
deletedUsers,
|
||||
@@ -165,8 +165,8 @@ const partsManagementDeprovisioning = async (req, res) => {
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: `Bodyshop ${p.shopId} and associated resources deleted successfully.`,
|
||||
deletedShop: { id: p.shopId, name: shop.shopname },
|
||||
message: `Bodyshop ${body.shopId} and associated resources deleted successfully.`,
|
||||
deletedShop: { id: body.shopId, name: shop.shopname },
|
||||
deletedAssociationsCount: associationsDeleted,
|
||||
deletedUsers: deletedUsers,
|
||||
deletedVendors: deletedVendors,
|
||||
|
||||
@@ -139,12 +139,11 @@ const insertUserAssociation = async (uid, email, shopId) => {
|
||||
*/
|
||||
const partsManagementProvisioning = async (req, res) => {
|
||||
const { logger } = req;
|
||||
const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
||||
// Can p be renamed to be more descriptive?
|
||||
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
||||
|
||||
try {
|
||||
await ensureEmailNotRegistered(p.userEmail);
|
||||
requireFields(p, [
|
||||
await ensureEmailNotRegistered(body.userEmail);
|
||||
requireFields(body, [
|
||||
"external_shop_id",
|
||||
"shopname",
|
||||
"address1",
|
||||
@@ -156,27 +155,27 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
"phone",
|
||||
"userEmail"
|
||||
]);
|
||||
await ensureExternalIdUnique(p.external_shop_id);
|
||||
await ensureExternalIdUnique(body.external_shop_id);
|
||||
|
||||
logger.log("admin-create-shop-user", "debug", p.userEmail, null, {
|
||||
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
|
||||
request: req.body,
|
||||
ioadmin: true
|
||||
});
|
||||
|
||||
const shopInput = {
|
||||
shopname: p.shopname,
|
||||
address1: p.address1,
|
||||
address2: p.address2 || null,
|
||||
city: p.city,
|
||||
state: p.state,
|
||||
zip_post: p.zip_post,
|
||||
country: p.country,
|
||||
email: p.email,
|
||||
external_shop_id: p.external_shop_id,
|
||||
timezone: p.timezone || DefaultNewShop.timezone,
|
||||
phone: p.phone,
|
||||
shopname: body.shopname,
|
||||
address1: body.address1,
|
||||
address2: body.address2 || null,
|
||||
city: body.city,
|
||||
state: body.state,
|
||||
zip_post: body.zip_post,
|
||||
country: body.country,
|
||||
email: body.email,
|
||||
external_shop_id: body.external_shop_id,
|
||||
timezone: body.timezone || DefaultNewShop.timezone,
|
||||
phone: body.phone,
|
||||
logo_img_path: {
|
||||
src: p.logoUrl,
|
||||
src: body.logoUrl,
|
||||
width: "",
|
||||
height: "",
|
||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||
@@ -201,7 +200,7 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
||||
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
||||
vendors: {
|
||||
data: p.vendors.map((v) => ({ //Many of these will be empty, but good to call out explicitly to self document.
|
||||
data: body.vendors.map((v) => ({
|
||||
name: v.name,
|
||||
street1: v.street1 || null,
|
||||
street2: v.street2 || null,
|
||||
@@ -222,23 +221,22 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
};
|
||||
|
||||
const newShopId = await insertBodyshop(shopInput);
|
||||
const userRecord = await createFirebaseUser(p.userEmail, p.userPassword);
|
||||
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
|
||||
let resetLink = null;
|
||||
if (!p.userPassword) resetLink = await generateResetLink(p.userEmail);
|
||||
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
|
||||
|
||||
const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId);
|
||||
//Association can be included in shop creation call, but this is also prescriptive and fine.
|
||||
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
|
||||
|
||||
return res.status(200).json({
|
||||
shop: { id: newShopId, shopname: p.shopname },
|
||||
shop: { id: newShopId, shopname: body.shopname },
|
||||
user: {
|
||||
id: createdUser.id,
|
||||
email: createdUser.email,
|
||||
resetLink: resetLink || undefined //Does WE know to expect this?
|
||||
resetLink: resetLink || undefined
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.log("admin-create-shop-user-error", "error", p.userEmail, null, {
|
||||
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
|
||||
message: err.message,
|
||||
detail: err.detail || err
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const {
|
||||
} = require("../partsManagement.queries");
|
||||
|
||||
// Defaults
|
||||
const FALLBACK_DEFAULT_ORDER_STATUS = "Open";
|
||||
const FALLBACK_DEFAULT_JOB_STATUS = "Open";
|
||||
|
||||
/**
|
||||
* Fetches the default order status for a bodyshop.
|
||||
@@ -22,13 +22,13 @@ const FALLBACK_DEFAULT_ORDER_STATUS = "Open";
|
||||
* @param {object} logger - The logger instance.
|
||||
* @returns {Promise<string>} The default status or fallback.
|
||||
*/
|
||||
const getDefaultOrderStatus = async (shopId, logger) => {
|
||||
const getDefaultJobStatus = async (shopId, logger) => {
|
||||
try {
|
||||
const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId });
|
||||
return bodyshop_by_pk?.md_order_statuses?.default_open || FALLBACK_DEFAULT_ORDER_STATUS; //I think this is intended to be called job status, not order status.
|
||||
return bodyshop_by_pk?.md_ro_statuses?.default_imported || FALLBACK_DEFAULT_JOB_STATUS;
|
||||
} catch (err) {
|
||||
logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err });
|
||||
return FALLBACK_DEFAULT_ORDER_STATUS;
|
||||
return FALLBACK_DEFAULT_JOB_STATUS;
|
||||
}
|
||||
};
|
||||
/**
|
||||
@@ -66,6 +66,7 @@ const extractJobData = (rq) => {
|
||||
|
||||
return {
|
||||
shopId: rq.ShopID || rq.shopId,
|
||||
// status: ci.ClaimStatus || null, Proper, setting it default for now
|
||||
refClaimNum: rq.RefClaimNum,
|
||||
ciecaid: rq.RqUID || null,
|
||||
// Pull Cieca_ttl from ClaimInfo per schema/sample
|
||||
@@ -81,8 +82,6 @@ const extractJobData = (rq) => {
|
||||
scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
|
||||
scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
|
||||
clm_no: ci.ClaimNum || null,
|
||||
// status: ci.ClaimStatus || null, Proper, setting it default for now
|
||||
status: FALLBACK_DEFAULT_ORDER_STATUS,
|
||||
policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null,
|
||||
ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
|
||||
};
|
||||
@@ -101,17 +100,19 @@ const extractOwnerData = (rq, shopId) => {
|
||||
const personName = personInfo.PersonName || {};
|
||||
const address = personInfo.Communications?.Address || {};
|
||||
|
||||
let ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph;
|
||||
let ownr_ph1, ownr_ph2, ownr_ea;
|
||||
|
||||
const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications)
|
||||
? ownerOrClaimant.ContactInfo.Communications
|
||||
: [ownerOrClaimant.ContactInfo?.Communications || {}];
|
||||
|
||||
for (const c of comms) {
|
||||
if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone; //PF: Should document this logic. 1 and 2 don't typically indicate type in EMS. This makes sense, but good to document.
|
||||
// TODO: Should document this logic. 1 and 2 don't
|
||||
// typically indicate type in EMS. This makes sense, but good to document.
|
||||
if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone;
|
||||
if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone;
|
||||
if (c.CommQualifier === "EM") ownr_ea = c.CommEmail;
|
||||
if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
|
||||
// if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -127,8 +128,8 @@ const extractOwnerData = (rq, shopId) => {
|
||||
ownr_ctry: address.Country || null,
|
||||
ownr_ph1,
|
||||
ownr_ph2,
|
||||
ownr_ea,
|
||||
ownr_alt_ph //PF: This is not in the DB, if this object is inserted in place, this will fail.
|
||||
ownr_ea
|
||||
// ownr_alt_ph
|
||||
// ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New
|
||||
// ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New
|
||||
// ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New
|
||||
@@ -159,38 +160,40 @@ const extractEstimatorData = (rq) => {
|
||||
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
||||
* @returns {object} Adjuster data.
|
||||
*/
|
||||
const extractAdjusterData = (rq) => {
|
||||
const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
|
||||
const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
|
||||
? adjParty.ContactInfo.Communications
|
||||
: [adjParty.ContactInfo?.Communications || {}];
|
||||
|
||||
return {
|
||||
agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null, //PF: I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
|
||||
agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
||||
agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
||||
agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
|
||||
};
|
||||
};
|
||||
// const extractAdjusterData = (rq) => {
|
||||
// const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
|
||||
// const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
|
||||
// ? adjParty.ContactInfo.Communications
|
||||
// : [adjParty.ContactInfo?.Communications || {}];
|
||||
//
|
||||
// return {
|
||||
// //TODO: I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
|
||||
// agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
|
||||
// agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
||||
// agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
||||
// agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
|
||||
// };
|
||||
// };
|
||||
|
||||
/**
|
||||
* Extracts repair facility data from the XML request.
|
||||
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
||||
* @returns {object} Repair facility data.
|
||||
*/
|
||||
const extractRepairFacilityData = (rq) => {
|
||||
const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
|
||||
const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
|
||||
? rfParty.ContactInfo.Communications
|
||||
: [rfParty.ContactInfo?.Communications || {}];
|
||||
|
||||
return {
|
||||
servicing_dealer: rfParty.OrgInfo?.CompanyName || null, //PF: The servicing dealer fields are a relic from synergy for a few folks
|
||||
//PF: I suspect RF data could be ignored since they are the RF.
|
||||
servicing_dealer_contact:
|
||||
rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
||||
};
|
||||
};
|
||||
// const extractRepairFacilityData = (rq) => {
|
||||
// const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
|
||||
// const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
|
||||
// ? rfParty.ContactInfo.Communications
|
||||
// : [rfParty.ContactInfo?.Communications || {}];
|
||||
//
|
||||
// return {
|
||||
// servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
|
||||
// // TODO: The servicing dealer fields are a relic from synergy for a few folks
|
||||
// // TODO: I suspect RF data could be ignored since they are the RF.
|
||||
// servicing_dealer_contact:
|
||||
// rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
||||
// };
|
||||
// };
|
||||
|
||||
/**
|
||||
* Extracts loss information from the XML request.
|
||||
@@ -204,10 +207,12 @@ const extractLossInfo = (rq) => {
|
||||
loss_date: loss.LossDateTime || null,
|
||||
loss_type: custom.LossTypeCode || null,
|
||||
loss_desc: custom.LossTypeDesc || null
|
||||
// primary_poi: loss.PrimaryPOI?.POICode || null, //PF: These map back to area_of_impact.impact_#
|
||||
// secondary_poi: loss.SecondaryPOI?.POICode || null,
|
||||
// area_of_impact: {
|
||||
// impact_1: loss.PrimaryPOI?.POICode || null,
|
||||
// imact_2 :loss.SecondaryPOI?.POICode || null,
|
||||
// },
|
||||
// tlosind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null,
|
||||
// damage_memo: loss.DamageMemo || null, //(maybe ins_memo)
|
||||
// total_loss_ind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null // New //PF: tlosind i believe is our field.
|
||||
};
|
||||
};
|
||||
|
||||
@@ -289,9 +294,11 @@ const extractVehicleData = (rq, shopId) => {
|
||||
v_color: exterior.Color?.ColorName || null,
|
||||
v_bstyle: desc.BodyStyle || null,
|
||||
v_engine: desc.EngineDesc || null,
|
||||
v_options: desc.SubModelDesc || null, //PF: Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
|
||||
// TODO Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
|
||||
v_options: desc.SubModelDesc || null,
|
||||
v_type: desc.FuelType || null,
|
||||
v_cond: rq.VehicleInfo?.Condition?.DrivableInd, //PF: there is a separate driveable flag on the job.
|
||||
// TODO there is a separate driveable flag on the job.
|
||||
v_cond: rq.VehicleInfo?.Condition?.DrivableInd,
|
||||
v_trimcode: desc.TrimCode || null,
|
||||
v_tone: exterior.Tone || null,
|
||||
v_stage: exterior.RefinishStage || rq.VehicleInfo?.Paint?.RefinishStage || null,
|
||||
@@ -343,7 +350,8 @@ const extractJobLines = (rq) => {
|
||||
line.ManualLineInd === true ||
|
||||
line.ManualLineInd === 1 ||
|
||||
line.ManualLineInd === "1" ||
|
||||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y"); //PF: manual line tracks manual in IO or not, this woudl presumably always be false
|
||||
// TODO: manual line tracks manual in IO or not, this woudl presumably always be false
|
||||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
||||
} else {
|
||||
lineOut.manual_line = null;
|
||||
}
|
||||
@@ -356,8 +364,10 @@ const extractJobLines = (rq) => {
|
||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null; //PF: if aftermarket part, we have alt_part_no to capture.
|
||||
lineOut.db_price = isNaN(price) ? 0 : price; //PF: the Db and act price often are different. These should map back to their EMS equivalents.
|
||||
//TODO: if aftermarket part, we have alt_part_no to capture.
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
//TODO: the Db and act price often are different. These should map back to their EMS equivalents.
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
|
||||
// Tax flag from PartInfo.TaxableInd when provided
|
||||
@@ -373,8 +383,11 @@ const extractJobLines = (rq) => {
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
}
|
||||
} else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0); //PF: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically could. May require additional discussion.
|
||||
}
|
||||
//TODO: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically
|
||||
// could.May require additional discussion.
|
||||
else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
lineOut.part_type = "PAS"; // Sublet as parts-as-service
|
||||
lineOut.part_qty = 1;
|
||||
lineOut.act_price = isNaN(amt) ? 0 : amt;
|
||||
@@ -390,12 +403,14 @@ const extractJobLines = (rq) => {
|
||||
if (hasLabor) {
|
||||
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
||||
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
||||
lineOut.lbr_op = laborInfo.LaborOperation || null; //PF: can add lbr_op_desc according to mapping available in new partner.
|
||||
//TODO: can add lbr_op_desc according to mapping available in new partner.
|
||||
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
||||
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
//PF: what's the BMS logic for this? Body and refinish operations can often happen to the same part, but most systems output a second line for the refinish labor.
|
||||
//PF: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
|
||||
//TODO: what's the BMS logic for this? Body and refinish operations can often happen to the same part,
|
||||
// but most systems output a second line for the refinish labor.
|
||||
//TODO: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
|
||||
// Refinish labor (if present) recorded on the same line using secondary labor fields
|
||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
@@ -406,9 +421,9 @@ const extractJobLines = (rq) => {
|
||||
!isNaN(rAmt) ||
|
||||
!!refinishInfo.LaborOperation);
|
||||
if (hasRefinish) {
|
||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //PF: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;//PF: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //PF: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //TODO: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; //TODO: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //TODO: _j fields indicate judgement, and are bool type.
|
||||
// Aggregate refinish labor amount into the total labor amount for the line
|
||||
if (!isNaN(rAmt)) {
|
||||
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
@@ -424,26 +439,26 @@ const extractJobLines = (rq) => {
|
||||
};
|
||||
|
||||
// Helper to extract a GRAND TOTAL amount from RepairTotalsInfo
|
||||
const extractGrandTotal = (rq) => {
|
||||
const rti = rq.RepairTotalsInfo;
|
||||
const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
|
||||
for (const grp of groups) {
|
||||
const sums = Array.isArray(grp.SummaryTotalsInfo)
|
||||
? grp.SummaryTotalsInfo
|
||||
: grp.SummaryTotalsInfo
|
||||
? [grp.SummaryTotalsInfo]
|
||||
: [];
|
||||
for (const s of sums) {
|
||||
const type = (s.TotalType || "").toString().toUpperCase();
|
||||
const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
|
||||
if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
|
||||
const amt = parseFloat(s.TotalAmt ?? "NaN");
|
||||
if (!isNaN(amt)) return amt;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// const extractGrandTotal = (rq) => {
|
||||
// const rti = rq.RepairTotalsInfo;
|
||||
// const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
|
||||
// for (const grp of groups) {
|
||||
// const sums = Array.isArray(grp.SummaryTotalsInfo)
|
||||
// ? grp.SummaryTotalsInfo
|
||||
// : grp.SummaryTotalsInfo
|
||||
// ? [grp.SummaryTotalsInfo]
|
||||
// : [];
|
||||
// for (const s of sums) {
|
||||
// const type = (s.TotalType || "").toString().toUpperCase();
|
||||
// const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
|
||||
// if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
|
||||
// const amt = parseFloat(s.TotalAmt ?? "NaN");
|
||||
// if (!isNaN(amt)) return amt;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// };
|
||||
|
||||
/**
|
||||
* Inserts an owner and returns the owner ID.
|
||||
@@ -462,24 +477,26 @@ const insertOwner = async (ownerInput, logger) => {
|
||||
};
|
||||
|
||||
// Fallback: compute a naive total from joblines (parts + sublet + labor amounts)
|
||||
const computeLinesTotal = (joblines = []) => {
|
||||
let parts = 0;
|
||||
let labor = 0;
|
||||
for (const jl of joblines) {
|
||||
if (jl?.part_type) {
|
||||
const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
|
||||
const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
|
||||
parts += price * (qty || 1);
|
||||
} else if (!jl.part_type && Number.isFinite(jl.act_price)) {
|
||||
parts += jl.act_price;
|
||||
}
|
||||
if (Number.isFinite(jl.lbr_amt)) {
|
||||
labor += jl.lbr_amt;
|
||||
}
|
||||
}
|
||||
const total = parts + labor; //PF: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
|
||||
return Number.isFinite(total) && total > 0 ? total : 0;
|
||||
};
|
||||
// const computeLinesTotal = (joblines = []) => {
|
||||
// let parts = 0;
|
||||
// let labor = 0;
|
||||
// for (const jl of joblines) {
|
||||
// if (jl?.part_type) {
|
||||
// const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
|
||||
// const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
|
||||
// parts += price * (qty || 1);
|
||||
// } else if (!jl.part_type && Number.isFinite(jl.act_price)) {
|
||||
// parts += jl.act_price;
|
||||
// }
|
||||
// if (Number.isFinite(jl.lbr_amt)) {
|
||||
// labor += jl.lbr_amt;
|
||||
// }
|
||||
// }
|
||||
// const total = parts + labor;
|
||||
//
|
||||
// //TODO: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
|
||||
// return Number.isFinite(total) && total > 0 ? total : 0;
|
||||
// };
|
||||
|
||||
/**
|
||||
* Handles the VehicleDamageEstimateAddRq XML request from parts management.
|
||||
@@ -516,9 +533,9 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
scheduled_in,
|
||||
scheduled_completion,
|
||||
clm_no,
|
||||
status,
|
||||
policy_no,
|
||||
ded_amt
|
||||
// status,
|
||||
} = extractJobData(rq);
|
||||
|
||||
if (!shopId) {
|
||||
@@ -526,22 +543,22 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
}
|
||||
|
||||
// Get default status
|
||||
const defaultStatus = await getDefaultOrderStatus(shopId, logger); //This likely should be get default job status, not order.
|
||||
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 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);
|
||||
// const grandTotal = extractGrandTotal(rq);
|
||||
// const computedTotal = grandTotal ?? computeLinesTotal(joblinesData);
|
||||
|
||||
// Find or create relationships
|
||||
const ownerid = await insertOwner(ownerData, logger);
|
||||
@@ -560,8 +577,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
class: classType,
|
||||
parts_tax_rates,
|
||||
clm_no,
|
||||
status: status || defaultStatus,
|
||||
clm_total: computedTotal || null,
|
||||
status: defaultStatus,
|
||||
clm_total: 0, // computedTotal || null,
|
||||
policy_no,
|
||||
ded_amt,
|
||||
comment,
|
||||
@@ -576,8 +593,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
...lossInfo,
|
||||
...ownerData,
|
||||
...estimatorData,
|
||||
...adjusterData,
|
||||
...repairFacilityData,
|
||||
// ...adjusterData,
|
||||
// ...repairFacilityData,
|
||||
// Inline vehicle data
|
||||
v_vin: vehicleData.v_vin,
|
||||
v_model_yr: vehicleData.v_model_yr,
|
||||
|
||||
@@ -38,13 +38,15 @@ const findJob = async (shopId, jobId, logger) => {
|
||||
const extractUpdatedJobData = (rq) => {
|
||||
const doc = rq.DocumentInfo || {};
|
||||
const claim = rq.ClaimInfo || {};
|
||||
//PF: In the full BMS world, much more can change.
|
||||
//TODO: In the full BMS world, much more can change, this will need to be expanded
|
||||
// before it can be considered an generic BMS importer, currently it is bespoke to webest
|
||||
const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;
|
||||
|
||||
const out = {
|
||||
comment: doc.Comment || null,
|
||||
clm_no: claim.ClaimNum || null,
|
||||
status: claim.ClaimStatus || null,
|
||||
// TODO: Commented out so they do not blow over with 'Auth Cust'
|
||||
// status: claim.ClaimStatus || null,
|
||||
policy_no: policyNo
|
||||
};
|
||||
|
||||
@@ -239,7 +241,6 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
||||
}
|
||||
}
|
||||
// --- End fetch current notes ---
|
||||
//PF: These are several different calls that can be pulled together to make the operation faster.
|
||||
|
||||
const updatedJobData = extractUpdatedJobData(rq);
|
||||
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes);
|
||||
@@ -247,12 +248,13 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
||||
|
||||
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||
|
||||
//PF: for changed lines, are they deleted and then reinserted?
|
||||
//PF: Updated lines should get an upsert to update things like desc, price, etc.
|
||||
//TODO: for changed lines, are they deleted and then reinserted?
|
||||
//TODO: Updated lines should get an upsert to update things like desc, price, etc.
|
||||
if (deletedLineIds?.length || updatedSeqs?.length) {
|
||||
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
|
||||
if (allToDelete.length) {
|
||||
await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete }); //PF: appears to soft delete updated lines as well.
|
||||
await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete });
|
||||
//TODO: appears to soft delete updated lines as well.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const GET_BODYSHOP_STATUS = `
|
||||
query GetBodyshopStatus($id: uuid!) {
|
||||
bodyshops_by_pk(id: $id) {
|
||||
md_order_statuses
|
||||
md_ro_statuses
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user