From 7af7f3c4e7a4963ea812f50d18dd0348e31c74ac Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 14 Aug 2025 14:56:02 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - vehicleDamageEstimateAddRq.js enhancements --- .../endpoints/vehicleDamageEstimateAddRq.js | 310 +++++++++---- .../partsManagement/sampleData/schemaData.md | 431 ++++++++++++++++++ 2 files changed, 641 insertions(+), 100 deletions(-) create mode 100644 server/integrations/partsManagement/sampleData/schemaData.md diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index 2699c39d9..e28e03685 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -3,7 +3,6 @@ const client = require("../../../graphql-client/graphql-client").client; const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); -const { toArray } = require("lodash"); // GraphQL Queries and Mutations const { @@ -35,6 +34,8 @@ const KNOWN_PART_RATE_TYPES = [ "CCM", "CCDR" ]; +// Config: include labor lines and labor in totals (default false for development ease) +const INCLUDE_LABOR = process.env.PARTS_MGMT_INCLUDE_LABOR === "true"; /** * Fetches the default order status for a bodyshop. * @param {string} shopId - The bodyshop UUID. @@ -60,47 +61,61 @@ const extractPartsTaxRates = (profile = {}) => { const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; const partsTaxRates = {}; - for (const code of KNOWN_PART_RATE_TYPES) { - const rateInfo = rateInfos.find((r) => { - const rateType = - typeof r?.RateType === "string" - ? r.RateType - : typeof r?.RateType === "object" && r?.RateType._ // xml2js sometimes uses _ for text content - ? r.RateType._ - : ""; + for (const r of rateInfos) { + const rateTypeRaw = + typeof r?.RateType === "string" + ? r.RateType + : typeof r?.RateType === "object" && r?.RateType._ + ? r.RateType._ + : ""; + const rateType = (rateTypeRaw || "").toUpperCase(); + if (!KNOWN_PART_RATE_TYPES.includes(rateType)) continue; - return rateType.toUpperCase() === code; - }); - - if (!rateInfo) { - partsTaxRates[code] = {}; - continue; - } - - const taxInfo = rateInfo.TaxInfo; + const taxInfo = r.TaxInfo; const taxTier = taxInfo?.TaxTierInfo; let percentage = parseFloat(taxTier?.Percentage ?? "NaN"); if (isNaN(percentage)) { - const tierRate = Array.isArray(rateInfo.RateTierInfo) - ? rateInfo.RateTierInfo[0]?.Rate - : rateInfo.RateTierInfo?.Rate; + const tierRate = Array.isArray(r.RateTierInfo) ? r.RateTierInfo[0]?.Rate : r.RateTierInfo?.Rate; percentage = parseFloat(tierRate ?? "NaN"); } - partsTaxRates[code] = isNaN(percentage) - ? {} - : { - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: percentage / 100 - }; + if (!isNaN(percentage)) { + partsTaxRates[rateType] = { + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: percentage / 100 + }; + } } return partsTaxRates; }; + +/** + * Finds an existing vehicle by shopId and VIN. + * @param {string} shopId - The bodyshop UUID. + * @param {string} v_vin - The vehicle VIN. + * @param {object} logger - The logger instance. + * @returns {Promise} The vehicle ID or null if not found. + */ +const findExistingVehicle = async (shopId, v_vin, logger) => { + if (!v_vin) return null; + + try { + const { vehicles } = await client.request(GET_VEHICLE_BY_SHOP_VIN, { shopid: shopId, v_vin }); + if (vehicles?.length > 0) { + logger.log("parts-vehicle-found", "info", vehicles[0].id, null, { shopid: shopId, v_vin }); + return vehicles[0].id; + } + } catch (err) { + logger.log("parts-vehicle-fetch-failed", "warn", null, null, { error: err }); + } + return null; +}; + /** * Extracts job-related data from the XML request. * @param {object} rq - The VehicleDamageEstimateAddRq object. @@ -116,12 +131,14 @@ const extractJobData = (rq) => { shopId: rq.ShopID || rq.shopId, refClaimNum: rq.RefClaimNum, ciecaid: rq.RqUID || null, - cieca_ttl: parseFloat(rq.Cieca_ttl || 0), + // Pull Cieca_ttl from ClaimInfo per schema/sample + cieca_ttl: parseFloat(ci.Cieca_ttl || 0), cat_no: doc.VendorCode || null, category: doc.DocumentType || null, classType: doc.DocumentStatus || null, comment: doc.Comment || null, - date_exported: doc.TransmitDateTime || null, + // TODO: This causes the job to be read only in the UI + // date_exported: doc.TransmitDateTime || null, asgn_no: asgn.AssignmentNumber || null, asgn_type: asgn.AssignmentType || null, asgn_date: asgn.AssignmentDate || null, @@ -130,23 +147,8 @@ const extractJobData = (rq) => { scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null, clm_no: ci.ClaimNum || null, status: ci.ClaimStatus || null, - policy_no: ci.PolicyInfo?.PolicyNum || null, + policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null, ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0) - // document_id: doc.DocumentID || null, - // bms_version: doc.BMSVer || null, - // reference_info: - // doc.ReferenceInfo?.OtherReferenceInfo?.map((ref) => ({ - // name: ref.OtherReferenceName, - // number: ref.OtherRefNum - // })) || [], - // currency_info: doc.CurrencyInfo - // ? { - // code: doc.CurrencyInfo.CurCode, - // base_code: doc.CurrencyInfo.BaseCurCode, - // rate: parseFloat(doc.CurrencyInfo.CurRate || 0), - // rule: doc.CurrencyInfo.CurConvertRule - // } - // : null }; }; @@ -340,7 +342,8 @@ const extractVehicleData = (rq, shopId) => { const interior = rq.VehicleInfo?.Paint?.Interior || {}; return { shopid: shopId, - v_vin: rq.VehicleInfo?.VINInfo?.VIN?.VINNum || null, + // VIN may be either VINInfo.VINNum or VINInfo.VIN.VINNum depending on producer + v_vin: rq.VehicleInfo?.VINInfo?.VINNum || rq.VehicleInfo?.VINInfo?.VIN?.VINNum || null, plate_no: rq.VehicleInfo?.License?.LicensePlateNum || null, plate_st: rq.VehicleInfo?.License?.LicensePlateStateProvince || null, v_model_yr: desc.ModelYear || null, @@ -363,8 +366,6 @@ const extractVehicleData = (rq, shopId) => { v_makecode: desc.MakeCode || null, trim_color: interior.ColorName || desc.TrimColor || null, db_v_code: desc.DatabaseCode || null - // v_model_num: desc.ModelNum || null - // v_odo: desc.OdometerInfo?.OdometerReading || null }; }; @@ -374,71 +375,154 @@ const extractVehicleData = (rq, shopId) => { * @returns {object[]} Array of job line objects. */ const extractJobLines = (rq) => { - const damageLines = toArray(rq.DamageLineInfo); + // Normalize to array without lodash toArray (which flattens object values incorrectly) + const dl = rq.DamageLineInfo; + const damageLines = Array.isArray(dl) ? dl : dl ? [dl] : []; if (damageLines.length === 0) { - return []; // Or throw if required + return []; } - return damageLines.map((line) => { + const out = []; + + for (const line of damageLines) { const partInfo = line.PartInfo || {}; const laborInfo = line.LaborInfo || {}; + const refinishInfo = line.RefinishLaborInfo || {}; const subletInfo = line.SubletInfo || {}; let jobLineType = "PART"; if (Object.keys(subletInfo).length > 0) jobLineType = "SUBLET"; else if (Object.keys(laborInfo).length > 0 && Object.keys(partInfo).length === 0) jobLineType = "LABOR"; - const jobLine = { + const base = { line_no: parseInt(line.LineNum || 0, 10), unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), status: line.LineStatusCode || null, - line_desc: line.LineDesc || null - // line_type: jobLineType // New field for clarity + line_desc: line.LineDesc || null, + notes: line.LineMemo || null }; if (jobLineType === "PART") { - jobLine.part_type = partInfo.PartType || null; - jobLine.part_qty = parseFloat(partInfo.Quantity || 0); - jobLine.oem_partno = partInfo.OEMPartNum || null; - jobLine.db_price = parseFloat(partInfo.PartPrice || 0); - jobLine.act_price = parseFloat(partInfo.PartPrice || 0); // Or NonOEM if selected + const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); + out.push({ + ...base, + part_type: partInfo.PartType || null, + part_qty: parseFloat(partInfo.Quantity || 0) || 1, + oem_partno: partInfo.OEMPartNum || null, + db_price: price, + act_price: price, + // Labor fields if present on same line + mod_lbr_ty: laborInfo.LaborType || null, + mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0), + lbr_op: laborInfo.LaborOperation || null, + lbr_amt: INCLUDE_LABOR ? parseFloat(laborInfo.LaborAmt || 0) : 0, + // Tax flag from PartInfo.TaxableInd when provided + ...(partInfo.TaxableInd !== undefined + ? { + tax_part: + partInfo.TaxableInd === true || + partInfo.TaxableInd === 1 || + partInfo.TaxableInd === "1" || + (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y") + } + : {}), + // Manual line flag coercion + ...(line.ManualLineInd !== undefined + ? { + manual_line: + line.ManualLineInd === true || + line.ManualLineInd === 1 || + line.ManualLineInd === "1" || + (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") + } + : { manual_line: null }) + }); } else if (jobLineType === "SUBLET") { - jobLine.part_type = "SUB"; // Custom code for sublet - jobLine.part_qty = 1; // Default - jobLine.act_price = parseFloat(subletInfo.SubletAmount || 0); - // jobLine.sublet_vendor = subletInfo.SubletVendorName || null; // TODO: Clarify - // jobLine.lbr_hrs = parseFloat(subletInfo.SubletLaborHours || 0); // TODO: Clarify - } // Labor-only already handled + out.push({ + ...base, + part_type: "PAS", + part_qty: 1, + act_price: parseFloat(subletInfo.SubletAmount || 0), + // Manual line flag + ...(line.ManualLineInd !== undefined + ? { + manual_line: + line.ManualLineInd === true || + line.ManualLineInd === 1 || + line.ManualLineInd === "1" || + (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") + } + : { manual_line: null }) + }); + } else if (INCLUDE_LABOR) { + // Labor-only line (only when enabled) + out.push({ + ...base, + mod_lbr_ty: laborInfo.LaborType || null, + mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0), + lbr_op: laborInfo.LaborOperation || null, + lbr_amt: parseFloat(laborInfo.LaborAmt || 0), + ...(line.ManualLineInd !== undefined + ? { + manual_line: + line.ManualLineInd === true || + line.ManualLineInd === 1 || + line.ManualLineInd === "1" || + (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") + } + : { manual_line: null }) + }); + } - jobLine.mod_lbr_ty = laborInfo.LaborType || null; - jobLine.mod_lb_hrs = parseFloat(laborInfo.LaborHours || 0); - jobLine.lbr_op = laborInfo.LaborOperation || null; - jobLine.lbr_amt = parseFloat(laborInfo.LaborAmt || 0); - jobLine.notes = line.LineMemo || null; - jobLine.manual_line = line.ManualLineInd || null; + // Add a separate refinish labor line if present and enabled + if (INCLUDE_LABOR && Object.keys(refinishInfo).length > 0) { + const hrs = parseFloat(refinishInfo.LaborHours || 0); + const amt = parseFloat(refinishInfo.LaborAmt || 0); + if (!isNaN(hrs) || !isNaN(amt)) { + out.push({ + ...base, + // tweak unq_seq to avoid collisions in later upserts + unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, + line_desc: base.line_desc || "Refinish", + mod_lbr_ty: "LAR", + mod_lb_hrs: isNaN(hrs) ? 0 : hrs, + lbr_op: refinishInfo.LaborOperation || null, + lbr_amt: isNaN(amt) ? 0 : amt, + ...(line.ManualLineInd !== undefined + ? { + manual_line: + line.ManualLineInd === true || + line.ManualLineInd === 1 || + line.ManualLineInd === "1" || + (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y") + } + : { manual_line: null }) + }); + } + } + } - return jobLine; - }); + return out; }; -/** - * Finds an existing vehicle by shopId and VIN. - * @param {string} shopId - The bodyshop UUID. - * @param {string} v_vin - The vehicle VIN. - * @param {object} logger - The logger instance. - * @returns {Promise} The vehicle ID or null if not found. - */ -const findExistingVehicle = async (shopId, v_vin, logger) => { - if (!v_vin) return null; - - try { - const { vehicles } = await client.request(GET_VEHICLE_BY_SHOP_VIN, { shopid: shopId, v_vin }); - if (vehicles?.length > 0) { - logger.log("parts-vehicle-found", "info", vehicles[0].id, null, { shopid: shopId, v_vin }); - return vehicles[0].id; +// 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; + } } - } catch (err) { - logger.log("parts-vehicle-fetch-failed", "warn", null, null, { error: err }); } return null; }; @@ -459,6 +543,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 && 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 (INCLUDE_LABOR && Number.isFinite(jl.lbr_amt)) { + labor += jl.lbr_amt; + } + } + const total = parts + labor; + return Number.isFinite(total) && total > 0 ? total : 0; +}; + /** * Handles the VehicleDamageEstimateAddRq XML request from parts management. * @param {object} req - The HTTP request object. @@ -517,6 +621,10 @@ const vehicleDamageEstimateAddRq = async (req, res) => { 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); @@ -524,6 +632,7 @@ const vehicleDamageEstimateAddRq = async (req, res) => { // Build job input const jobInput = { shopid: shopId, + converted: true, ownerid, ro_number: refClaimNum, ciecaid, @@ -534,7 +643,7 @@ const vehicleDamageEstimateAddRq = async (req, res) => { parts_tax_rates, clm_no, status: status || defaultStatus, - clm_total: cieca_ttl, + clm_total: computedTotal || null, policy_no, ded_amt, comment, @@ -544,12 +653,13 @@ const vehicleDamageEstimateAddRq = async (req, res) => { asgn_date, scheduled_in, scheduled_completion, - ...insuranceData, // Inline insurance data - ...lossInfo, // Inline loss information - ...ownerData, // Inline owner data - ...estimatorData, // Inline estimator data - ...adjusterData, // Inline adjuster data - ...repairFacilityData, // Inline repair facility data + // 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, diff --git a/server/integrations/partsManagement/sampleData/schemaData.md b/server/integrations/partsManagement/sampleData/schemaData.md new file mode 100644 index 000000000..3e6a12442 --- /dev/null +++ b/server/integrations/partsManagement/sampleData/schemaData.md @@ -0,0 +1,431 @@ +Awesome — thanks for the dumps. I pulled the structures directly from the XSDs you uploaded and +focused on **`VehicleDamageEstimateAddRq`** and the graph of types it depends on. Below is a +developer-grade map you can hand to a coding agent. + +--- + +# What it is & where it lives + +* **Global element**: `VehicleDamageEstimateAddRq` +* **Namespace**: `http://www.cieca.com/BMS` (default ns in your files) +* **Defined in**: `BMSEstimateMessages_2024R1_V6.9.0.xsd` +* **Type**: `EstimateRqType` (from `BMSEstimateCommonTypes_2024R1_V6.9.0.xsd`) +* **Service group** (where this message is accepted): `EstimateService` in + `BMSEstimateService_2024R1_V6.9.0.xsd` + Includes: `PropertyDamageEstimateAddRq/Rs`, `VehicleDamageEstimateAddRq/Rs`, + `VehicleDamageEstimateChgRq/Rs`, `VehicleDamagePhotoEstimateAddRq/Rs`. + +--- + +# Top-level schema (for `VehicleDamageEstimateAddRq` → `EstimateRqType`) + +`EstimateRqType` **extends** `MessageHeaderType` (from `BMSCommonGlobalTypes_2024R1_V6.9.0.xsd`) and +then adds the following sequence. I’ve marked **required** vs *optional* and multiplicity: + +**Header (inherited from `MessageHeaderType`):** + +* **`RqUID`** (UUID) — **required** +* `AsyncRqUID` (UUID) — *optional* +* `PartnerKey` (Identifier) — *optional* + +**Body (from `EstimateRqType`):** + +* `SvcProviderName` (Identifier) — *optional* +* `RefClaimNum` (Char\_50) — *optional* +* **`DocumentInfo`** (`DocumentInfoType`) — **required, 1** +* **`ApplicationInfo`** (`ApplicationInfoType`) — **required, 1..**\* +* `EventInfo` (`EventInfoType`) — *optional* +* **`AdminInfo`** (`AdminInfoType`) — **required** +* **`EstimatorIDs`** (`EstimatorIDsTypeType`) — **required** +* `ClaimInfo` (`ClaimInfoType`) — *optional* +* **`VehicleInfo`** (`VehicleInfoType`) **OR** `PropertyInfo` (`PropertyInfoType`) — **choice** → + for vehicle, use **`VehicleInfo`** +* **`ProfileInfo`** (`ProfileInfoType`) — **required** +* **`DamageLineInfo`** (`DamageLineInfoType`) — **required, 1..**\* (line items) +* `CalibrationInfo` (`CalibrationInfoType`) — *optional, 0..* +* `ScanInfo` (`ScanInfoType`) — *optional, 0..* +* `FileAttachment` (`FileAttachmentType`) — *optional* +* `NonNewOEMPartInd` (Boolean) — *optional* +* `StorageDuration` (Integer\_Range\_0-999) — *optional* +* **`RepairTotalsInfo`** (`RepairTotalsInfoType`) — **required, 1..**\* +* `RepairTotalsHistory` (`RepairTotalsHistoryType`) — *optional, 0..* +* `PaymentInfo` (`PaymentInfoType`) — *optional* +* `EstimateMemo` (C) — *optional* +* `AdministrativeMemo` (C) — *optional* +* `Disclaimers` (C) — *optional* +* `CustomMemo` (C) — *optional* +* `CustomPrintImage` (C) — *optional* +* `OtherMemos` (`OtherMemosType`) — *optional, 0..* + +**Files involved:** +`BMSEstimateMessages_2024R1_V6.9.0.xsd`, `BMSEstimateCommonTypes_2024R1_V6.9.0.xsd`, +`BMSCommonGlobalTypes_2024R1_V6.9.0.xsd`, `BMSSimpleTypes_2024R1_V6.9.0.xsd` + code lists XSDs for +enums. + +--- + +# Key dependent types (immediate children you’ll actually populate) + +Below are the **first-level** structures you’ll typically use. I’ve trimmed to the practical fields; +each type has many optional parties and details you can ignore for a minimal AddRq. + +## `DocumentInfoType` (BMSCommonGlobalTypes) + +Typical header metadata: + +* **`BMSVer`** (`BMSVersionClosedEnumType`) — e.g. **`6.9.0`** +* **`DocumentType`** (`DocumentTypeClosedEnumType`) — code for message family (e.g. `E` for + estimate; codelists provide the letter codes) +* `DocumentSubType` (`DocumentSubTypeClosedEnumType`) — e.g. “Original Estimate”, “Copy”, etc. +* `DocumentID` (Char\_50) — your ID +* `VendorCode` (VendorCodeOpenEnumType) — optional +* `DocumentVer` (`DocumentVerType`) — versioning container (0..\*) +* **`CreateDateTime`** (DateTime) +* `TransmitDateTime` (DateTime) +* `ReferenceInfo` (`RefInfoType`) — links to prior docs +* `CountryCode`, `CurrencyInfo`, `CultureCode` — optional locale bits + +## `ApplicationInfoType` (BMSCommonGlobalTypes) **(1..\*)** + +* **`ApplicationType`** (`ApplicationTypeClosedEnumType`) — e.g., Estimator, Shop Mgmt, etc. +* **`ApplicationName`** (Char\_30) +* **`ApplicationVer`** (Char\_12) +* `DatabaseVer` (Char\_12) +* `DatabaseDateTime` (DateTime) + +## `AdminInfoType` (BMSCommonGlobalTypes) + +Large party/role roster; **all child elements are optional**, but the container itself is required. +Common ones: + +* `InsuranceCompany` (`InsuranceCompanyType`) +* `PolicyHolder` (`PolicyHolderType`) +* `Insured` / `Owner` / `Customer` (`GenericPartyType`) +* `Claimant` (`ClaimantType`) +* `Estimator` (0..\*) (`EstimatorType`) +* `RepairFacility` (`RepairFacilityType`) +* `RentalProvider`, `TowCompany`, `Lender`, `Lienholder` (0..\*), etc. + (You can send `` if you don’t need parties; it validates.) + +## `EstimatorIDsTypeType` (BMSEstimateCommonTypes) + +* `OriginalEstimatorID` (Char\_40) — optional +* `EstimatorHistory` (0..\*) → `EstimatorHistoryType` ⇒ (`DocumentVerCode`, `DocumentVerNum`) + +## `ClaimInfoType` (BMSCommonGlobalTypes) *(optional)* + +* `ClaimNum` (Char\_50) +* `PolicyInfo` (0..\*) (`PolicyInfoType`) +* `LossInfo` (`LossInfoType`) — details on loss/time/location/coverage +* `AdditionalIDInfo` (0..\*) (`IDInfoType`) +* `ClaimStatus`, `PreviousPaymentAmt`, `ClaimMemo`, etc. + +## `VehicleInfoType` (BMSCommonGlobalTypes) *(choose this over PropertyInfo)* + +* `VINInfo` (0..\*) (`VINInfoType`) → **choice** of `VINAvailabilityCode` or one or more `VIN` ( + `VINType`) +* `License` (`LicenseType`) +* `VehicleDesc` (`VehicleDescType`) — **ModelYear**, **MakeDesc/MakeCode**, **ModelName/ModelNum**, + `VehicleType`, etc. +* `Paint`, `Body`, `Powertrain`, `Condition`, `Valuation`, `VehicleMemo` +* `PolicyVehicleNum`, `LossVehicleNum` +* `FileAttachment` (`FileAttachmentType`) +* `CustomElement` (0..\*) +* `UnitNum` (Char\_20) + +> Note: `VINType` is referenced but its concrete restriction is provided elsewhere in BMS; you can +> treat it as a VIN string (17-char typical) and your validator will enforce the real facet. + +## `ProfileInfoType` (BMSEstimateCommonTypes) **required** + +Controls rates, tax, and rules used to compute totals: + +* `ProfileName` (Char\_40) +* **`RateInfo`** (1..\*) (`RateInfoType`) + + * `RateType` (`RateTypeClosedEnumType`) — e.g., BODY\_LABOR, PAINT\_LABOR, MECHANICAL\_LABOR, + MATERIAL, etc. + * `RateTierInfo` / `RateTierHistory` (0..\*) + * `TaxableInd`, `TaxRate`, `AdjustmentInfo` (0..*), `TaxInfo` (0..*) + * `MaterialCalcSettings` (optional) +* `AlternatePartInfo` (0..*), `PartCertification` (0..*), `RefinishCalcSettings`, + `PreTaxDiscountRate`, `TaxExemptInfo` (0..\*), `CanadianTax` (for CA specifics) + +## `DamageLineInfoType` (BMSEstimateCommonTypes) **1..**\* + +One per estimate line. Core children: + +* `LineNum`, `UniqueSequenceNum`, `ParentLineNum` (hierarchy) +* `ManualLineInd`, `AutomatedEntry`, `LineStatusCode` +* `LineDesc`, `LineDescCode` +* `SubletInfo` (`SubletInfoType`) +* `PartInfo` (0..\*) (`PartInfoType`) +* `LaborInfo` (`LaborInfoType`) +* `RefinishLaborInfo` (`LaborInfoType`) +* `MaterialType`, `OtherChargesInfo`, `WhoPays` +* `LineAdjustment`, `AppliedAdjustment` +* `PDRInfo`, `LineType`, `LineMemo`, `VendorRefNum` (0..\*) + +**`PartInfoType`** highlights: + +* `PartMaterialCode`, `PartType`, `LineItemCategoryCode` +* `PartDesc`, `PartNum`, `OEMPartNum` +* `NonOEM` (0..\*) (`NonOEMType`) — alternate sources/quality +* `ListPrice`, `PartPrice`, `UnitPartPrice`, `TotalPartPrice`, `OEMPartPrice` +* `PriceAdjustment` (0..\*) (`PriceAdjustmentType`) +* `TaxableInd`, `AppliedTaxes` +* `CertificationType` (0..\*), `AlternatePartInd`, `GlassPartInd` +* `Quantity`, `PartStatus`, `Dimensions`, `Glass*`, `QuotedPartList` … + +**`LaborInfoType`** highlights: + +* **`LaborType`** (`LaborTypeClosedEnumType`) — **required** +* `LaborOperation`, `LaborHours`, `LaborHourlyRate`, `LaborAmt` +* `DatabaseLaborType/Hours/Amt` +* `LaborAdjustment` (0..\*) +* Judgment/flags (e.g., `LaborAmtJudgmentInd`, `OverlapInd`) +* Paint-specific fields (`PaintStagesNum`, `PaintTonesNum`) +* `AssemblyLaborCode` + +## `CalibrationInfoType` / `ScanInfoType` (BMSEstimateCommonTypes) + +* **`ScanInfoType`**: `ScanDetailsList` (optional), `FileAttachment` (optional), `ScanTool`, + `ScanDateTime` (**required**), flags `CleanScanInd`, `FollowUpInd`, plus `Technician`. +* **`CalibrationInfoType`**: optional lists for details & technicians, plus process flags ( + `PrerequisitesMetInd`, `ProceduresFollowedInd`, `ADASReviewedWithOwnerInd`). + +## `FileAttachmentType` (BMSCommonGlobalTypes) + +* `DocAttachment` (0..\*) (`DocAttachmentType`) + + * `AttachmentType` (open enum) + * `AttachmentTitle` **or** `AttachmentMemo` + * `AttachmentFileType`, `AttachmentFileName`, `AttachmentLength` + * **One of:** `AttachmentURI` **or** `EmbeddedAttachmentType` + + * `EmbeddedAttachmentType` → **choice**: `EmbeddedAttachment` (Binary) **or** + `EmbeddedAttachmentText` (C) + * `AttachmentIntegrity` (0..\*) (optionally includes Binary integrity blobs) + * `AttachmentStatusCode` (open enum) + +## `RepairTotalsInfoType` (BMSEstimateCommonTypes) **1..**\* + +* `LaborTotalsInfo` (0..\*) (`TotalsInfoType`) +* `PartsTotalsInfo` (0..\*) (`TotalsInfoType`) +* `OtherChargesTotalsInfo` (0..\*) (`TotalsInfoType`) +* `NumOfDamageLines` (optional) +* **`SummaryTotalsInfo`** (1..\*) (`TotalsInfoType`) — your rolled-up totals +* `RepairTotalsType` (`LineTypeClosedEnumType`) — optional (e.g., gross vs. customer-pay segments) + +**`TotalsInfoType`** (BMSCommonGlobalTypes) highlights: + +* **`TotalType`** (`TotalTypeOpenEnumType`) — category (e.g., LABOR, PARTS, TAX, GRAND\_TOTAL,…) +* `TotalSubType` (open enum) +* **`TotalTypeDesc`** (Char\_30) +* Hours quantities & units, item quantity, unit price +* Detailed `TotalTaxInfo` / `TotalAdjustmentInfo` (0..\*) +* Amounts: `NonTaxableAmt`, `TaxableAmt`, `TaxTotalAmt`, `OtherCharges*`, **`TotalAmt`**, + `TotalPct`, `TotalCost` +* `AmtDueInfo` (0..\*) + +## `RepairTotalsHistoryType` (BMSEstimateCommonTypes) + +* Version stamp and one or more `HistoryTotalsInfo` entries. + +## `PaymentInfoType` (BMSCommonGlobalTypes) *(optional)* + +* `PayerType`, `PaymentType` +* `Payee`/`PayerInfo`/`PayeeInfo` +* `PaymentDateTime`, **`PaymentAmt`** +* `PaymentID`, `PaymentMemo`, `PaymentAmtType` + +## `OtherMemosType` (BMSCommonGlobalTypes) + +* `OtherMemoRef` (open enum), `OtherMemo` (C) + +--- + +# Minimal, schema-valid XML skeleton (vehicle path) + +> Uses only **required** containers/fields; values shown as **PLACEHOLDER**. +> You must add at least one **DamageLineInfo** and one **SummaryTotalsInfo** item, and at least one +**RateInfo** inside **ProfileInfo**. +> Enumerations are *code lists*; use valid codes from your system. + +```xml + + + + 00000000-0000-0000-0000-000000000000 + + + + 6.9.0 + E + 2025-08-14T12:00:00Z + + + + INSERT_APP_TYPE + INSERT_APP_NAME + INSERT_APP_VER + + + + + + + + + + + + + + + + INSERT_RATE_TYPE + + + + + + + + + INSERT_LABOR_TYPE + + + + + + + + INSERT_TOTAL_TYPE + Grand Total + 0.00 + + + +``` + +--- + +# Implementation notes & gotchas (important) + +1. **Required containers vs. required content** + +* `AdminInfo` and `EstimatorIDs` are **required containers** but their **children are optional**. + Empty elements validate. +* `ProfileInfo` is required and must include **≥1 `RateInfo`** with a `RateType`. +* You must include the **choice** of **`VehicleInfo`** (for this message) instead of `PropertyInfo`. +* Include **≥1 `DamageLineInfo`** and **≥1 `RepairTotalsInfo`** each containing * + *≥1 `SummaryTotalsInfo`**. + +2. **Header** + +* `RqUID` is required; use a real UUID. + +3. **Enumerations / code lists** + +* Many fields are `ClosedEnumType`/`OpenEnumType` and validated against the BMS code list XSDs you + included (e.g., `BMSCodeLists_*.xsd`). Use the exact code values your trading partner expects ( + e.g., `DocumentType` = `E` for estimates). +* `BMSVer` supports `6.9.0`. + +4. **Line hierarchy** + +* For nested kits/assemblies, use `ParentLineNum`; `UniqueSequenceNum` helps ordering. `LineType` + can label grouping (e.g., Sublet, Labor, Part, etc.). + +5. **Attachments** + +* You can embed binary (`EmbeddedAttachmentType/EmbeddedAttachment`) **or** provide a URI ( + `AttachmentURI`). Provide `AttachmentFileType` and `AttachmentFileName` either way. + +6. **Scans & calibrations** + +* If you include `ScanInfo`, it **requires** `ScanTool` and `ScanDateTime`. Calibrations are + optional but provide strong ADAS traceability. + +7. **Totals integrity** + +* `RepairTotalsInfo/SummaryTotalsInfo` acts as your roll-up. Ensure it reconciles with the sum of + `DamageLineInfo` components and the profile’s rates/taxes so consumers don’t reject on mismatches. + +8. **Currency / numeric facets** + +* Monetary fields use `Currency`. Hours/rates/quantities have explicit facets (e.g., + `Decimal_Range_-999.9-999.9`). Stay within ranges. + +9. **Canada specifics** + +* `DocumentInfo/CountryCode` = `CA`, and `ProfileInfo/CanadianTax` is available for PST/HST/GST + modeling if you need to encode tax policy explicitly. + +--- + +# Quick field checklist for a typical *valid* “vehicle add” you’ll generate + +* **Header** + + * `RqUID` ✅ + +* **Doc header** + + * `DocumentInfo/BMSVer` = `6.9.0` ✅ + * `DocumentInfo/DocumentType` = `E` ✅ + * `DocumentInfo/CreateDateTime` ✅ + +* **App** + + * `ApplicationInfo[1..*]/(ApplicationType, ApplicationName, ApplicationVer)` ✅ + +* **Admin** + + * `` (or populate parties) ✅ + +* **EstimatorIDs** + + * `` (or add contents) ✅ + +* **Vehicle** + + * `VehicleInfo` (VIN + YMM recommended) ✅ + +* **Profile & rates** + + * `ProfileInfo/RateInfo[1..*]/RateType` ✅ + +* **Lines** + + * `DamageLineInfo[1..*]` with at least one `LaborInfo/LaborType` or `PartInfo` ✅ + +* **Totals** + + * `RepairTotalsInfo[1..*]/SummaryTotalsInfo[1..*]/(TotalType, TotalTypeDesc, TotalAmt)` ✅ + +--- + +# Pointers to definitions in your bundle (for traceability) + +* `VehicleDamageEstimateAddRq` element → `BMSEstimateMessages_2024R1_V6.9.0.xsd` +* `EstimateRqType` → `BMSEstimateCommonTypes_2024R1_V6.9.0.xsd` +* `MessageHeaderType`, `DocumentInfoType`, `VehicleInfoType`, `FileAttachmentType`, + `PaymentInfoType`, etc. → `BMSCommonGlobalTypes_2024R1_V6.9.0.xsd` +* Rates/lines/totals/calibration/scan subtypes → mostly `BMSEstimateCommonTypes_2024R1_V6.9.0.xsd` +* Enums/code lists → `BMSCodeLists_ClassicCode_2024R1_V6.9.0.xsd`, + `BMSCodeLists_CodeExt_2024R1_V6.9.0.xsd` +* Service wrapper (which messages are valid to send/receive) → + `BMSEstimateService_2024R1_V6.9.0.xsd` + +---