From e5ed11287d298594c149a8f961c613af32452fb9 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 7 Nov 2025 15:43:44 -0500 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - rr-Utils hardening --- server/rr/rr-customers.js | 105 ++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 39 deletions(-) diff --git a/server/rr/rr-customers.js b/server/rr/rr-customers.js index ae199313a..3014aae8c 100644 --- a/server/rr/rr-customers.js +++ b/server/rr/rr-customers.js @@ -1,3 +1,4 @@ +// File: server/rr/rr-customers.js const { RRClient } = require("./lib/index.cjs"); const { getRRConfigFromBodyshop } = require("./rr-config"); const RRLogger = require("./rr-logger"); @@ -13,8 +14,6 @@ function buildClientAndOpts(bodyshop) { 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 = { routing: cfg.routing, envelope: { @@ -30,49 +29,83 @@ function buildClientAndOpts(bodyshop) { return { client, opts }; } -// minimal field extraction function digitsOnly(s) { return String(s || "").replace(/\D/g, ""); } -function buildCustomerPayloadFromJob(job, overrides = {}) { - const firstName = overrides.firstName ?? job?.ownr_fn ?? job?.customer?.first_name ?? ""; - const lastName = overrides.lastName ?? job?.ownr_ln ?? job?.customer?.last_name ?? ""; - const company = overrides.company ?? job?.ownr_co_nm ?? job?.customer?.company_name ?? ""; +function uniq(arr) { + return Array.from(new Set(arr)); +} - // Prefer owner phone; fall back to customer phones - const phone = - overrides.phone ?? - job?.ownr_ph1 ?? - job?.customer?.mobile ?? - job?.customer?.home_phone ?? - job?.customer?.phone ?? - ""; +/** + * Build a payload that matches the RR client expectations for insert/update: + * - ibFlag: 'I' (individual) or 'B' (business). If we have a first name, default to 'I', else 'B' if company present. + * - Must include lastName OR customerName. + * - addresses[] / phones[] / emails[] per the library’s toView() contract. + */ +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 = { - // These keys follow the RR client’s conventions; the lib normalizes case internally. + ibFlag, // 'I' or 'B' firstName: firstName || undefined, lastName: lastName || undefined, - companyName: company || undefined, - phone: digitsOnly(phone) || undefined, - email: overrides.email || job?.ownr_ea || job?.customer?.email || undefined, - address: { - line1: overrides.addressLine1 ?? job?.ownr_addr1 ?? job?.customer?.address_line1 ?? undefined, - line2: overrides.addressLine2 ?? job?.ownr_addr2 ?? job?.customer?.address_line2 ?? undefined, - 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" - } + customerName: companyName || undefined, + createdBy: overrides.createdBy || "ImEX 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 { custNo, raw }. - * NOTE: The library returns { data: { dmsRecKey, status, statusCode }, statusBlocks, ... }. - * We map data.dmsRecKey -> custNo for compatibility with existing callers. + * Create a customer in RR and return { customerNo, raw }. + * Maps data.dmsRecKey -> customerNo for compatibility with existing callers. */ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) { const log = RRLogger(socket, { ns: "rr" }); @@ -83,16 +116,14 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) { try { res = await client.insertCustomer(payload, opts); } 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; } - const data = res?.data ?? res; // be tolerant to shapes + const data = res?.data ?? res; const trx = res?.statusBlocks?.transaction; - // Primary: map dmsRecKey -> custNo let customerNo = data?.dmsRecKey; - if (!customerNo) { log("error", "RR insertCustomer returned no dmsRecKey/custNo", { status: trx?.status, @@ -107,11 +138,7 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) { ); } - // Normalize to string for safety - customerNo = String(customerNo); - - // Preserve existing return shape so callers don’t need changes - return { customerNo, raw: data }; + return { customerNo: String(customerNo), raw: data }; } module.exports = {