Files
bodyshop/server/rr/rr-mappers.js

427 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file rr-mappers.js
* @description Maps internal ImEX (Hasura) entities into Rome (Reynolds & Reynolds) XML structures.
* Each function returns a plain JS object that matches Mustache templates in xml-templates/.
*/
const dayjs = require("dayjs");
const { normalizeRRDealerFields } = require("./rr-constants");
/**
* Utility: formats date/time to R&Rs preferred format (ISO or yyyy-MM-dd).
*/
const formatDate = (val) => {
if (!val) return undefined;
return dayjs(val).format("YYYY-MM-DD");
};
/**
* Utility: safely pick numeric values and stringify for XML.
*/
const num = (val) => (val != null ? String(val) : undefined);
const toBoolStr = (v) => (v === true ? "true" : v === false ? "false" : undefined);
const hasAny = (obj) => !!obj && Object.values(obj).some((v) => v !== undefined && v !== null && v !== "");
/**
* Pull canonical Dealer/Store/Branch fields from cfg (tolerate snake_case during migration).
* Enforces DB-provided values upstream (no env fallback here).
*/
function getDSB(cfg) {
const { dealerNumber, storeNumber, branchNumber } = normalizeRRDealerFields(cfg || {});
return { dealerNumber, storeNumber, branchNumber };
}
//
// ===================== CUSTOMER =====================
//
/**
* Map internal customer record to Rome CustomerInsertRq.
*/
function mapCustomerInsert(customer, bodyshopConfig) {
if (!customer) return {};
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `CUST-INSERT-${customer.id}`,
Environment: process.env.NODE_ENV,
CustomerId: customer.external_id || undefined,
CustomerType: customer.type || "RETAIL",
CompanyName: customer.company_name,
FirstName: customer.first_name,
MiddleName: customer.middle_name,
LastName: customer.last_name,
PreferredName: customer.display_name || customer.first_name,
ActiveFlag: customer.active ? "true" : "false",
CustomerGroup: customer.group_name,
TaxExempt: customer.tax_exempt ? "true" : "false",
DiscountLevel: num(customer.discount_level),
PreferredLanguage: customer.language || "EN",
Addresses: (customer.addresses || []).map((a) => ({
AddressType: a.type || "BILLING",
AddressLine1: a.line1,
AddressLine2: a.line2,
City: a.city,
State: a.state,
PostalCode: a.postal_code,
Country: a.country || "US"
})),
Phones: (customer.phones || []).map((p) => ({
PhoneNumber: p.number,
PhoneType: p.type || "MOBILE",
Preferred: p.preferred ? "true" : "false"
})),
Emails: (customer.emails || []).map((e) => ({
EmailAddress: e.address,
EmailType: e.type || "WORK",
Preferred: e.preferred ? "true" : "false"
})),
Insurance: customer.insurance
? {
CompanyName: customer.insurance.company,
PolicyNumber: customer.insurance.policy,
ExpirationDate: formatDate(customer.insurance.expiration_date),
ContactName: customer.insurance.contact_name,
ContactPhone: customer.insurance.contact_phone
}
: undefined,
LinkedAccounts: (customer.linked_accounts || []).map((a) => ({
Type: a.type,
AccountNumber: a.account_number,
CreditLimit: num(a.credit_limit)
})),
Notes: customer.notes?.length ? { Items: customer.notes.map((n) => n.text || n) } : undefined
};
}
/**
* Map internal customer record to Rome CustomerUpdateRq.
*/
function mapCustomerUpdate(customer, bodyshopConfig) {
if (!customer) return {};
return {
...mapCustomerInsert(customer, bodyshopConfig),
RequestId: `CUST-UPDATE-${customer.id}`
};
}
//
// ===================== VEHICLE =====================
//
/**
* Map vehicle to Rome ServiceVehicleAddRq.
*/
function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) {
if (!vehicle) return {};
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `VEH-${vehicle.id}`,
CustomerId: ownerCustomer?.external_id,
VIN: vehicle.vin,
UnitNumber: vehicle.unit_number,
StockNumber: vehicle.stock_number,
Year: num(vehicle.year),
Make: vehicle.make,
Model: vehicle.model,
Trim: vehicle.trim,
BodyStyle: vehicle.body_style,
Transmission: vehicle.transmission,
Engine: vehicle.engine,
FuelType: vehicle.fuel_type,
DriveType: vehicle.drive_type,
Color: vehicle.color,
LicensePlate: vehicle.license_plate,
LicenseState: vehicle.license_state,
Odometer: num(vehicle.odometer),
OdometerUnits: vehicle.odometer_units || "KM",
InServiceDate: formatDate(vehicle.in_service_date),
Insurance: vehicle.insurance
? {
CompanyName: vehicle.insurance.company,
PolicyNumber: vehicle.insurance.policy,
ExpirationDate: formatDate(vehicle.insurance.expiration_date)
}
: undefined,
Warranty: vehicle.warranty
? {
WarrantyCompany: vehicle.warranty.company,
WarrantyNumber: vehicle.warranty.number,
WarrantyType: vehicle.warranty.type,
ExpirationDate: formatDate(vehicle.warranty.expiration_date)
}
: undefined,
VehicleNotes: vehicle.notes?.length ? { Items: vehicle.notes.map((n) => n.text || n) } : undefined
};
}
//
// ===================== REPAIR ORDER =====================
//
/**
* Map internal job to Rome RepairOrderInsertRq.
*/
function mapRepairOrderCreate(job, bodyshopConfig) {
if (!job) return {};
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
const cust = job.customer || {};
const veh = job.vehicle || {};
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `RO-${job.id}`,
Environment: process.env.NODE_ENV,
RepairOrderNumber: job.ro_number,
DmsRepairOrderId: job.external_id,
OpenDate: formatDate(job.open_date),
PromisedDate: formatDate(job.promised_date),
CloseDate: formatDate(job.close_date),
ServiceAdvisorId: job.advisor_id,
TechnicianId: job.technician_id,
Department: job.department,
ProfitCenter: job.profit_center,
ROType: job.ro_type,
Status: job.status,
IsBodyShop: "true",
DRPFlag: job.drp_flag ? "true" : "false",
Customer: {
CustomerId: cust.external_id,
CustomerName: cust.full_name,
PhoneNumber: cust.phone,
EmailAddress: cust.email
},
Vehicle: {
VehicleId: veh.external_id,
VIN: veh.vin,
LicensePlate: veh.license_plate,
Year: num(veh.year),
Make: veh.make,
Model: veh.model,
Odometer: num(veh.odometer),
Color: veh.color
},
JobLines: (job.joblines || []).map((l, i) => ({
Sequence: i + 1,
ParentSequence: l.parent_sequence,
LineType: l.line_type,
Category: l.category,
OpCode: l.op_code,
Description: l.description,
LaborHours: num(l.labor_hours),
LaborRate: num(l.labor_rate),
PartNumber: l.part_number,
PartDescription: l.part_description,
Quantity: num(l.quantity),
UnitPrice: num(l.unit_price),
ExtendedPrice: num(l.extended_price),
DiscountAmount: num(l.discount_amount),
TaxCode: l.tax_code,
GLAccount: l.gl_account,
ControlNumber: l.control_number,
Taxes: l.taxes?.length
? {
Items: l.taxes.map((t) => ({
Code: t.code,
Amount: num(t.amount),
Rate: num(t.rate)
}))
}
: undefined
})),
Totals: {
Currency: job.currency || "CAD",
LaborTotal: num(job.totals?.labor),
PartsTotal: num(job.totals?.parts),
MiscTotal: num(job.totals?.misc),
DiscountTotal: num(job.totals?.discount),
TaxTotal: num(job.totals?.tax),
GrandTotal: num(job.totals?.grand)
},
Payments: job.payments?.length
? {
Items: job.payments.map((p) => ({
PayerType: p.payer_type,
PayerName: p.payer_name,
Amount: num(p.amount),
Method: p.method,
Reference: p.reference,
ControlNumber: p.control_number
}))
}
: undefined,
Insurance: job.insurance
? {
CompanyName: job.insurance.company,
ClaimNumber: job.insurance.claim_number,
AdjusterName: job.insurance.adjuster_name,
AdjusterPhone: job.insurance.adjuster_phone
}
: undefined,
Notes: job.notes?.length ? { Items: job.notes.map((n) => n.text || n) } : undefined
};
}
/**
* Map for repair order updates.
*/
function mapRepairOrderUpdate(job, bodyshopConfig) {
return {
...mapRepairOrderCreate(job, bodyshopConfig),
RequestId: `RO-UPDATE-${job.id}`
};
}
//
// ===================== LOOKUPS =====================
//
function mapAdvisorLookup(criteria, bodyshopConfig) {
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `LOOKUP-ADVISOR-${Date.now()}`,
SearchCriteria: {
Department: criteria.department || "Body Shop",
Status: criteria.status || "ACTIVE"
}
};
}
function mapPartsLookup(criteria, bodyshopConfig) {
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: `LOOKUP-PART-${Date.now()}`,
SearchCriteria: {
PartNumber: criteria.part_number,
Description: criteria.description,
Make: criteria.make,
Model: criteria.model,
Year: num(criteria.year),
Category: criteria.category,
MaxResults: criteria.max_results || 25
}
};
}
function mapCombinedSearch(criteria = {}, bodyshopConfig) {
const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig);
// accept nested or flat input
const c = criteria || {};
const cust = c.customer || c.Customer || {};
const veh = c.vehicle || c.Vehicle || {};
const comp = c.company || c.Company || {};
// build optional blocks only if they have at least one value
const customerBlock = {
FirstName: cust.firstName || cust.FirstName || c.firstName,
LastName: cust.lastName || cust.LastName || c.lastName,
PhoneNumber: cust.phoneNumber || cust.PhoneNumber || c.phoneNumber || c.phone,
EmailAddress: cust.email || cust.EmailAddress || c.email,
CompanyName: cust.companyName || cust.CompanyName || c.companyName,
CustomerId: cust.customerId || cust.CustomerId || c.customerId
};
const vehicleBlock = {
VIN: veh.vin || veh.VIN || c.vin,
LicensePlate: veh.licensePlate || veh.LicensePlate || c.licensePlate,
Make: veh.make || veh.Make || c.make,
Model: veh.model || veh.Model || c.model,
Year: veh.year != null ? String(veh.year) : c.year != null ? String(c.year) : undefined,
VehicleId: veh.vehicleId || veh.VehicleId || c.vehicleId
};
const companyBlock = {
Name: comp.name || comp.Name || c.companyName,
Phone: comp.phone || comp.Phone || c.companyPhone
};
return {
DealerCode: bodyshopConfig?.dealer_code || "ROME",
DealerName: bodyshopConfig?.dealer_name,
DealerNumber: dealerNumber,
StoreNumber: storeNumber,
BranchNumber: branchNumber,
RequestId: c.requestId || `COMBINED-${Date.now()}`,
Environment: process.env.NODE_ENV,
// Only include these blocks when they have content; Mustache {{#Block}} respects undefined
Customer: hasAny(customerBlock) ? customerBlock : undefined,
Vehicle: hasAny(vehicleBlock) ? vehicleBlock : undefined, // template wraps as <rr:ServiceVehicle>…</rr:ServiceVehicle>
Company: hasAny(companyBlock) ? companyBlock : undefined,
// Search behavior flags
SearchMode: c.searchMode || c.SearchMode, // EXACT | PARTIAL
ExactMatch: toBoolStr(c.exactMatch ?? c.ExactMatch),
PartialMatch: toBoolStr(c.partialMatch ?? c.PartialMatch),
CaseInsensitive: toBoolStr(c.caseInsensitive ?? c.CaseInsensitive),
// Result shaping (default to true when unspecified)
ReturnCustomers: toBoolStr(c.returnCustomers ?? c.ReturnCustomers ?? true),
ReturnVehicles: toBoolStr(c.returnVehicles ?? c.ReturnVehicles ?? true),
ReturnCompanies: toBoolStr(c.returnCompanies ?? c.ReturnCompanies ?? true),
// Paging / sorting
MaxResults: c.maxResults ?? c.MaxResults,
PageNumber: c.pageNumber ?? c.PageNumber,
SortBy: c.sortBy ?? c.SortBy, // e.g., NAME, VIN, PARTNUMBER
SortDirection: c.sortDirection ?? c.SortDirection // ASC | DESC
};
}
module.exports = {
mapCustomerInsert,
mapCustomerUpdate,
mapServiceVehicle,
mapRepairOrderCreate,
mapRepairOrderUpdate,
mapAdvisorLookup,
mapPartsLookup,
mapCombinedSearch
};