334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
// -----------------------------------------------------------------------------
|
||
// Centralized mapping & normalization for Reynolds & Reynolds (RR)
|
||
//
|
||
// This is scaffolding aligned to the Rome RR PDFs you provided:
|
||
//
|
||
// - Rome Customer Insert Specification 1.2.pdf
|
||
// - Rome Customer Update Specification 1.2.pdf
|
||
// - Rome Insert Service Vehicle Interface Specification.pdf
|
||
// - Rome Create Body Shop Management Repair Order Interface Specification.pdf
|
||
// - Rome Update Body Shop Management Repair Order Interface Specification.pdf
|
||
// - Rome Get Advisors Specification.pdf
|
||
// - Rome Get Part Specification.pdf
|
||
// - Rome Search Customer Service Vehicle Combined Specification.pdf
|
||
//
|
||
// Replace all TODO:RR with exact element/attribute names and enumerations from
|
||
// the PDFs above. The shapes here are intentionally close to other providers
|
||
// so you can reuse upstream plumbing without surprises.
|
||
// -----------------------------------------------------------------------------
|
||
|
||
const _ = require("lodash");
|
||
|
||
// Keep this consistent with other providers
|
||
const replaceSpecialRegex = /[^a-zA-Z0-9 .,\n #]+/g;
|
||
|
||
// ---------- Generic helpers --------------------------------------------------
|
||
|
||
function sanitize(value) {
|
||
if (value === null || value === undefined) return value;
|
||
return String(value).replace(replaceSpecialRegex, "").trim();
|
||
}
|
||
|
||
function asStringOrNull(value) {
|
||
const s = sanitize(value);
|
||
return s && s.length > 0 ? s : null;
|
||
}
|
||
|
||
function toUpperOrNull(value) {
|
||
const s = asStringOrNull(value);
|
||
return s ? s.toUpperCase() : null;
|
||
}
|
||
|
||
/**
|
||
* Normalize postal/zip minimally; keep simple and provider-agnostic for now.
|
||
* TODO:RR — If RR enforces specific postal formatting by country, implement it here.
|
||
*/
|
||
function normalizePostal(raw) {
|
||
if (!raw) return null;
|
||
return String(raw).trim();
|
||
}
|
||
|
||
function mapPhones({ ph1, ph2, mobile }) {
|
||
// TODO:RR — Replace "HOME|WORK|MOBILE" with RR's phone type codes + any flags (preferred, sms ok).
|
||
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 }) {
|
||
// TODO:RR — If RR supports multiple with flags, expand (preferred, statement, etc.).
|
||
if (!email) return [];
|
||
return [{ address: sanitize(email), type: "PERSONAL" }];
|
||
}
|
||
|
||
// ---------- Address/Contact from Rome JobData --------------------------------
|
||
|
||
function mapPostalAddressFromJob(job) {
|
||
// Rome job-level owner fields (aligned with other providers)
|
||
// TODO:RR — Confirm exact element names (e.g., AddressLine vs Street1, State vs Province).
|
||
return {
|
||
addressLine1: asStringOrNull(job.ownr_addr1),
|
||
addressLine2: asStringOrNull(job.ownr_addr2),
|
||
city: asStringOrNull(job.ownr_city),
|
||
state: asStringOrNull(job.ownr_st || job.ownr_state),
|
||
province: asStringOrNull(job.ownr_province), // keep both for CA use-cases if distinct in RR
|
||
postalCode: normalizePostal(job.ownr_zip),
|
||
country: asStringOrNull(job.ownr_ctry) || "USA"
|
||
};
|
||
}
|
||
|
||
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 mappers --------------------------------------------------
|
||
|
||
/**
|
||
* Customer Insert
|
||
* Matches call-site: const body = mapCustomerInsert(JobData);
|
||
*
|
||
* TODO:RR — Replace envelope and field names with exact RR schema:
|
||
* e.g., CustomerInsertRq.Customer (Organization vs Person), name blocks, ids, codes, etc.
|
||
*/
|
||
function mapCustomerInsert(job) {
|
||
const isCompany = Boolean(job?.ownr_co_nm && job.ownr_co_nm.trim() !== "");
|
||
|
||
return {
|
||
// Example envelope — rename to match the PDF (e.g., "CustomerInsertRq")
|
||
CustomerInsertRq: {
|
||
// High-level type — confirm the exact enum RR expects.
|
||
customerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
|
||
|
||
// Name block — ensure RR's exact element names and casing.
|
||
customerName: {
|
||
companyName: isCompany ? toUpperOrNull(job.ownr_co_nm) : null,
|
||
firstName: isCompany ? null : toUpperOrNull(job.ownr_fn),
|
||
lastName: isCompany ? null : toUpperOrNull(job.ownr_ln)
|
||
},
|
||
|
||
// Mailing address
|
||
postalAddress: mapPostalAddressFromJob(job),
|
||
|
||
// Contacts
|
||
contactMethods: {
|
||
phones: mapPhonesFromJob(job),
|
||
emailAddresses: mapEmailsFromJob(job)
|
||
}
|
||
|
||
// TODO:RR — Common optional fields: tax/resale codes, pricing flags, AR terms, source codes, etc.
|
||
// taxCode: null,
|
||
// termsCode: null,
|
||
// marketingOptIn: null,
|
||
// dealerSpecificFields: []
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Customer Update
|
||
* Matches call-site: const body = mapCustomerUpdate(existingCustomer, patch);
|
||
*
|
||
* - existingCustomer: RR's current representation (from Read/Query)
|
||
* - patch: a thin delta from UI/Job selection
|
||
*
|
||
* TODO:RR — Swap envelope/fields for RR's specific Update schema.
|
||
*/
|
||
function mapCustomerUpdate(existingCustomer, patch = {}) {
|
||
const merged = _.merge({}, existingCustomer || {}, patch || {});
|
||
const id = merged?.customerId || merged?.id || merged?.CustomerId || merged?.customer?.id || null;
|
||
|
||
const isCompany = Boolean(merged?.customerName?.companyName) || Boolean(merged?.companyName);
|
||
|
||
const normalizedName = {
|
||
companyName: asStringOrNull(merged?.customerName?.companyName) || asStringOrNull(merged?.companyName) || null,
|
||
firstName: isCompany
|
||
? null
|
||
: asStringOrNull(merged?.customerName?.firstName) || asStringOrNull(merged?.firstName) || null,
|
||
lastName: isCompany
|
||
? null
|
||
: asStringOrNull(merged?.customerName?.lastName) || asStringOrNull(merged?.lastName) || null
|
||
};
|
||
|
||
const normalizedAddress = {
|
||
addressLine1: asStringOrNull(merged?.postalAddress?.addressLine1) || asStringOrNull(merged?.addressLine1) || null,
|
||
addressLine2: asStringOrNull(merged?.postalAddress?.addressLine2) || asStringOrNull(merged?.addressLine2) || null,
|
||
city: asStringOrNull(merged?.postalAddress?.city) || asStringOrNull(merged?.city) || null,
|
||
state:
|
||
asStringOrNull(merged?.postalAddress?.state) ||
|
||
asStringOrNull(merged?.state) ||
|
||
asStringOrNull(merged?.stateOrProvince) ||
|
||
asStringOrNull(merged?.province) ||
|
||
null,
|
||
province: asStringOrNull(merged?.postalAddress?.province) || asStringOrNull(merged?.province) || null,
|
||
postalCode: normalizePostal(merged?.postalAddress?.postalCode || merged?.postalCode),
|
||
country: asStringOrNull(merged?.postalAddress?.country) || asStringOrNull(merged?.country) || "USA"
|
||
};
|
||
|
||
// Contacts (reuse existing unless patch supplied a new structure upstream)
|
||
const normalizedPhones = merged?.contactMethods?.phones || merged?.phones || [];
|
||
|
||
const normalizedEmails = merged?.contactMethods?.emailAddresses || merged?.emailAddresses || [];
|
||
|
||
return {
|
||
// Example envelope — rename to match the PDF (e.g., "CustomerUpdateRq")
|
||
CustomerUpdateRq: {
|
||
customerId: id,
|
||
customerType: normalizedName.companyName ? "ORGANIZATION" : "INDIVIDUAL",
|
||
customerName: normalizedName,
|
||
postalAddress: normalizedAddress,
|
||
contactMethods: {
|
||
phones: normalizedPhones,
|
||
emailAddresses: normalizedEmails
|
||
}
|
||
// TODO:RR — include fields that RR requires for update (version, hash, lastUpdatedTs, etc.)
|
||
}
|
||
};
|
||
}
|
||
|
||
// ---------- Vehicle mappers ---------------------------------------------------
|
||
|
||
/**
|
||
* Vehicle Insert from JobData
|
||
* Called (or call-able) by InsertVehicle.
|
||
*
|
||
* TODO:RR — Replace envelope/field names with the exact RR vehicle schema.
|
||
*/
|
||
function mapVehicleInsertFromJob(job, txEnvelope = {}) {
|
||
return {
|
||
ServiceVehicleInsertRq: {
|
||
vin: asStringOrNull(job.v_vin),
|
||
// Year/make/model — validate source fields vs RR required fields
|
||
year: job.v_model_yr || null,
|
||
make: toUpperOrNull(txEnvelope.dms_make || job.v_make),
|
||
model: toUpperOrNull(txEnvelope.dms_model || job.v_model),
|
||
// Mileage/odometer — confirm units/element names
|
||
odometer: txEnvelope.kmout || txEnvelope.miout || null,
|
||
// Plate — uppercase and sanitize
|
||
licensePlate: job.plate_no ? toUpperOrNull(job.plate_no) : null
|
||
// TODO:RR — owner/customer link, color, trim, fuel, DRIVETRAIN, etc.
|
||
}
|
||
};
|
||
}
|
||
|
||
// ---------- Repair Order mappers ---------------------------------------------
|
||
|
||
/**
|
||
* Create Repair Order
|
||
* Matches call-site: mapRepairOrderCreate({ JobData, txEnvelope })
|
||
*
|
||
* TODO:RR — Use the exact request envelope/fields for Create RO from the PDF:
|
||
* Header (customer/vehicle/ro-no/dates), lines/labors/parts/taxes, totals.
|
||
*/
|
||
function mapRepairOrderCreate({ JobData, txEnvelope }) {
|
||
return {
|
||
RepairOrderCreateRq: {
|
||
// Header
|
||
referenceNumber: asStringOrNull(JobData.ro_number),
|
||
customerId: JobData?.customer?.id || null, // supply from previous step or selection
|
||
vehicleId: JobData?.vehicle?.id || null, // supply from previous step
|
||
openedAt: JobData?.actual_in || null, // confirm expected datetime format
|
||
promisedAt: JobData?.promise_date || null,
|
||
advisorId: txEnvelope?.advisorId || null,
|
||
|
||
// Lines (placeholder)
|
||
lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [],
|
||
|
||
// Taxes (placeholder)
|
||
taxes: mapTaxes(JobData),
|
||
|
||
// Payments (placeholder)
|
||
payments: mapPayments(txEnvelope)
|
||
|
||
// TODO:RR — add required flags, shop supplies, labor matrix, discounts, etc.
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Update Repair Order
|
||
* Matches call-site: mapRepairOrderUpdate({ JobData, txEnvelope })
|
||
*
|
||
* TODO:RR — RR may want delta format (change set) vs full replace.
|
||
* Add versioning/concurrency tokens if specified in the PDF.
|
||
*/
|
||
function mapRepairOrderUpdate({ JobData, txEnvelope }) {
|
||
return {
|
||
RepairOrderUpdateRq: {
|
||
repairOrderId: JobData?.id || txEnvelope?.repairOrderId || null,
|
||
referenceNumber: asStringOrNull(JobData?.ro_number),
|
||
|
||
// Example: only pass changed lines (you may need your diff before mapping)
|
||
// For scaffolding, we pass what we have; replace with proper deltas later.
|
||
lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [],
|
||
|
||
taxes: mapTaxes(JobData),
|
||
payments: mapPayments(txEnvelope)
|
||
|
||
// TODO:RR — include RO status transitions, close/invoice flags, etc.
|
||
}
|
||
};
|
||
}
|
||
|
||
/* ----- Line/Tax/Payment helpers (placeholders) ----------------------------- */
|
||
|
||
function mapJobLineToRRLine(line) {
|
||
// TODO:RR — Replace with RR RO line schema (labor/part/misc line types, op-code, flags).
|
||
return {
|
||
lineType: line?.type || "LABOR", // e.g., LABOR | PART | MISC
|
||
sequence: line?.sequence || null,
|
||
opCode: line?.opCode || line?.opcode || null,
|
||
description: asStringOrNull(line?.description || line?.descr),
|
||
quantity: line?.part_qty || line?.qty || 1,
|
||
unitPrice: line?.price || line?.unitPrice || null,
|
||
extendedAmount: line?.ext || null
|
||
};
|
||
}
|
||
|
||
function mapTaxes(job) {
|
||
// TODO:RR — Implement per RR tax structure (rates by jurisdiction, taxable flags, rounding rules).
|
||
// Return empty array as scaffolding.
|
||
return [];
|
||
}
|
||
|
||
function mapPayments(txEnvelope = {}) {
|
||
// TODO:RR — Implement per RR payment shape (payer types, amounts, reference ids)
|
||
// For Fortellis/CDK parity, txEnvelope.payers often exists; adapt to RR fields.
|
||
if (!Array.isArray(txEnvelope?.payers)) return [];
|
||
return txEnvelope.payers.map((p) => ({
|
||
payerType: p.type || "INSURER", // e.g., CUSTOMER | INSURER | WARRANTY
|
||
reference: asStringOrNull(p.controlnumber || p.ref),
|
||
amount: p.amount != null ? Number(p.amount) : null
|
||
}));
|
||
}
|
||
|
||
// ---------- Exports -----------------------------------------------------------
|
||
|
||
module.exports = {
|
||
// Used by current call-sites:
|
||
mapCustomerInsert,
|
||
mapCustomerUpdate,
|
||
mapRepairOrderCreate,
|
||
mapRepairOrderUpdate,
|
||
|
||
// Extra scaffolds you’ll likely use soon:
|
||
mapVehicleInsertFromJob,
|
||
mapJobLineToRRLine,
|
||
mapTaxes,
|
||
mapPayments,
|
||
|
||
// Low-level utils (handy in tests)
|
||
_sanitize: sanitize,
|
||
_normalizePostal: normalizePostal,
|
||
_toUpperOrNull: toUpperOrNull
|
||
};
|