427 lines
13 KiB
JavaScript
427 lines
13 KiB
JavaScript
/**
|
||
* @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&R’s 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
|
||
};
|