/** * Get last 8 chars of a string, uppercased * @param v * @returns {string} */ const last8 = (v) => { return (String(v || "") || "").slice(-8).toUpperCase(); }; /** * Extract owner customer numbers from VIN-based blocks * @param blocks * @param jobVin * @returns {Set} */ const ownersFromVinBlocks = (blocks = [], jobVin = null) => { const out = new Set(); const want8 = jobVin ? last8(jobVin) : null; for (const blk of Array.isArray(blocks) ? blocks : []) { const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : []; for (const sv of serv) { const svVin = String(sv?.Vehicle?.Vin || ""); if (want8 && last8(svVin) !== want8) continue; const custNo = sv?.VehicleServInfo?.CustomerNo; if (custNo != null && String(custNo).trim() !== "") { out.add(String(custNo).trim()); } } } return out; }; /** * Make vehicle search payload from job data * @param job * @returns {null|{kind: string, license: string, maxResults: number}|{kind: string, vin: string, maxResults: number}} */ const makeVehicleSearchPayloadFromJob = (job) => { const vin = job?.v_vin; if (vin) return { kind: "vin", vin: String(vin).trim(), maxResults: 50 }; const plate = job?.plate_no; if (plate) return { kind: "license", license: String(plate).trim(), maxResults: 50 }; return null; }; /** * Normalize customer candidates from VIN/name blocks, including address + owner flag * IMPORTANT: If no ServVehicle/CustomerNo exists (e.g. name-only hits), fall back to NameRecId. * @param res * @param ownersSet * @returns {any[]} */ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => { const blocks = Array.isArray(res?.data) ? res.data : []; const out = []; const pickAddr = (addrArr) => { const arr = Array.isArray(addrArr) ? addrArr : addrArr ? [addrArr] : []; if (!arr.length) return null; const chosen = arr.find((a) => (a?.Type || a?.type || "").toString().toUpperCase() === "P") || arr[0]; const line1 = chosen?.Addr1 ?? chosen?.AddressLine1 ?? chosen?.Line1 ?? chosen?.Street1 ?? undefined; const line2 = chosen?.Addr2 ?? chosen?.AddressLine2 ?? chosen?.Line2 ?? chosen?.Street2 ?? undefined; const city = chosen?.City ?? chosen?.city ?? undefined; const state = chosen?.State ?? chosen?.StateOrProvince ?? chosen?.state ?? undefined; const postalCode = chosen?.Zip ?? chosen?.PostalCode ?? chosen?.zip ?? undefined; const country = chosen?.Country ?? chosen?.CountryCode ?? chosen?.country ?? undefined; const county = chosen?.County ?? chosen?.county ?? undefined; // << added if (!line1 && !city && !state && !postalCode && !country && !county) return null; return { line1, line2, city, state, postalCode, country, county }; }; for (const blk of blocks) { const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : []; const custNos = serv.map((sv) => sv?.VehicleServInfo?.CustomerNo).filter(Boolean); const nci = blk?.NameContactId; const ind = nci?.NameId?.IndName; const bus = nci?.NameId?.BusName; const personal = [ind?.FirstName ?? ind?.FName, ind?.LastName ?? ind?.LName].filter(Boolean).join(" ").trim(); const company = bus?.CompanyName ?? bus?.BName; const name = (personal || company || "").trim(); const address = pickAddr(nci?.Address); // fallback to NameRecId when no ServVehicle/CustomerNo exists (e.g., pure name search) const nameRecIdRaw = nci?.NameId?.NameRecId; const nameRecId = nameRecIdRaw != null ? String(nameRecIdRaw).trim() : ""; if (Array.isArray(custNos) && custNos.length > 0) { for (const custNo of custNos) { const cno = String(custNo).trim(); if (!cno) continue; const item = { custNo: cno, name: name || `Customer ${cno}`, address: address || undefined }; if (ownersSet && ownersSet.has(cno)) { item.isVehicleOwner = true; item.vinOwner = true; } out.push(item); } } else if (nameRecId) { // Use NameRecId as the identifier const cno = nameRecId; const item = { custNo: cno, name: name || `Customer ${cno}`, address: address || undefined }; out.push(item); } } // Deduplicate by custNo, merge owner/address flags const byId = new Map(); for (const c of out) { const key = (c.custNo || "").trim(); if (!key) continue; const prev = byId.get(key); if (!prev) byId.set(key, c); else { byId.set(key, { ...prev, isVehicleOwner: prev.isVehicleOwner || c.isVehicleOwner, vinOwner: prev.vinOwner || c.vinOwner, address: prev.address || c.address }); } } return Array.from(byId.values()); }; /** * Read advisor number from payload or cached value * @param payload * @param cached * @returns {string|null} */ const readAdvisorNo = (payload, cached) => { const tx = payload?.txEnvelope || payload?.envelope || {}; const get = (v) => (v != null && String(v).trim() !== "" ? String(v).trim() : null); return get(tx?.advisorNo) || get(payload?.advisorNo) || get(cached) || null; }; /** * Cache enum keys for RR session transaction data * @type {{txEnvelope: string, JobData: string, SelectedCustomer: string, AdvisorNo: string, VINCandidates: string, SelectedVin: string, ExportResult: string, PendingRO: string}} */ const RRCacheEnums = { txEnvelope: "RR.txEnvelope", JobData: "RR.JobData", SelectedCustomer: "RR.SelectedCustomer", AdvisorNo: "RR.AdvisorNo", VINCandidates: "RR.VINCandidates", SelectedVin: "RR.SelectedVin", ExportResult: "RR.ExportResult", PendingRO: "RR.PendingRO" }; /** * Get transaction type string for job ID * @param jobid * @returns {`rr:${string}`} */ const getTransactionType = (jobid) => `rr:${jobid}`; /** * Default RR TTL (1 hour) * @type {number} */ const defaultRRTTL = 60 * 60; /** * Resolve RR OpCode from bodyshop.rr_configuration.defaults * Is Duplicated on client/src/utils/dmsUtils.js * @param bodyshop * @returns {string} */ const resolveRROpCodeFromBodyshop = (bodyshop) => { if (!bodyshop) throw new Error("bodyshop is required"); const cfg = bodyshop?.rr_configuration || {}; const defaults = cfg?.defaults || {}; const prefix = (defaults.prefix ?? "").toString().trim(); const base = (defaults.base ?? "").toString().trim(); const suffix = (defaults.suffix ?? "").toString().trim(); if (!prefix && !base && !suffix) { throw new Error("No RR OpCode parts found in bodyshop configuration"); } return `${prefix}${base}${suffix}`; }; module.exports = { RRCacheEnums, defaultRRTTL, getTransactionType, ownersFromVinBlocks, makeVehicleSearchPayloadFromJob, normalizeCustomerCandidates, readAdvisorNo, resolveRROpCodeFromBodyshop };