feature/IO-3255-simplified-parts-management - Beef Up Change Request Parser, add Change Request documentation data
This commit is contained in:
@@ -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
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// CamelCase is used for GraphQL and database fields.
|
// CamelCase is used for GraphQL and database fields.
|
||||||
|
|
||||||
const client = require("../../../graphql-client/graphql-client").client;
|
const client = require("../../../graphql-client/graphql-client").client;
|
||||||
|
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||||
|
|
||||||
// GraphQL Queries and Mutations
|
// GraphQL Queries and Mutations
|
||||||
@@ -15,27 +16,7 @@ const {
|
|||||||
// Defaults
|
// Defaults
|
||||||
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";
|
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";
|
||||||
|
|
||||||
// Known part rate types for tax rates
|
// Config: include labor lines and labor in totals (default true)
|
||||||
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)
|
|
||||||
const INCLUDE_LABOR = true;
|
const INCLUDE_LABOR = true;
|
||||||
/**
|
/**
|
||||||
* Fetches the default order status for a bodyshop.
|
* Fetches the default order status for a bodyshop.
|
||||||
@@ -52,49 +33,6 @@ const getDefaultOrderStatus = async (shopId, logger) => {
|
|||||||
return FALLBACK_DEFAULT_ORDER_STATUS;
|
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.
|
* Finds an existing vehicle by shopId and VIN.
|
||||||
* @param {string} shopId - The bodyshop UUID.
|
* @param {string} shopId - The bodyshop UUID.
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
|
|
||||||
const client = require("../../../graphql-client/graphql-client").client;
|
const client = require("../../../graphql-client/graphql-client").client;
|
||||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||||
|
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
GET_JOB_BY_CLAIM,
|
GET_JOB_BY_CLAIM,
|
||||||
UPDATE_JOB_BY_ID,
|
UPDATE_JOB_BY_ID,
|
||||||
UPSERT_JOBLINES,
|
DELETE_JOBLINES_BY_IDS,
|
||||||
DELETE_JOBLINES_BY_IDS
|
INSERT_JOBLINES
|
||||||
} = require("../partsManagement.queries");
|
} = require("../partsManagement.queries");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,52 +38,143 @@ const extractUpdatedJobData = (rq) => {
|
|||||||
const doc = rq.DocumentInfo || {};
|
const doc = rq.DocumentInfo || {};
|
||||||
const claim = rq.ClaimInfo || {};
|
const claim = rq.ClaimInfo || {};
|
||||||
|
|
||||||
return {
|
const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;
|
||||||
|
|
||||||
|
const out = {
|
||||||
comment: doc.Comment || null,
|
comment: doc.Comment || null,
|
||||||
clm_no: claim.ClaimNum || null,
|
clm_no: claim.ClaimNum || null,
|
||||||
status: claim.ClaimStatus || 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.
|
* Extracts updated job lines from the request payload, mirroring the AddRq splitting rules:
|
||||||
* @param addsChgs
|
* - PART lines carry only part pricing (act_price) and related fields
|
||||||
* @param jobId
|
* - If LaborInfo exists on a part line, add a separate LABOR line at unq_seq + 400000
|
||||||
* @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}[]}
|
* - 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 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) => ({
|
const coerceManual = (val) =>
|
||||||
jobid: jobId,
|
val === true || val === 1 || val === "1" || (typeof val === "string" && val.toUpperCase() === "Y");
|
||||||
line_no: parseInt(line.LineNum, 10),
|
|
||||||
unq_seq: parseInt(line.UniqueSequenceNum, 10),
|
const out = [];
|
||||||
status: line.LineStatusCode || null,
|
|
||||||
line_desc: line.LineDesc || null,
|
for (const line of linesIn) {
|
||||||
part_type: line.PartInfo?.PartType || null,
|
if (!line || Object.keys(line).length === 0) continue;
|
||||||
part_qty: parseFloat(line.PartInfo?.Quantity || 0),
|
|
||||||
oem_partno: line.PartInfo?.OEMPartNum || null,
|
const partInfo = line.PartInfo || {};
|
||||||
db_price: parseFloat(line.PartInfo?.PartPrice || 0),
|
const laborInfo = line.LaborInfo || {};
|
||||||
act_price: parseFloat(line.PartInfo?.PartPrice || 0),
|
const refinishInfo = line.RefinishLaborInfo || {};
|
||||||
mod_lbr_ty: line.LaborInfo?.LaborType || null,
|
const subletInfo = line.SubletInfo || {};
|
||||||
mod_lb_hrs: parseFloat(line.LaborInfo?.LaborHours || 0),
|
|
||||||
lbr_op: line.LaborInfo?.LaborOperation || null,
|
const base = {
|
||||||
lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0),
|
jobid: jobId,
|
||||||
notes: line.LineMemo || null,
|
line_no: parseInt(line.LineNum || 0, 10),
|
||||||
manual_line: line.ManualLineInd === "1"
|
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.
|
* Extracts deletion IDs from the deletions object, also removing any derived labor/refinish lines
|
||||||
* @param deletions
|
* by including offsets (base + 400000, base + 500000).
|
||||||
* @returns {number[]}
|
|
||||||
*/
|
*/
|
||||||
const extractDeletions = (deletions = {}) => {
|
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 });
|
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||||
|
|
||||||
if (updatedLines.length > 0) {
|
// Build a set of unq_seq that will be updated (replaced). We delete them first to avoid duplicates.
|
||||||
await client.request(UPSERT_JOBLINES, {
|
const updatedSeqs = Array.from(
|
||||||
joblines: updatedLines
|
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) {
|
if (updatedLines.length > 0) {
|
||||||
await client.request(DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: deletedLineIds });
|
// 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);
|
logger.log("parts-job-changed", "info", job.id, null);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
<VehicleDamageEstimateChgRq xmlns="http://www.cieca.com/BMS">
|
||||||
|
<!-- MessageHeaderType -->
|
||||||
|
<RqUID>00000000-0000-0000-0000-000000000000</RqUID>
|
||||||
|
|
||||||
|
<!-- EstimateChgRqType sequence -->
|
||||||
|
<DocumentInfo>
|
||||||
|
<BMSVer>6.9.0</BMSVer>
|
||||||
|
<DocumentType>E</DocumentType>
|
||||||
|
<CreateDateTime>2025-08-14T12:00:00Z</CreateDateTime>
|
||||||
|
<!-- Strongly recommended for change requests: -->
|
||||||
|
<!-- <DocumentID>EST-12345</DocumentID> -->
|
||||||
|
<!-- <DocumentVer>
|
||||||
|
<DocumentVerCode>REV</DocumentVerCode>
|
||||||
|
<DocumentVerNum>2</DocumentVerNum>
|
||||||
|
</DocumentVer>
|
||||||
|
<ReferenceInfo>
|
||||||
|
<RefDocumentID>EST-12345</RefDocumentID>
|
||||||
|
<RefDocumentVerNum>1</RefDocumentVerNum>
|
||||||
|
</ReferenceInfo> -->
|
||||||
|
</DocumentInfo>
|
||||||
|
|
||||||
|
<!-- Add only what changed. Examples: -->
|
||||||
|
|
||||||
|
<!-- Update a rate -->
|
||||||
|
<!--
|
||||||
|
<ProfileInfo>
|
||||||
|
<RateInfo>
|
||||||
|
<RateType>BODY_LABOR</RateType>
|
||||||
|
<TaxableInd>true</TaxableInd>
|
||||||
|
<TaxRate>13.00</TaxRate>
|
||||||
|
</RateInfo>
|
||||||
|
</ProfileInfo>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Add/update a labor line -->
|
||||||
|
<!--
|
||||||
|
<DamageLineInfo>
|
||||||
|
<LineNum>10</LineNum>
|
||||||
|
<LaborInfo>
|
||||||
|
<LaborType>BODY</LaborType>
|
||||||
|
<LaborHours>1.5</LaborHours>
|
||||||
|
<LaborHourlyRate>85.00</LaborHourlyRate>
|
||||||
|
</LaborInfo>
|
||||||
|
</DamageLineInfo>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Sync totals if your partner requires it with each change -->
|
||||||
|
<!--
|
||||||
|
<RepairTotalsInfo>
|
||||||
|
<SummaryTotalsInfo>
|
||||||
|
<TotalType>GRAND_TOTAL</TotalType>
|
||||||
|
<TotalTypeDesc>Grand Total</TotalTypeDesc>
|
||||||
|
<TotalAmt>1234.56</TotalAmt>
|
||||||
|
</SummaryTotalsInfo>
|
||||||
|
</RepairTotalsInfo>
|
||||||
|
-->
|
||||||
|
</VehicleDamageEstimateChgRq>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
---
|
||||||
Reference in New Issue
Block a user