From c586d0283bb8390ad700e2a1fc224e380d9f0020 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 18 Aug 2025 12:42:06 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - Beef Up Change Request Parser, add Change Request documentation data --- .../endpoints/lib/extractPartsTaxRates.js | 66 +++++ .../endpoints/vehicleDamageEstimateAddRq.js | 66 +---- .../endpoints/vehicleDamageEstimateChgRq.js | 177 ++++++++++--- .../{schemaData.md => schemaDataAdd.md} | 0 .../sampleData/schemaDataChange.md | 250 ++++++++++++++++++ 5 files changed, 457 insertions(+), 102 deletions(-) create mode 100644 server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js rename server/integrations/partsManagement/sampleData/{schemaData.md => schemaDataAdd.md} (100%) create mode 100644 server/integrations/partsManagement/sampleData/schemaDataChange.md diff --git a/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js b/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js new file mode 100644 index 000000000..f5e4cb757 --- /dev/null +++ b/server/integrations/partsManagement/endpoints/lib/extractPartsTaxRates.js @@ -0,0 +1,66 @@ +const KNOWN_PART_RATE_TYPES = [ + "PAA", + "PAC", + "PAG", + "PAL", + "PAM", + "PAN", + "PAO", + "PAP", + "PAR", + "PAS", + "PASL", + "CCC", + "CCD", + "CCF", + "CCM", + "CCDR" +]; + +/** + * Extracts and processes parts tax rates from profile info. + * @param {object} profile - The ProfileInfo object from XML. + * @returns {object} The parts tax rates object. + */ + +const extractPartsTaxRates = (profile = {}) => { + const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; + const partsTaxRates = {}; + + 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; + + const taxInfo = r.TaxInfo; + const taxTier = taxInfo?.TaxTierInfo; + let percentage = parseFloat(taxTier?.Percentage ?? "NaN"); + + if (isNaN(percentage)) { + const tierRate = Array.isArray(r.RateTierInfo) ? r.RateTierInfo[0]?.Rate : r.RateTierInfo?.Rate; + percentage = parseFloat(tierRate ?? "NaN"); + } + + 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; +}; + +module.exports = { + extractPartsTaxRates, + KNOWN_PART_RATE_TYPES +}; diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index 90b175dec..4efa59f89 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -2,6 +2,7 @@ // CamelCase is used for GraphQL and database fields. const client = require("../../../graphql-client/graphql-client").client; +const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); // GraphQL Queries and Mutations @@ -15,27 +16,7 @@ const { // Defaults const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; -// Known part rate types for tax rates -const KNOWN_PART_RATE_TYPES = [ - "PAA", - "PAC", - "PAG", - "PAL", - "PAM", - "PAN", - "PAO", - "PAP", - "PAR", - "PAS", - "PASL", - "CCC", - "CCD", - "CCF", - "CCM", - "CCDR" -]; - -// Config: include labor lines and labor in totals (default false for development ease) +// Config: include labor lines and labor in totals (default true) const INCLUDE_LABOR = true; /** * Fetches the default order status for a bodyshop. @@ -52,49 +33,6 @@ const getDefaultOrderStatus = async (shopId, logger) => { return FALLBACK_DEFAULT_ORDER_STATUS; } }; - -/** - * Extracts and processes parts tax rates from profile info. - * @param {object} profile - The ProfileInfo object from XML. - * @returns {object} The parts tax rates object. - */ -const extractPartsTaxRates = (profile = {}) => { - const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}]; - const partsTaxRates = {}; - - 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; - - const taxInfo = r.TaxInfo; - const taxTier = taxInfo?.TaxTierInfo; - let percentage = parseFloat(taxTier?.Percentage ?? "NaN"); - - if (isNaN(percentage)) { - const tierRate = Array.isArray(r.RateTierInfo) ? r.RateTierInfo[0]?.Rate : r.RateTierInfo?.Rate; - percentage = parseFloat(tierRate ?? "NaN"); - } - - 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. diff --git a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js index e9e6ea93f..d38d0a162 100644 --- a/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js @@ -3,12 +3,13 @@ const client = require("../../../graphql-client/graphql-client").client; const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); +const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); const { GET_JOB_BY_CLAIM, UPDATE_JOB_BY_ID, - UPSERT_JOBLINES, - DELETE_JOBLINES_BY_IDS + DELETE_JOBLINES_BY_IDS, + INSERT_JOBLINES } = require("../partsManagement.queries"); /** @@ -37,52 +38,143 @@ const extractUpdatedJobData = (rq) => { const doc = rq.DocumentInfo || {}; const claim = rq.ClaimInfo || {}; - return { + 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, - policy_no: claim.PolicyInfo?.PolicyNum || null + policy_no: policyNo }; + + // If ProfileInfo provided in ChangeRq, update parts_tax_rates to stay in sync with AddRq behavior + if (rq.ProfileInfo) { + out.parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo); + } + + return out; }; /** - * Extracts updated job lines from the request payload. - * @param addsChgs - * @param jobId - * @returns {{jobid: *, line_no: number, unq_seq: number, status, line_desc, part_type, part_qty: number, oem_partno, db_price: number, act_price: number, mod_lbr_ty, mod_lb_hrs: number, lbr_op, lbr_amt: number, notes, manual_line: boolean}[]} + * Extracts updated job lines from the request payload, mirroring the AddRq splitting rules: + * - PART lines carry only part pricing (act_price) and related fields + * - If LaborInfo exists on a part line, add a separate LABOR line at unq_seq + 400000 + * - If RefinishLaborInfo exists, add a separate LABOR line at unq_seq + 500000 with mod_lbr_ty=LAR + * - SUBLET lines become PAS part_type with act_price=SubletAmount */ const extractUpdatedJobLines = (addsChgs = {}, jobId) => { - const lines = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || []]; + const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}]; - return lines.map((line) => ({ - jobid: jobId, - line_no: parseInt(line.LineNum, 10), - unq_seq: parseInt(line.UniqueSequenceNum, 10), - status: line.LineStatusCode || null, - line_desc: line.LineDesc || null, - part_type: line.PartInfo?.PartType || null, - part_qty: parseFloat(line.PartInfo?.Quantity || 0), - oem_partno: line.PartInfo?.OEMPartNum || null, - db_price: parseFloat(line.PartInfo?.PartPrice || 0), - act_price: parseFloat(line.PartInfo?.PartPrice || 0), - 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), - notes: line.LineMemo || null, - manual_line: line.ManualLineInd === "1" - })); + const coerceManual = (val) => + val === true || val === 1 || val === "1" || (typeof val === "string" && val.toUpperCase() === "Y"); + + const out = []; + + for (const line of linesIn) { + if (!line || Object.keys(line).length === 0) continue; + + const partInfo = line.PartInfo || {}; + const laborInfo = line.LaborInfo || {}; + const refinishInfo = line.RefinishLaborInfo || {}; + const subletInfo = line.SubletInfo || {}; + + const base = { + jobid: jobId, + line_no: parseInt(line.LineNum || 0, 10), + unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), + status: line.LineStatusCode || null, + line_desc: line.LineDesc || null, + notes: line.LineMemo || null, + manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null + }; + + const hasPart = Object.keys(partInfo).length > 0; + const hasLaborOnly = Object.keys(laborInfo).length > 0 && !hasPart && Object.keys(subletInfo).length === 0; + const hasSublet = Object.keys(subletInfo).length > 0; + + if (hasPart) { + const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); + out.push({ + ...base, + part_type: partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null, + part_qty: parseFloat(partInfo.Quantity || 0) || 1, + oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null, + db_price: isNaN(price) ? 0 : price, + act_price: isNaN(price) ? 0 : price + }); + + // Split any attached labor on the part line into a derived labor jobline + const hrs = parseFloat(laborInfo.LaborHours || 0); + const amt = parseFloat(laborInfo.LaborAmt || 0); + const hasLabor = + (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || + (!isNaN(hrs) && hrs !== 0) || + (!isNaN(amt) && amt !== 0); + if (hasLabor) { + out.push({ + ...base, + unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000, + mod_lbr_ty: laborInfo.LaborType || null, + mod_lb_hrs: isNaN(hrs) ? 0 : hrs, + lbr_op: laborInfo.LaborOperation || null, + lbr_amt: isNaN(amt) ? 0 : amt + }); + } + } else if (hasSublet) { + out.push({ + ...base, + part_type: "PAS", + part_qty: 1, + act_price: parseFloat(subletInfo.SubletAmount || 0) || 0 + }); + } + + // Labor-only line (no PartInfo): still upsert as a labor entry + if (hasLaborOnly) { + out.push({ + ...base, + mod_lbr_ty: laborInfo.LaborType || null, + mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0) || 0, + lbr_op: laborInfo.LaborOperation || null, + lbr_amt: parseFloat(laborInfo.LaborAmt || 0) || 0 + }); + } + + // Separate refinish labor line + if (Object.keys(refinishInfo).length > 0) { + const rHrs = parseFloat(refinishInfo.LaborHours || 0); + const rAmt = parseFloat(refinishInfo.LaborAmt || 0); + if (!isNaN(rHrs) || !isNaN(rAmt)) { + out.push({ + ...base, + unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, + line_desc: base.line_desc || "Refinish", + mod_lbr_ty: "LAR", + mod_lb_hrs: isNaN(rHrs) ? 0 : rHrs, + lbr_op: refinishInfo.LaborOperation || null, + lbr_amt: isNaN(rAmt) ? 0 : rAmt + }); + } + } + } + + return out; }; /** - * Extracts deletion IDs from the deletions object. - * @param deletions - * @returns {number[]} + * Extracts deletion IDs from the deletions object, also removing any derived labor/refinish lines + * by including offsets (base + 400000, base + 500000). */ const extractDeletions = (deletions = {}) => { - const lines = Array.isArray(deletions.DamageLineInfo) ? deletions.DamageLineInfo : [deletions.DamageLineInfo || []]; + const items = Array.isArray(deletions.DamageLineInfo) ? deletions.DamageLineInfo : [deletions.DamageLineInfo || {}]; + const baseSeqs = items.map((line) => parseInt(line.UniqueSequenceNum, 10)).filter((id) => Number.isInteger(id)); - return lines.map((line) => parseInt(line.UniqueSequenceNum, 10)).filter((id) => !isNaN(id)); + const allSeqs = []; + for (const u of baseSeqs) { + allSeqs.push(u, u + 400000, u + 500000); + } + // De-dup + return Array.from(new Set(allSeqs)); }; /** @@ -113,14 +205,23 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => { await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData }); - if (updatedLines.length > 0) { - await client.request(UPSERT_JOBLINES, { - joblines: updatedLines - }); + // Build a set of unq_seq that will be updated (replaced). We delete them first to avoid duplicates. + const updatedSeqs = Array.from( + new Set((updatedLines || []).map((l) => l && l.unq_seq).filter((v) => Number.isInteger(v))) + ); + + if ((deletedLineIds && deletedLineIds.length) || (updatedSeqs && updatedSeqs.length)) { + const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])])); + if (allToDelete.length) { + await client.request(DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete }); + } } - if (deletedLineIds.length > 0) { - await client.request(DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: deletedLineIds }); + if (updatedLines.length > 0) { + // Insert fresh versions after deletion so we don’t depend on a unique constraint + await client.request(INSERT_JOBLINES, { + joblines: updatedLines + }); } logger.log("parts-job-changed", "info", job.id, null); diff --git a/server/integrations/partsManagement/sampleData/schemaData.md b/server/integrations/partsManagement/sampleData/schemaDataAdd.md similarity index 100% rename from server/integrations/partsManagement/sampleData/schemaData.md rename to server/integrations/partsManagement/sampleData/schemaDataAdd.md diff --git a/server/integrations/partsManagement/sampleData/schemaDataChange.md b/server/integrations/partsManagement/sampleData/schemaDataChange.md new file mode 100644 index 000000000..f47ba5ad4 --- /dev/null +++ b/server/integrations/partsManagement/sampleData/schemaDataChange.md @@ -0,0 +1,250 @@ +You got it—here’s the same style of breakdown for **`VehicleDamageEstimateChgRq`** (the *change +request* variant). I pulled this straight from your XSD set and focused on what differs from +`…AddRq`, what’s required vs optional, and what a minimal-but-valid payload looks like. + +--- + +# What it is & where it lives + +* **Global element**: `VehicleDamageEstimateChgRq` +* **Namespace**: `http://www.cieca.com/BMS` +* **Defined in**: `BMSEstimateMessages_2024R1_V6.9.0.xsd` +* **Type**: `EstimateChgRqType` (declared in `BMSEstimateCommonTypes_2024R1_V6.9.0.xsd`) +* **Service group**: `EstimateService` from `BMSEstimateService_2024R1_V6.9.0.xsd` + Group includes: `PropertyDamageEstimateAddRq/Rs`, `VehicleDamageEstimateAddRq/Rs`, * + *`VehicleDamageEstimateChgRq/Rs`**, `VehicleDamagePhotoEstimateAddRq/Rs`. + +--- + +# Top-level schema (for `VehicleDamageEstimateChgRq` → `EstimateChgRqType`) + +`EstimateChgRqType` **extends** `MessageHeaderType` (same header as `…AddRq`) but the **body is +almost entirely optional** (intended to send only what’s changing). Only **`DocumentInfo`** is +required. + +**Header (inherited from `MessageHeaderType`):** + +* **`RqUID`** (UUID) — **required** +* `AsyncRqUID` (UUID) — *optional* +* `PartnerKey` (Identifier) — *optional* + +**Body (from `EstimateChgRqType`):** + +* `SvcProviderName` (Identifier) — *optional* +* `RefClaimNum` (Char\_50) — *optional* +* **`DocumentInfo`** (`DocumentInfoType`) — **required** +* `ApplicationInfo` (`ApplicationInfoType`) — *optional, 0..*\*\* +* `EventInfo` (`EventInfoType`) — *optional* +* `AdminInfo` (`AdminInfoType`) — *optional* +* `EstimatorIDs` (`EstimatorIDsTypeType`) — *optional* +* `ClaimInfo` (`ClaimInfoType`) — *optional* +* **Choice** — *both optional*: + + * `VehicleInfo` (`VehicleInfoType`) — *optional* + * `PropertyInfo` (`PropertyInfoType`) — *optional* +* `ProfileInfo` (`ProfileInfoType`) — *optional* +* `DamageLineInfo` (`DamageLineInfoType`) — *optional, 0..*\*\* (send only changed/affected lines) +* `NonNewOEMPartInd` (Boolean) — *optional* +* `StorageDuration` (Integer\_Range\_0-999) — *optional* +* `RepairTotalsInfo` (`RepairTotalsInfoType`) — *optional, 0..*\*\* +* `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..*\*\* + +**Key deltas vs `VehicleDamageEstimateAddRq`:** + +* `…AddRq` *requires* several containers (`AdminInfo`, `EstimatorIDs`, `ProfileInfo`, + `DamageLineInfo`, `RepairTotalsInfo` with `SummaryTotalsInfo`, etc.). +* `…ChgRq` **only requires** `MessageHeaderType/RqUID` and **`DocumentInfo`**; everything else is + optional so you can send *just what changed*. +* `CalibrationInfo` and `ScanInfo` (present in `…AddRq`) are **not** present in `EstimateChgRqType`. +* Because almost everything is optional, **the burden is on you** to correctly identify the target + document/version in `DocumentInfo` (or via `ReferenceInfo`) and to include all fields necessary + for the receiver to apply your changes. + +--- + +# Important dependent types (same as Add, but optional here) + +* **`DocumentInfoType`** (BMSCommonGlobalTypes) — **required** + + * Use this to identify *which* estimate you’re changing. Typical: + + * **`BMSVer`** = `6.9.0` + * **`DocumentType`** = `E` (estimate) + * `DocumentID` — your estimate ID + * `CreateDateTime` — when you formed this change message + * `ReferenceInfo` — link back to the prior/authoritative doc (e.g., original `DocumentID`/ + `DocumentVer`), if your workflow uses references + * `DocumentVer` — version info list, if you lifecycle versions +* **`ApplicationInfoType`** — software fingerprint (optional, 0..\*) +* **`AdminInfoType`** — parties/roles (optional) +* **`EstimatorIDsTypeType`** — supplemental estimator IDs/history (optional) +* **`ClaimInfoType`** — claim-level data (optional) +* **`VehicleInfoType`** (or `PropertyInfoType`) — vehicle path stays under `VehicleInfo` (optional) +* **`ProfileInfoType`** — rates/taxes/rules (optional) +* **`DamageLineInfoType`** — **send changed/added/removed lines only** (your trading partner may + require specific flags/LineStatusCode or use `ParentLineNum`+`UniqueSequenceNum` to identify + updates) +* **`RepairTotalsInfoType`** — updated totals (optional; some partners expect totals to reconcile + with changed lines) +* **`PaymentInfoType`**, memos, custom print/image & `OtherMemos` — all optional + +> Because `ChgRq` is sparse by design, **schema validation won’t catch semantic issues** (e.g., you +> remove a part but don’t update totals). Make sure your payload is self-consistent per partner +> rules. + +--- + +# Minimal, schema-valid XML skeleton (change request) + +> This represents the *absolute floor* to validate: **Header/RqUID** + **DocumentInfo** with basic +> fields. In practice, include `DocumentID` and some way to reference the prior document/version so +> the receiver can apply changes. + +```xml + + + + 00000000-0000-0000-0000-000000000000 + + + + 6.9.0 + E + 2025-08-14T12:00:00Z + + + + + + + + + + + + + + + + +``` + +--- + +# Practical guidance & gotchas + +1. **Targeting the right document/version** + +* `DocumentInfo/DocumentID` + `DocumentVer` and/or `ReferenceInfo` should point unambiguously to the + estimate being changed. This is essential because the schema does **not** include a separate + “ChangeTarget” field—partners expect this info in `DocumentInfo`/`ReferenceInfo`. + +2. **Sparsity vs completeness** + +* You can send just the changed sections (e.g., one `DamageLineInfo`, one `RateInfo`). +* Some receivers require you to **also** include reconciled `RepairTotalsInfo/SummaryTotalsInfo`. + Check partner specs. + +3. **Line identity** + +* If you’re updating an existing line, keep its identity stable using `LineNum` and/or + `UniqueSequenceNum`. +* For nested structures, preserve `ParentLineNum`. Use `LineStatusCode` if your partner requires + explicit “added/changed/deleted” flags. + +4. **Profile impacts** + +* If a change affects pricing (rates, taxes, discounts), update `ProfileInfo` (and possibly totals). + Omitting totals may be acceptable for some partners; others will reject mismatches. + +5. **What’s *not* in ChgRq vs AddRq** + +* `CalibrationInfo` and `ScanInfo` do not appear in `EstimateChgRqType`. If you need to change those + data, partner workflows may expect a re-send under Add/PhotoAdd or a separate message + family—confirm externally. + +6. **Header is still mandatory** + +* `RqUID` must be a real UUID. + +7. **Code lists** + +* Enumerations (e.g., `DocumentType`, `RateType`, `TotalType`, `LaborType`) are validated against + your code list XSDs. Use exact codes. + +--- + +# Quick field checklist for a *solid* ChgRq + +* **Header** + + * `RqUID` ✅ + +* **Doc identity** + + * `DocumentInfo/BMSVer` = `6.9.0` ✅ + * `DocumentInfo/DocumentType` = `E` ✅ + * `DocumentInfo/CreateDateTime` ✅ + * `DocumentInfo/DocumentID` (recommended) ✅ + * `DocumentInfo/DocumentVer` and/or `ReferenceInfo` (recommended) ✅ + +* **Changed data only** + + * `ProfileInfo/RateInfo` (if rates/taxes changed) + * `DamageLineInfo[0..*]` (added/updated/removed lines) + * `RepairTotalsInfo/SummaryTotalsInfo` (if required by partner) + * Any updated `AdminInfo`, `ClaimInfo`, `VehicleInfo` fragments as needed + +--- + +# Pointers to definitions in your bundle + +* `VehicleDamageEstimateChgRq` element → `BMSEstimateMessages_2024R1_V6.9.0.xsd` +* `EstimateChgRqType` → `BMSEstimateCommonTypes_2024R1_V6.9.0.xsd` +* `MessageHeaderType`, `DocumentInfoType`, `VehicleInfoType`, `TotalsInfoType`, etc. → + `BMSCommonGlobalTypes_2024R1_V6.9.0.xsd` +* Code lists → `BMSCodeLists_ClassicCode_2024R1_V6.9.0.xsd`, + `BMSCodeLists_CodeExt_2024R1_V6.9.0.xsd` +* Service wrapper → `BMSEstimateService_2024R1_V6.9.0.xsd` (group `EstimateService` contains the + ChgRq/Rs) + +---