// File: server/rr/rr-service-vehicles.js // Idempotent Service Vehicle ensure: if VIN exists (owner match or not), don't fail. const RRLogger = require("./rr-logger"); const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup"); // --- helpers --- function pickVin({ vin, job }) { const v = vin || job?.v_vin || job?.vehicle?.vin || job?.vin || job?.vehicleVin || null; if (!v) return ""; return String(v) .replace(/[^A-Za-z0-9]/g, "") .toUpperCase() .slice(0, 17); } function pickCustNo({ selectedCustomerNo, custNo, customerNo }) { const c = selectedCustomerNo ?? custNo ?? customerNo ?? null; return c != null ? String(c).trim() : ""; } function ownersFromCombined(res, wantedVin) { const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []; const owners = new Set(); for (const blk of blocks) { const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : []; for (const sv of serv) { const v = sv?.Vehicle || {}; const vin = (v?.Vin || v?.VIN || v?.vin || "").toString().toUpperCase(); const cust = sv?.VehicleServInfo?.CustomerNo; if (vin && cust && (!wantedVin || vin === wantedVin)) { owners.add(String(cust)); } } } return owners; } function isAlreadyExistsError(e) { if (!e) return false; if (e.code === 300) return true; const msg = (e.message || "").toUpperCase(); return msg.includes("ALREADY EXISTS") || msg.includes("VEHICLE ALREADY EXISTS"); } /** * Ensure/create a Service Vehicle in RR for the given VIN + customer. * Args may contain: * - client, routing (prebuilt) OR bodyshop (we'll build a client) * - vin OR job (we'll derive vin from job) * - selectedCustomerNo / custNo / customerNo * - license (optional) * - socket (for logging) * - logNs (namespace for logs) * * Returns: { created:boolean, exists:boolean, vin, customerNo, svId?, status? } */ async function ensureRRServiceVehicle(args = {}) { const { client: inClient, routing: inRouting, bodyshop, job, vin, selectedCustomerNo, custNo, customerNo, license, socket, logNs = "rr-service-vehicles" } = args; const log = RRLogger(socket, { ns: logNs }); // Build/derive essentials let client = inClient; let routing = inRouting; if (!client || !routing) { if (!bodyshop) { throw new Error("ensureRRServiceVehicle: either (client & routing) or bodyshop is required"); } const built = buildClientAndOpts(bodyshop); client = built.client; routing = built.opts?.routing; } const vinStr = pickVin({ vin, job }); const custNoStr = pickCustNo({ selectedCustomerNo, custNo, customerNo }); if (!vinStr) throw new Error("ensureRRServiceVehicle: vin required"); if (!custNoStr) throw new Error("ensureRRServiceVehicle: customerNo required"); // --- Preflight: does VIN already exist (and under whom)? --- try { let owners = new Set(); if (bodyshop) { const comb = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 }); owners = ownersFromCombined(comb, vinStr); } // Short-circuit: VIN exists anywhere -> don't try to insert if (owners.size > 0) { const ownedBySame = owners.has(custNoStr); log(ownedBySame ? "info" : "warn", "{SV} VIN already present in RR; skipping insert", { vin: vinStr, selectedCustomerNo: custNoStr, owners: Array.from(owners) }); return { created: false, exists: true, vin: vinStr, customerNo: custNoStr, ownedBySame }; } } catch (e) { // Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled) log("warn", "{SV} VIN preflight lookup failed; continuing to insert", { vin: vinStr, error: e?.message }); } // --- Attempt insert (idempotent) --- // IMPORTANT: The current RR lib build validates `vehicleServInfo.customerNo`. // To be future-proof, we also include top-level `customerNo`. const insertPayload = { vin: vinStr, customerNo: custNoStr, // fallback form (some builds accept this) vehicleServInfo: { customerNo: custNoStr }, // primary form expected by the lib vehicleDetail: license ? { licNo: String(license).trim() } : undefined }; const insertOpts = { routing, envelope: { sender: { component: "Rome", task: "SV", referenceId: "Insert", creator: "RCI", senderName: "RCI" } } }; log("debug", "{SV} Inserting service vehicle", { vin: vinStr, selectedCustomerNo: custNoStr, payloadShape: Object.keys(insertPayload).filter((k) => insertPayload[k] != null) }); // Be tolerant to method name differences const fnName = typeof client.insertServiceVehicle === "function" ? "insertServiceVehicle" : typeof client.serviceVehicleInsert === "function" ? "serviceVehicleInsert" : null; if (!fnName) { throw new Error("RR client does not expose insertServiceVehicle/serviceVehicleInsert"); } try { const res = await client[fnName](insertPayload, insertOpts); const data = res?.data ?? {}; const svId = data?.dmsRecKey || data?.svId || undefined; log("info", "{SV} insertServiceVehicle: success", { vin: vinStr, customerNo: custNoStr, svId }); return { created: true, exists: false, vin: vinStr, customerNo: custNoStr, svId, status: res?.statusBlocks?.transaction }; } catch (e) { if (isAlreadyExistsError(e)) { // Treat as idempotent success log("warn", "{SV} insertServiceVehicle: already exists; treating as success", { vin: vinStr, customerNo: custNoStr, code: e?.code, status: e?.meta?.status || e?.status }); return { created: false, exists: true, vin: vinStr, customerNo: custNoStr, status: e?.meta?.status || e?.status }; } log("error", "{SV} insertServiceVehicle: failure", { message: e?.message, code: e?.code, status: e?.meta?.status || e?.status }); throw e; } } module.exports = { ensureRRServiceVehicle };