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

249 lines
8.8 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.
// server/rr/rr-mappers.js
// Centralized mapping & normalization for Reynolds & Reynolds (RR)
//
// NOTE: This is scaffolding intended to be completed against the RR XML/JSON
// schemas in the Rome RR specs you dropped (Customer Insert/Update, Repair Order,
// Service Vehicle, etc.). Field names below are placeholders where noted.
// Fill the TODOs with the exact RR element/attribute names.
//
// Usage expectation from calling code (example you gave):
// const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
// const body = mapCustomerInsert(JobData);
// const body = mapCustomerUpdate(existingCustomer, patch);
const _ = require("lodash");
const InstanceMgr = require("../utils/instanceMgr").default;
// Keep this consistent with other providers
const replaceSpecialRegex = /[^a-zA-Z0-9 .,\n #]+/g;
function sanitize(value) {
if (value === null || value === undefined) return value;
return String(value).replace(replaceSpecialRegex, "").trim();
}
function normalizePostal(raw) {
// Match Fortellis/CDK behavior for CA vs US formatting
// (Use InstanceMgr profile detection already present in your codebase)
return InstanceMgr({
imex: raw && String(raw).toUpperCase().replace(/\W/g, "").replace(/(...)/, "$1 "),
rome: raw
});
}
function asStringOrNull(value) {
const s = sanitize(value);
return s && s.length > 0 ? s : null;
}
function mapPhones({ ph1, ph2, mobile }) {
// TODO: Update to RRs final phone structure and codes/types when wiring
const out = [];
if (ph1) out.push({ number: sanitize(ph1), type: "HOME" });
if (ph2) out.push({ number: sanitize(ph2), type: "WORK" });
if (mobile) out.push({ number: sanitize(mobile), type: "MOBILE" });
return out;
}
function mapEmails({ email }) {
// RR often supports multiple emails; start with one.
// TODO: Update per RR schema (email flags, preferred, etc.)
if (!email) return [];
return [{ address: sanitize(email), type: "PERSONAL" }];
}
function mapPostalAddressFromJob(job) {
// Rome job-level owner fields (aligning to prior provider scaffolds)
return {
addressLine1: asStringOrNull(job.ownr_addr1),
addressLine2: asStringOrNull(job.ownr_addr2),
city: asStringOrNull(job.ownr_city),
state: asStringOrNull(job.ownr_st || job.ownr_state),
postalCode: normalizePostal(job.ownr_zip),
country: asStringOrNull(job.ownr_ctry) || "USA", // default, adjust as needed
province: asStringOrNull(job.ownr_st) // keep both state/province fields for CA cases
};
}
function mapPhonesFromJob(job) {
return mapPhones({
ph1: job.ownr_ph1,
ph2: job.ownr_ph2,
mobile: job.ownr_mobile
});
}
function mapEmailsFromJob(job) {
return mapEmails({ email: job.ownr_ea });
}
/**
* Customer Insert
* Matches your call site:
* const body = mapCustomerInsert(JobData)
*
* Return shape intentionally mirrors Fortellis scaffolding so the same
* MakeRRCall pipeline can be reused. Replace placeholders with the RR specs
* request envelope/element names (e.g., CustomerInsertRq, CustomerRq, etc.).
*/
function mapCustomerInsert(job) {
const isCompany = Boolean(job.ownr_co_nm && job.ownr_co_nm.trim() !== "");
// Skeleton payload — replace keys under CustomerInsertRq with the actual RR names
return {
CustomerInsertRq: {
// TODO: Confirm RR element/attribute names from spec PDFs
customerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
customerName: {
companyName: asStringOrNull(job.ownr_co_nm)?.toUpperCase() || null,
firstName: isCompany ? null : asStringOrNull(job.ownr_fn)?.toUpperCase(),
lastName: isCompany ? null : asStringOrNull(job.ownr_ln)?.toUpperCase()
},
postalAddress: mapPostalAddressFromJob(job),
contactMethods: {
phones: mapPhonesFromJob(job),
emailAddresses: mapEmailsFromJob(job)
}
// Optional / placeholders for future fields in RR spec
// taxCode: null,
// groupCode: null,
// dealerFields: []
}
};
}
/**
* Customer Update
* Matches your call site:
* const body = mapCustomerUpdate(existingCustomer, patch)
*
* - existingCustomer: prior RR customer payload/shape (from RR Read/Query)
* - patch: minimal delta from UI/Job selections to overlay onto the RR model
*
* We return a merged/normalized payload for RR Update.
*/
function mapCustomerUpdate(existingCustomer, patch = {}) {
// NOTE:
// 1) We assume existingCustomer already resembles RRs stored shape.
// 2) We overlay patch fields into that shape, then project to the
// RR Update request envelope.
// 3) Replace inner keys with exact RR Update schema element names.
const merged = _.merge({}, existingCustomer || {}, patch || {});
const id = merged?.customerId || merged?.id || merged?.CustomerId || merged?.customer?.id || null;
// Derive a normalized name object from merged data (handles org/person)
const isCompany = Boolean(merged?.customerName?.companyName) || Boolean(merged?.companyName) || false;
const normalizedName = {
companyName: asStringOrNull(merged?.customerName?.companyName) || asStringOrNull(merged?.companyName),
firstName: isCompany ? null : asStringOrNull(merged?.customerName?.firstName) || asStringOrNull(merged?.firstName),
lastName: isCompany ? null : asStringOrNull(merged?.customerName?.lastName) || asStringOrNull(merged?.lastName)
};
const normalizedAddress = {
addressLine1: asStringOrNull(merged?.postalAddress?.addressLine1) || asStringOrNull(merged?.addressLine1),
addressLine2: asStringOrNull(merged?.postalAddress?.addressLine2) || asStringOrNull(merged?.addressLine2),
city: asStringOrNull(merged?.postalAddress?.city) || asStringOrNull(merged?.city),
state:
asStringOrNull(merged?.postalAddress?.state) ||
asStringOrNull(merged?.state) ||
asStringOrNull(merged?.stateOrProvince) ||
asStringOrNull(merged?.province),
postalCode: normalizePostal(merged?.postalAddress?.postalCode || merged?.postalCode),
country: asStringOrNull(merged?.postalAddress?.country) || asStringOrNull(merged?.country) || "USA",
province: asStringOrNull(merged?.postalAddress?.province) || asStringOrNull(merged?.province)
};
// Phones
const normalizedPhones = merged?.contactMethods?.phones || merged?.phones || [];
// Emails
const normalizedEmails = merged?.contactMethods?.emailAddresses || merged?.emailAddresses || [];
return {
CustomerUpdateRq: {
// TODO: Confirm exact RR element/attribute names for update
customerId: id,
customerType: normalizedName.companyName ? "ORGANIZATION" : "INDIVIDUAL",
customerName: normalizedName,
postalAddress: normalizedAddress,
contactMethods: {
phones: normalizedPhones,
emailAddresses: normalizedEmails
}
// Optional change tracking fields, timestamps, etc., per RR spec can go here
}
};
}
/* ===== Additional mappers (scaffolding for upcoming work) ===== */
function mapVehicleInsertFromJob(job, txEnvelope = {}) {
// TODO: Replace with RR Service Vehicle Insert schema
return {
ServiceVehicleInsertRq: {
vin: asStringOrNull(job.v_vin),
year: job.v_model_yr || null,
make: txEnvelope.dms_make || asStringOrNull(job.v_make),
model: txEnvelope.dms_model || asStringOrNull(job.v_model),
odometer: txEnvelope.kmout || null,
licensePlate: job.plate_no && /\w/.test(job.plate_no) ? asStringOrNull(job.plate_no).toUpperCase() : null
}
};
}
function mapRepairOrderAddFromJob(job) {
// TODO: Replace with RR RepairOrder Add schema (headers, lines, taxes)
return {
RepairOrderAddRq: {
customerId: job.customer?.id || null,
vehicleId: job.vehicle?.id || null,
referenceNumber: asStringOrNull(job.ro_number),
openedAt: job.actual_in || null,
closedAt: job.invoice_date || null
// lines: job.joblines?.map(mapJobLineToRRLine),
// taxes: mapTaxes(job),
// payments: mapPayments(job)
}
};
}
function mapRepairOrderChangeFromJob(job) {
// TODO: Replace with RR RepairOrder Update schema
return {
RepairOrderChgRq: {
repairOrderId: job.id,
referenceNumber: asStringOrNull(job.ro_number)
// delta lines, amounts, status, etc.
}
};
}
/* Example line mapper (placeholder) */
function mapJobLineToRRLine(line) {
return {
// TODO: set RR fields
seq: line.sequence || null,
opCode: line.opCode || null,
description: asStringOrNull(line.description),
qty: line.part_qty || null,
price: line.price || null
};
}
module.exports = {
// Required by your current calling code:
mapCustomerInsert,
mapCustomerUpdate,
// Extra scaffolds well likely use right after:
mapVehicleInsertFromJob,
mapRepairOrderAddFromJob,
mapRepairOrderChangeFromJob,
mapJobLineToRRLine,
// low-level utils (export if you want to reuse in tests)
_sanitize: sanitize,
_normalizePostal: normalizePostal
};