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 };