// server/rr/rr-service-vehicles.js const { RRClient } = require("./lib/index.cjs"); const { getRRConfigFromBodyshop } = require("./rr-config"); const RRLogger = require("./rr-logger"); const { ownersFromVinBlocks, getTransactionType, defaultRRTTL, RRCacheEnums } = require("../rr/rr-utils"); function 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: "SV", // default for insertServiceVehicle referenceId: "Insert", creator: "RCI", senderName: "RCI" } } }; return { client, opts }; } function buildServiceVehiclePayload({ job, custNo, overrides = {} }) { // Helpers const isBlank = (v) => v == null || (typeof v === "string" && v.trim() === ""); const toStr = (v) => (v == null ? undefined : String(v)); const toNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : undefined; }; const dropEmptyDeep = (obj) => { if (Array.isArray(obj)) { const arr = obj .map(dropEmptyDeep) .filter( (v) => !( v == null || (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0) || (typeof v === "string" && v.trim() === "") ) ); return arr.length ? arr : undefined; } if (obj && typeof obj === "object") { const out = {}; for (const [k, v] of Object.entries(obj)) { const vv = dropEmptyDeep(v); if (!(vv == null || (typeof vv === "string" && vv.trim() === ""))) out[k] = vv; } return Object.keys(out).length ? out : undefined; } return obj; }; // Derive odometer + units from job let odometer = undefined; let odometerUnits = undefined; if (!isBlank(job?.kmout)) { odometer = toNum(job.kmout); odometerUnits = "KM"; } else if (!isBlank(job?.kmin)) { odometer = toNum(job.kmin); odometerUnits = "KM"; } else if (!isBlank(job?.odometer)) { odometer = toNum(job.odometer); // if you know this is miles in your dataset, set "MI"; otherwise omit units } // Map common vehicle descriptors const year = overrides.year ?? job?.v_model_yr; const modelDesc = overrides.modelDesc ?? job?.v_model_desc; const makeDesc = overrides.makeDesc ?? job?.v_make_desc; // not in the spec list, used only to help build `carline` const extClrDesc = overrides.extClrDesc ?? job?.v_color; const intClrDesc = overrides.intClrDesc ?? job?.v_int_color; const trimDesc = overrides.trimDesc ?? job?.v_trim; const bodyStyle = overrides.bodyStyle ?? job?.v_body_style; const engineDesc = overrides.engineDesc ?? job?.v_engine_desc ?? job?.engine_desc; const transDesc = overrides.transDesc ?? job?.v_trans_desc ?? job?.trans_desc; // Carline: prefer explicit field; otherwise derive from make+model when both are present const carline = overrides.carline ?? job?.v_carline ?? (makeDesc && modelDesc ? `${String(makeDesc).trim()} ${String(modelDesc).trim()}` : undefined); // Advisor (NameRecId) if you have it handy (often provided via txEnvelope or cache) – pass in overrides.advisorNameRecId const advisorNameRecId = overrides.advisorNameRecId; // Optional warranty fields (pass through via overrides.*) const vehExtWarranty = (() => { const contractNumber = overrides?.vehExtWarranty?.contractNumber ?? job?.warranty_contract_no; const expirationDate = overrides?.vehExtWarranty?.expirationDate ?? job?.warranty_exp_date; const expirationMileage = overrides?.vehExtWarranty?.expirationMileage ?? job?.warranty_exp_mileage; const candidate = { contractNumber: isBlank(contractNumber) ? undefined : String(contractNumber), expirationDate: isBlank(expirationDate) ? undefined : String(expirationDate), expirationMileage: isBlank(expirationMileage) ? undefined : String(expirationMileage) }; const any = Object.values(candidate).some((v) => !isBlank(v)); return any ? candidate : undefined; })(); // Sales/team/in-service/mileage optional fields const salesmanNo = overrides.salesmanNo ?? job?.salesman_no; const inServiceDate = overrides.inServiceDate ?? job?.in_service_date; const svMileage = overrides.mileage ?? odometer; // mirror root odometer if you want const teamCode = overrides.teamCode ?? job?.team_code; // Build raw payload (include everything we can; drop empties after) const raw = { // Required vin: toStr(overrides.vin ?? job?.v_vin), // Optional attributes modelDesc: toStr(modelDesc), carline: toStr(carline), extClrDesc: toStr(extClrDesc), intClrDesc: toStr(intClrDesc), trimDesc: toStr(trimDesc), bodyStyle: toStr(bodyStyle), engineDesc: toStr(engineDesc), transDesc: toStr(transDesc), // Optional elements year: toStr(year), odometer, odometerUnits: toStr(overrides.odometerUnits ?? odometerUnits), // VehicleDetail vehicleDetail: { licNo: toStr(overrides.licNo ?? job?.plate_no) }, // VehicleServInfo (CustomerNo is required) vehicleServInfo: { customerNo: toStr(custNo), salesmanNo: toStr(salesmanNo), inServiceDate: toStr(inServiceDate), mileage: svMileage, teamCode: toStr(teamCode), vehExtWarranty, advisor: advisorNameRecId ? { contactInfo: { nameRecId: toStr(advisorNameRecId) } } : undefined } }; const cleaned = dropEmptyDeep(raw); if (!cleaned?.vin) throw new Error("buildServiceVehiclePayload: VIN is required"); if (!cleaned?.vehicleServInfo?.customerNo) throw new Error("buildServiceVehiclePayload: customerNo is required"); return cleaned; } /** * Ensure a Service Vehicle (for job.v_vin) exists and is associated to the given custNo. * - Prefer cached VIN blocks saved during step {2} under RR.VINCandidates * - If no cache, do a single combinedSearch by VIN, then cache it * - If not owned, insert service vehicle to associate to selected customer */ async function ensureRRServiceVehicle({ bodyshop, custNo, job, overrides = {}, socket, redisHelpers }) { const log = RRLogger(socket); const vin = job?.v_vin && String(job.v_vin).trim(); if (!vin) { log("warn", "ensureRRServiceVehicle: no VIN on job; nothing to ensure", { jobId: job?.id }); return { ensured: false, reason: "no-vin" }; } const custNoStr = String(custNo); const { client, opts } = buildClientAndOpts(bodyshop); // 1) Try cached VIN combined-search blocks from step {2} let blocks = null; if (redisHelpers && socket?.id && job?.id) { try { blocks = await redisHelpers.getSessionTransactionData( socket.id, getTransactionType(job.id), RRCacheEnums.VINCandidates ); } catch { // ignore } } // 2) If no cached blocks, do one combinedSearch (VIN) and cache it if (!Array.isArray(blocks) || blocks.length === 0) { try { const precheckRes = await client.combinedSearch( { vin, maxRecs: 50, kind: "vin" }, { ...opts, envelope: { ...opts.envelope, sender: { ...opts.envelope.sender, task: "CVC", referenceId: "Query" } } } ); // Normalize to "blocks" shape (either .data or array) blocks = Array.isArray(precheckRes?.data) ? precheckRes.data : Array.isArray(precheckRes) ? precheckRes : []; if (redisHelpers && socket?.id && job?.id) { try { await redisHelpers.setSessionTransactionData( socket.id, getTransactionType(job.id), RRCacheEnums.VINCandidates, blocks, defaultRRTTL ); } catch { // } } } catch (e) { // Non-fatal: proceed to insert log("warn", "RR combinedSearch(VIN) precheck failed; will proceed to insert", { vin, err: e?.message }); blocks = []; } } // 3) Check ownership from blocks (strict) const ownersSet = ownersFromVinBlocks(blocks, vin); if (ownersSet.has(custNoStr)) { log("info", "ensureRRServiceVehicle: already owned", { vin, custNo: custNoStr }); return { ensured: true, alreadyOwned: true }; } // 4) Not owned → insert/associate service vehicle to selected customer const payload = buildServiceVehiclePayload({ job, custNo: custNoStr, overrides }); const res = await client.insertServiceVehicle(payload, opts); const data = res?.data ?? res; // Normalize a simple id for later (RR often returns a DMS key) const svId = data?.svId ?? data?.serviceVehicleId ?? data?.dmsRecKey ?? data?.DMSRecKey; if (!svId) { log("warn", "RR insert service vehicle returned no id", { vin, custNo: custNoStr, data }); } else { log("info", "ensureRRServiceVehicle: inserted service vehicle", { vin, custNo: custNoStr, svId }); } return { ensured: true, inserted: true, svId, raw: data }; } module.exports = { ensureRRServiceVehicle, buildServiceVehiclePayload, buildClientAndOpts };