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

279 lines
8.2 KiB
JavaScript

const { RRClient } = require("./lib/index.cjs");
const { getRRConfigFromBodyshop } = require("./rr-config");
const CreateRRLogEvent = require("./rr-logger-event");
const { withRRRequestXml } = require("./rr-log-xml");
const InstanceManager = require("../utils/instanceMgr").default;
/**
* Country code map for normalization
* @type {{US: string, USA: string, "UNITED STATES": string, CA: string, CAN: string, CANADA: string}}
*/
const COUNTRY_MAP = {
US: "US",
USA: "US",
"UNITED STATES": "US",
CA: "CA",
CAN: "CA",
CANADA: "CA"
};
/**
* Normalize country input to 2-char code
* @param v
* @returns {*|string}
*/
const toCountry2 = (v) => {
const s = String(v || "")
.trim()
.toUpperCase();
if (!s) return "US"; // sane default
if (COUNTRY_MAP[s]) return COUNTRY_MAP[s];
// fallbacks: prefer 2-char; last resort: take first 2
return s.length === 2 ? s : s.slice(0, 2);
};
/**
* Normalize phone number to 10-digit string
* @param num
* @returns {string}
*/
const normalizePhone = (num) => {
const d = String(num || "").replace(/\D/g, "");
const n = d.length === 11 && d.startsWith("1") ? d.slice(1) : d;
return n.slice(0, 10);
};
/**
* Normalize postal code based on country
* @param pc
* @param country
* @returns {string}
*/
const normalizePostal = (pc, country) => {
const s = String(pc || "").trim();
if (country === "US") return s.replace(/[^0-9]/g, "").slice(0, 5);
if (country === "CA") return s.toUpperCase().replace(/\s+/g, "").slice(0, 6);
return s;
};
/**
* Sanitize RR customer payload (addresses, phones, names)
* @param payload
* @returns {{}}
*/
const sanitizeRRCustomerPayload = (payload = {}) => {
const out = { ...payload };
out.addresses = (payload.addresses || []).map((a) => {
const country = toCountry2(a.country);
return {
...a,
country,
state: String(a.state || "")
.toUpperCase()
.slice(0, 2),
postalCode: normalizePostal(a.postalCode, country)
};
});
out.phones = (payload.phones || []).map((p) => ({
...p,
number: normalizePhone(p.number)
}));
// trim names defensively (RR has various max lengths by site config)
if (out.firstName) out.firstName = String(out.firstName).trim().slice(0, 30);
if (out.lastName) out.lastName = String(out.lastName).trim().slice(0, 30);
return out;
};
/**
* Build an RR client + common opts from a bodyshop row
* @param bodyshop
* @returns {{client: *, opts: {routing: {dealerNumber: *, storeNumber: *, areaNumber: *}, envelope: {sender: {component: string, task: string, referenceId: string, creator: string, senderName: string}}}}}
*/
const buildClientAndOpts = (bodyshop) => {
const cfg = getRRConfigFromBodyshop(bodyshop);
const client = new RRClient({
baseUrl: cfg.baseUrl,
username: cfg.username,
password: cfg.password,
timeoutMs: cfg.timeoutMs,
retries: cfg.retries
});
const opts = {
routing: cfg.routing,
envelope: {
sender: {
component: "Rome",
task: "CU",
referenceId: "Insert",
creator: "RCI",
senderName: "RCI"
}
}
};
return { client, opts };
};
/**
* Strip all non-digit characters from a string
* @param s
* @returns {string}
*/
const digitsOnly = (s) => {
return String(s || "").replace(/\D/g, "");
};
/**
* Build RR customer payload from job.ownr_* fields, with optional overrides.
* @param job
* @param overrides
* @returns {{ibFlag: string, firstName, lastName, customerName, createdBy, customerType, addresses: [{type, line1: *, line2, city, state, postalCode, country}], phones: {number: *}[], emails: [{address: string}]}}
*/
const 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 phone1 = digitsOnly(job?.ownr_ph1);
const phone2 = digitsOnly(job?.ownr_ph2);
let primaryPhone = phone1 || phone2;
if (primaryPhone) {
primaryPhone = primaryPhone.slice(-10); // enforce 10 digits
}
const phones = primaryPhone ? [{ number: primaryPhone, type: "H" }] : [];
// 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 = {
ibFlag, // 'I' or 'B'
firstName: firstName || undefined,
lastName: lastName || undefined,
customerName: companyName || undefined,
createdBy: overrides.createdBy || InstanceManager({ imex: "ImEX Online", rome: "Rome Online" }),
customerType: overrides.customerType || "R", // Retail default
addresses,
phones,
emails
};
Object.keys(payload).forEach((k) => payload[k] === undefined && delete payload[k]);
return payload;
};
/**
* Create a customer in RR and return { customerNo, raw }.
* Maps data.dmsRecKey -> customerNo for compatibility with existing callers.
*/
const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
const { client, opts } = buildClientAndOpts(bodyshop);
const payload = buildCustomerPayloadFromJob(job, overrides);
const safePayload = sanitizeRRCustomerPayload(payload);
// Story step: clearly show we are about to hit Reynolds insertCustomer
CreateRRLogEvent(socket, "DEBUG", "{CU} insertCustomer: begin", {
ibFlag: safePayload.ibFlag,
hasAddress: Array.isArray(safePayload.addresses) && safePayload.addresses.length > 0,
hasPhones: Array.isArray(safePayload.phones) && safePayload.phones.length > 0,
hasEmails: Array.isArray(safePayload.emails) && safePayload.emails.length > 0
});
let response;
try {
response = await client.insertCustomer(safePayload, opts);
// Very noisy; only show when log level is cranked to SILLY
CreateRRLogEvent(
socket,
"SILLY",
"{CU} insertCustomer: raw response",
withRRRequestXml(response, { response })
);
} catch (e) {
CreateRRLogEvent(
socket,
"ERROR",
"RR insertCustomer transport error",
withRRRequestXml(e, {
message: e?.message,
code: e?.code,
status: e?.meta?.status || e?.status,
payload: safePayload
})
);
throw e;
}
const data = response?.data ?? response;
const trx = response?.statusBlocks?.transaction;
let customerNo = data?.dmsRecKey;
if (!customerNo) {
CreateRRLogEvent(
socket,
"ERROR",
"RR insertCustomer returned no dmsRecKey/custNo",
withRRRequestXml(response, {
status: trx?.status,
statusCode: trx?.statusCode,
message: trx?.message,
data
})
);
throw new Error(
`RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${
trx?.message ? ` msg=${trx.message}` : ""
})`
);
}
const out = { customerNo: String(customerNo), raw: data };
CreateRRLogEvent(socket, "INFO", "{CU} insertCustomer: success", {
customerNo: out.customerNo,
status: trx || null
});
return out;
};
module.exports = {
createRRCustomer
};