feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - rr-Utils hardening
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
// File: server/rr/rr-customers.js
|
||||||
const { RRClient } = require("./lib/index.cjs");
|
const { RRClient } = require("./lib/index.cjs");
|
||||||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||||
const RRLogger = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
@@ -13,8 +14,6 @@ function buildClientAndOpts(bodyshop) {
|
|||||||
retries: cfg.retries
|
retries: cfg.retries
|
||||||
});
|
});
|
||||||
|
|
||||||
// For customer INSERT, the STAR envelope typically uses Task="CU" and ReferenceId="Insert".
|
|
||||||
// Routing (dealer/store/area) is provided via opts.routing and applied by the lib.
|
|
||||||
const opts = {
|
const opts = {
|
||||||
routing: cfg.routing,
|
routing: cfg.routing,
|
||||||
envelope: {
|
envelope: {
|
||||||
@@ -30,49 +29,83 @@ function buildClientAndOpts(bodyshop) {
|
|||||||
return { client, opts };
|
return { client, opts };
|
||||||
}
|
}
|
||||||
|
|
||||||
// minimal field extraction
|
|
||||||
function digitsOnly(s) {
|
function digitsOnly(s) {
|
||||||
return String(s || "").replace(/\D/g, "");
|
return String(s || "").replace(/\D/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCustomerPayloadFromJob(job, overrides = {}) {
|
function uniq(arr) {
|
||||||
const firstName = overrides.firstName ?? job?.ownr_fn ?? job?.customer?.first_name ?? "";
|
return Array.from(new Set(arr));
|
||||||
const lastName = overrides.lastName ?? job?.ownr_ln ?? job?.customer?.last_name ?? "";
|
}
|
||||||
const company = overrides.company ?? job?.ownr_co_nm ?? job?.customer?.company_name ?? "";
|
|
||||||
|
|
||||||
// Prefer owner phone; fall back to customer phones
|
/**
|
||||||
const phone =
|
* Build a payload that matches the RR client expectations for insert/update:
|
||||||
overrides.phone ??
|
* - ibFlag: 'I' (individual) or 'B' (business). If we have a first name, default to 'I', else 'B' if company present.
|
||||||
job?.ownr_ph1 ??
|
* - Must include lastName OR customerName.
|
||||||
job?.customer?.mobile ??
|
* - addresses[] / phones[] / emails[] per the library’s toView() contract.
|
||||||
job?.customer?.home_phone ??
|
*/
|
||||||
job?.customer?.phone ??
|
function buildCustomerPayloadFromJob(job, overrides = {}) {
|
||||||
"";
|
// Pull ONLY from job.ownr_* fields (no job.customer.*)
|
||||||
|
const firstName = overrides.firstName ?? job?.ownr_fn ?? undefined;
|
||||||
|
const lastName = overrides.lastName ?? job?.ownr_ln ?? undefined;
|
||||||
|
const companyName = overrides.companyName ?? overrides.company ?? job?.ownr_co_nm ?? undefined;
|
||||||
|
|
||||||
|
// Decide Individual vs Business (caller can force via overrides.ibFlag)
|
||||||
|
const ibFlag = (overrides.ibFlag || (firstName ? "I" : companyName ? "B" : "I")).toUpperCase();
|
||||||
|
|
||||||
|
// Email(s)
|
||||||
|
const email = overrides.email ?? job?.ownr_ea ?? undefined;
|
||||||
|
const emails = email ? [{ address: String(email) }] : undefined;
|
||||||
|
|
||||||
|
// Phones
|
||||||
|
const phoneCandidates = [overrides.phone, job?.ownr_ph1, job?.ownr_ph2]
|
||||||
|
.map((v) => digitsOnly(v))
|
||||||
|
.filter((v) => v && v.length >= 7);
|
||||||
|
|
||||||
|
const phones = uniq(phoneCandidates).map((num) => ({ number: num }));
|
||||||
|
|
||||||
|
// Address (include only if line1 exists; template requires Addr1 if address is present)
|
||||||
|
const line1 = overrides.addressLine1 ?? job?.ownr_addr1 ?? undefined;
|
||||||
|
|
||||||
|
const addresses = line1
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: overrides.addressType || "P",
|
||||||
|
line1,
|
||||||
|
line2: overrides.addressLine2 ?? job?.ownr_addr2 ?? undefined,
|
||||||
|
city: overrides.city ?? job?.ownr_city ?? undefined,
|
||||||
|
state: overrides.state ?? job?.ownr_st ?? undefined,
|
||||||
|
postalCode: overrides.postalCode ?? job?.ownr_zip ?? undefined,
|
||||||
|
country: (overrides.country ?? job?.ownr_ctry ?? "CA") || undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Enforce lib requirement: lastName OR customerName
|
||||||
|
if (!lastName && !companyName) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot build RR customer payload: lastName or companyName is required (no ownr_ln / ownr_co_nm on job)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
// These keys follow the RR client’s conventions; the lib normalizes case internally.
|
ibFlag, // 'I' or 'B'
|
||||||
firstName: firstName || undefined,
|
firstName: firstName || undefined,
|
||||||
lastName: lastName || undefined,
|
lastName: lastName || undefined,
|
||||||
companyName: company || undefined,
|
customerName: companyName || undefined,
|
||||||
phone: digitsOnly(phone) || undefined,
|
createdBy: overrides.createdBy || "ImEX Online",
|
||||||
email: overrides.email || job?.ownr_ea || job?.customer?.email || undefined,
|
customerType: overrides.customerType || "R", // Retail default
|
||||||
address: {
|
addresses,
|
||||||
line1: overrides.addressLine1 ?? job?.ownr_addr1 ?? job?.customer?.address_line1 ?? undefined,
|
phones,
|
||||||
line2: overrides.addressLine2 ?? job?.ownr_addr2 ?? job?.customer?.address_line2 ?? undefined,
|
emails
|
||||||
city: overrides.city ?? job?.ownr_city ?? job?.customer?.city ?? undefined,
|
|
||||||
state: overrides.state ?? job?.ownr_st ?? job?.customer?.state ?? job?.customer?.province ?? undefined,
|
|
||||||
postalCode: overrides.postalCode ?? job?.ownr_zip ?? job?.customer?.postal_code ?? undefined,
|
|
||||||
country: overrides.country ?? job?.ownr_ctry ?? job?.customer?.country ?? "CA"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Object.keys(payload).forEach((k) => payload[k] === undefined && delete payload[k]);
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a customer in RR and return { custNo, raw }.
|
* Create a customer in RR and return { customerNo, raw }.
|
||||||
* NOTE: The library returns { data: { dmsRecKey, status, statusCode }, statusBlocks, ... }.
|
* Maps data.dmsRecKey -> customerNo for compatibility with existing callers.
|
||||||
* We map data.dmsRecKey -> custNo for compatibility with existing callers.
|
|
||||||
*/
|
*/
|
||||||
async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
||||||
const log = RRLogger(socket, { ns: "rr" });
|
const log = RRLogger(socket, { ns: "rr" });
|
||||||
@@ -83,16 +116,14 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
|||||||
try {
|
try {
|
||||||
res = await client.insertCustomer(payload, opts);
|
res = await client.insertCustomer(payload, opts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack });
|
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack, payload });
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = res?.data ?? res; // be tolerant to shapes
|
const data = res?.data ?? res;
|
||||||
const trx = res?.statusBlocks?.transaction;
|
const trx = res?.statusBlocks?.transaction;
|
||||||
|
|
||||||
// Primary: map dmsRecKey -> custNo
|
|
||||||
let customerNo = data?.dmsRecKey;
|
let customerNo = data?.dmsRecKey;
|
||||||
|
|
||||||
if (!customerNo) {
|
if (!customerNo) {
|
||||||
log("error", "RR insertCustomer returned no dmsRecKey/custNo", {
|
log("error", "RR insertCustomer returned no dmsRecKey/custNo", {
|
||||||
status: trx?.status,
|
status: trx?.status,
|
||||||
@@ -107,11 +138,7 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize to string for safety
|
return { customerNo: String(customerNo), raw: data };
|
||||||
customerNo = String(customerNo);
|
|
||||||
|
|
||||||
// Preserve existing return shape so callers don’t need changes
|
|
||||||
return { customerNo, raw: data };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user