const CreateRRLogEvent = require("./rr-logger-event"); const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup"); const { QueryJobData } = require("./rr-job-helpers"); const { exportJobToRR, finalizeRRRepairOrder } = require("./rr-job-export"); const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; const { createRRCustomer } = require("./rr-customers"); const { ensureRRServiceVehicle } = require("./rr-service-vehicles"); const { classifyRRVendorError } = require("./rr-errors"); const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs"); const { makeVehicleSearchPayloadFromJob, ownersFromVinBlocks, readAdvisorNo, getTransactionType, normalizeCustomerCandidates, defaultRRTTL, RRCacheEnums } = require("./rr-utils"); const { GraphQLClient } = require("graphql-request"); const queries = require("../graphql-client/queries"); /** * Advisors cache TTL (7 days) * @type {number} */ const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds /** * Resolve job ID from various shapes * @param explicit * @param payload * @param job * @returns {*|null} */ const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || job?.id || null; /** * Resolve VIN from tx/job shapes * @param tx * @param job * @returns {*|null} */ const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null; /** * Sort vehicle owners first in the list, preserving original order otherwise. * @param list * @returns {*} */ const sortVehicleOwnerFirst = (list) => list .map((v, i) => ({ v, i })) .sort((a, b) => { const ao = a.v?.isVehicleOwner ? 1 : 0; const bo = b.v?.isVehicleOwner ? 1 : 0; if (ao !== bo) return bo - ao; return a.i - b.i; }) .map(({ v }) => v); /** * Merge customer candidates by custNo, combining isVehicleOwner flags and filling missing fields. * @param items * @returns {any[]} */ const mergeByCustNo = (items = []) => { const byId = new Map(); for (const c of items) { const id = (c?.custNo || "").trim(); if (!id) continue; const prev = byId.get(id); if (!prev) { byId.set(id, { ...c, isVehicleOwner: !!(c.vinOwner || c.isVehicleOwner) }); } else { byId.set(id, { ...prev, name: prev.name || c.name, isVehicleOwner: !!(prev.isVehicleOwner || prev.vinOwner || c.isVehicleOwner || c.vinOwner), vinOwner: !!(prev.vinOwner || c.vinOwner || prev.isVehicleOwner || c.isVehicleOwner), address: prev.address || c.address }); } } return Array.from(byId.values()); }; /** * Get session data or socket fallback * @param redisHelpers * @param socket * @returns {Promise<{bodyshopId: *, email: *, sess: null}>} */ const getSessionOrSocket = async (redisHelpers, socket) => { let sess = null; try { sess = await redisHelpers.getSessionData(socket.id); } catch { // } const bodyshopId = sess?.bodyshopId ?? socket.bodyshopId; const email = sess?.email ?? socket.user?.email; if (!bodyshopId) throw new Error("No bodyshopId (session/socket)"); return { bodyshopId, email, sess }; }; /** * Fetch bodyshop data for socket * @param bodyshopId * @param socket * @returns {Promise<{id: string, intellipay_config: {payment_map: {amex: string}}}|{id: string, intellipay_config: null}|*>} */ const getBodyshopForSocket = async ({ bodyshopId, socket }) => { const endpoint = process.env.GRAPHQL_ENDPOINT; if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured"); const token = (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token); const client = new GraphQLClient(endpoint, {}); const res = await client.setHeaders({ Authorization: `Bearer ${token}` }).request(queries.GET_BODYSHOP_BY_ID, { id: bodyshopId }); const bodyshop = res?.bodyshops_by_pk; if (!bodyshop) throw new Error(`Bodyshop not found: ${bodyshopId}`); return bodyshop; }; /** * Build advisors cache namespace and field * @param bodyshopId * @param routing * @param departmentType * @returns {{ns: string, field: string}} */ const buildAdvisorsCacheNS = ({ bodyshopId, routing, departmentType = "B" }) => { const dealer = routing?.dealerNumber || "unknown"; const store = routing?.storeNumber || "none"; const area = routing?.areaNumber || "none"; const dept = (departmentType || "B").toUpperCase(); return { ns: `rr:advisors:${bodyshopId}:${dealer}:${store}:${area}`, field: `dept:${dept}` }; }; /** * Run multi-query customer search (Full Name + VIN) and merge results. * @param bodyshop * @param job * @param socket * @param redisHelpers * @returns {Promise<*|*[]>} */ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) => { const queriesList = []; // 1) Full Name (preferred) const firstName = job?.ownr_fn && String(job.ownr_fn).trim(); const lastName = job?.ownr_ln && String(job.ownr_ln).trim(); const company = job?.ownr_co_nm && String(job.ownr_co_nm).trim(); if (firstName || lastName) { queriesList.push({ q: { kind: "name", name: { fname: firstName || undefined, lname: lastName || undefined }, maxResults: 50 }, fromVin: false }); } else if (company) { queriesList.push({ q: { kind: "name", name: { name: company }, maxResults: 50 }, fromVin: false }); } // 2) VIN (owner association) const vehQ = makeVehicleSearchPayloadFromJob(job); if (vehQ && vehQ.kind === "vin") queriesList.push({ q: vehQ, fromVin: true }); if (!queriesList.length) return []; let ownersSet = null; const merged = []; for (const { q, fromVin } of queriesList) { try { CreateRRLogEvent(socket, "DEBUG", `{RR-SEARCH} Executing ${q.kind} query`, { q }); const multiResponse = await rrCombinedSearch(bodyshop, q); CreateRRLogEvent(socket, "SILLY", "Multi Customer Search - raw combined search", { response: multiResponse }); if (fromVin) { const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : []; ownersSet = ownersFromVinBlocks(multiBlocks, job?.v_vin); try { await redisHelpers.setSessionTransactionData( socket.id, getTransactionType(job.id), RRCacheEnums.VINCandidates, multiBlocks, defaultRRTTL ); } catch { /* ignore cache write issues */ } } const norm = normalizeCustomerCandidates(multiResponse, { ownersSet }); merged.push(...norm); } catch (e) { CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", { kind: q.kind, error: e.message }); } } // NEW: dedupe across queries (name + vin) const deduped = mergeByCustNo(merged); return sortVehicleOwnerFirst(deduped); }; /** * Register RR socket events * @param socket * @param redisHelpers */ const registerRREvents = ({ socket, redisHelpers }) => { socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => { try { const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: begin", { jobid, params }); const response = await rrCombinedSearch(bodyshop, params || {}); CreateRRLogEvent(socket, "SILLY", "rr-lookup-combined: received response", { response }); let ownersSet = null; if ((params?.kind || "").toLowerCase() === "vin") { const blocks = Array.isArray(response?.data) ? response.data : []; ownersSet = ownersFromVinBlocks(blocks); } const normalized = sortVehicleOwnerFirst(normalizeCustomerCandidates(response, { ownersSet })); const rid = resolveJobId(jobid, { jobid }, null); const decorated = normalized.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner })); cb?.({ jobid: rid, data: decorated }); socket.emit("rr-select-customer", decorated); CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: emitted rr-select-customer", { count: decorated.length }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", { error: e.message, jobid }); cb?.({ jobid, error: e.message }); } }); socket.on("rr-get-advisors", async (args = {}, ack) => { const refresh = !!args?.refresh; const requestedDept = (args?.departmentType || "B").toUpperCase(); try { const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: begin", { args }); // Build routing to bind cache key to bodyshop + dealer/store/area const { client, opts } = await buildClientAndOpts(bodyshop); const routing = opts?.routing || client?.opts?.routing || {}; if (!routing?.dealerNumber) { throw new Error("rr-get-advisors: routing.dealerNumber required"); } const { ns, field } = buildAdvisorsCacheNS({ bodyshopId, routing, departmentType: requestedDept }); let result = null; let fromCache = false; // 1) Try cache (unless forced refresh) if (!refresh) { try { const cached = await redisHelpers.getProviderCache(ns, field); if (cached && Array.isArray(cached)) { result = cached; fromCache = true; CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache hit", { ns, field, count: cached.length, ttl: ADVISORS_CACHE_TTL }); } } catch (e) { CreateRRLogEvent(socket, "WARN", "rr-get-advisors: cache read failed", { ns, field, error: e?.message }); } } // 2) Fetch + cache when no cache or forced refresh if (!result) { const getAdvisorsCall = await rrGetAdvisors(bodyshop, { departmentType: requestedDept }); result = Array.isArray(getAdvisorsCall?.data) ? getAdvisorsCall.data : []; try { await redisHelpers.setProviderCache(ns, field, result, ADVISORS_CACHE_TTL); CreateRRLogEvent(socket, "SILLY", "rr-get-advisors: fetched live data", { getAdvisorsCall }); CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache populated", { ns, field, count: result.length, ttl: ADVISORS_CACHE_TTL }); } catch (e) { CreateRRLogEvent(socket, "WARN", "rr-get-advisors: cache write failed", { ns, field, error: e?.message }); } } // 3) Respond ack?.({ ok: true, result, fromCache }); socket.emit("rr-get-advisors:result", { result, fromCache }); CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: success", { count: Array.isArray(result) ? result.length : undefined, fromCache }); } catch (err) { CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", { error: err?.message }); ack?.({ ok: false, error: err?.message || "get advisors failed" }); } }); socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}) => { const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null); try { if (!rid) throw new Error("RR export: jobid required"); CreateRRLogEvent(socket, "DEBUG", `{1} Received RR export request`, { jobid: rid }); await redisHelpers.setSessionTransactionData( socket.id, getTransactionType(rid), RRCacheEnums.txEnvelope, txEnvelope || {}, defaultRRTTL ); CreateRRLogEvent(socket, "DEBUG", `{1.1} Cached txEnvelope`, { hasTxEnvelope: !!txEnvelope }); const job = await QueryJobData({ redisHelpers }, rid); await redisHelpers.setSessionTransactionData( socket.id, getTransactionType(rid), RRCacheEnums.JobData, job, defaultRRTTL ); CreateRRLogEvent(socket, "DEBUG", `{1.2} Cached JobData`, { vin: job?.v_vin, ro: job?.ro_number }); const adv = readAdvisorNo( { txEnvelope }, await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo) ); if (adv) { await redisHelpers.setSessionTransactionData( socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo, String(adv), defaultRRTTL ); CreateRRLogEvent(socket, "DEBUG", `{1.3} Cached advisorNo`, { advisorNo: String(adv) }); } const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); const bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); CreateRRLogEvent(socket, "DEBUG", `{2} Running multi-search (Full Name + VIN)`); const candidates = await rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }); const decorated = candidates.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner })); socket.emit("rr-select-customer", decorated); CreateRRLogEvent(socket, "DEBUG", `{2.1} Emitted rr-select-customer`, { count: decorated.length, anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner) }); } catch (error) { CreateRRLogEvent(socket, "ERROR", `Error during RR export (prepare)`, { error: error.message, stack: error.stack, jobid: rid }); try { socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message }); } catch { // } } }); socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => { const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null); let bodyshop = null; let job = null; let createdCustomer = false; try { if (!rid) throw new Error("jobid required"); CreateRRLogEvent(socket, "DEBUG", `{3} rr-selected-customer`, { jobid: rid, custNo, selectedCustomerId, create: !!create }); const ns = getTransactionType(rid); let selectedCustNo = (custNo && String(custNo)) || (selectedCustomerId && String(selectedCustomerId)) || (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer)); job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData); const txEnvelope = (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.txEnvelope)) || {}; if (!job) throw new Error("Staged JobData not found (run rr-export-job first)."); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); // Create customer (if requested or none chosen) if (create === true || !selectedCustNo) { CreateRRLogEvent(socket, "DEBUG", `{3.1} Creating RR customer`); const created = await createRRCustomer({ bodyshop, job, socket }); selectedCustNo = String(created?.customerNo); if (!selectedCustNo) throw new Error("RR create customer returned no custNo"); createdCustomer = true; CreateRRLogEvent(socket, "DEBUG", `{3.2} Created customer`, { custNo: selectedCustNo }); } // VIN owner pre-check try { const vehQ = makeVehicleSearchPayloadFromJob(job); if (vehQ && vehQ.kind === "vin" && job?.v_vin) { const vinResponse = await rrCombinedSearch(bodyshop, vehQ); CreateRRLogEvent(socket, "SILLY", `VIN owner pre-check response`, { response: vinResponse }); const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : []; try { await redisHelpers.setSessionTransactionData( socket.id, ns, RRCacheEnums.VINCandidates, vinBlocks, defaultRRTTL ); } catch { // } const ownersSet = ownersFromVinBlocks(vinBlocks, job.v_vin); if (ownersSet?.size) { const sel = String(selectedCustNo); if (!ownersSet.has(sel)) { const [existingOwner] = Array.from(ownersSet).map(String); CreateRRLogEvent(socket, "DEBUG", `{3.2a} VIN exists; switching to VIN owner`, { vin: job.v_vin, selected: sel, existingOwner }); selectedCustNo = existingOwner; } } } } catch (e) { CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer`, { error: e?.message }); } // Cache final/effective customer selection const effectiveCustNo = String(selectedCustNo); await redisHelpers.setSessionTransactionData( socket.id, ns, RRCacheEnums.SelectedCustomer, effectiveCustNo, defaultRRTTL ); CreateRRLogEvent(socket, "DEBUG", `{3.3} Cached selected customer`, { custNo: effectiveCustNo }); // Build client & routing const { client, opts } = await buildClientAndOpts(bodyshop); const routing = opts?.routing || client?.opts?.routing || null; if (!routing?.dealerNumber) throw new Error("ensureRRServiceVehicle: routing.dealerNumber required"); // Reconstruct a lightweight tx object (so resolveVin can use the same shape we logged at {1.2}) const tx = { jobData: { ...job, vin: job?.v_vin }, txEnvelope }; const vin = resolveVin({ tx, job }); if (!vin) { CreateRRLogEvent(socket, "ERROR", "{3.x} No VIN found for ensureRRServiceVehicle", { jobid: rid }); throw new Error("ensureRRServiceVehicle: vin required"); } CreateRRLogEvent(socket, "DEBUG", "{3.2} ensureRRServiceVehicle: starting", { jobid: rid, selectedCustomerNo: effectiveCustNo, vin, dealerNumber: routing.dealerNumber, storeNumber: routing.storeNumber, areaNumber: routing.areaNumber }); const ensured = await ensureRRServiceVehicle({ client, routing, bodyshop, selectedCustomerNo: effectiveCustNo, custNo: effectiveCustNo, customerNo: effectiveCustNo, vin, job, socket, redisHelpers }); CreateRRLogEvent(socket, "DEBUG", "{3.4} ensureRRServiceVehicle: done", ensured); const cachedAdvisor = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo); const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor); if (!advisorNo) { CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`); await insertRRFailedExportLog({ socket, jobId: rid, job, bodyshop, error: new Error("Advisor is required (advisorNo)."), classification: { errorCode: "RR_MISSING_ADVISOR", friendlyMessage: "Advisor is required." } }); socket.emit("export-failed", { vendor: "rr", jobId: rid, error: "Advisor is required (advisorNo)." }); return ack?.({ ok: false, error: "Advisor is required (advisorNo)." }); } await redisHelpers.setSessionTransactionData( socket.id, ns, RRCacheEnums.AdvisorNo, String(advisorNo), defaultRRTTL ); // CREATE/UPDATE (first step only) CreateRRLogEvent(socket, "DEBUG", `{4} Performing RR create/update (step 1)`); const result = await exportJobToRR({ bodyshop, job, selectedCustomer: { customerNo: effectiveCustNo, custNo: effectiveCustNo }, advisorNo: String(advisorNo), txEnvelope, socket, svId: ensured?.svId || null }); // Cache raw export result + pending RO number for finalize await redisHelpers.setSessionTransactionData( socket.id, ns, RRCacheEnums.ExportResult, result || {}, defaultRRTTL ); if (result?.success) { const data = result?.data || {}; // Prefer explicit return from export function; then fall back to fields const dmsRoNo = result?.roNo ?? data?.dmsRoNo ?? null; const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null; await redisHelpers.setSessionTransactionData( socket.id, ns, RRCacheEnums.PendingRO, { outsdRoNo, dmsRoNo, customerNo: String(effectiveCustNo), advisorNo: String(advisorNo), vin: job?.v_vin || null }, defaultRRTTL ); CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for validation.`, { dmsRoNo: dmsRoNo || null, outsdRoNo: outsdRoNo || null }); // Tell FE to prompt for "Finished/Close" socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo }); // Still emit info result if you want socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result }); // ACK but indicate it's pending finalize + include customer number + created flag ack?.({ ok: true, pendingFinalize: true, dmsRoNo, outsdRoNo, result, custNo: String(effectiveCustNo), createdCustomer }); } else { // classify & fail (no finalize) const vendorStatusCode = Number( result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode ); const cls = classifyRRVendorError({ code: vendorStatusCode, message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed" }); CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, { roStatus: result?.roStatus, classification: cls }); await insertRRFailedExportLog({ socket, jobId: rid, job, bodyshop, error: new Error(cls.friendlyMessage || result?.error || "RR export failed"), classification: cls, result }); socket.emit("export-failed", { vendor: "rr", jobId: rid, error: cls?.friendlyMessage || result?.error || "RR export failed", ...cls }); ack?.({ ok: false, error: cls.friendlyMessage || result?.error || "RR export failed", result, classification: cls }); } } catch (error) { const cls = classifyRRVendorError(error); CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, { error: error.message, vendorStatusCode: cls.vendorStatusCode, code: cls.errorCode, friendly: cls.friendlyMessage, stack: error.stack, jobid: rid }); try { if (!bodyshop || !job) { const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket })); job = job || (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData)); } } catch { // } await insertRRFailedExportLog({ socket, jobId: rid, job, bodyshop, error, classification: cls }); try { socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message, ...cls }); socket.emit("rr-user-notice", { jobId: rid, ...cls }); } catch { // } ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls }); } }); socket.on("rr-finalize-repair-order", async ({ jobid, jobId } = {}, ack) => { const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null); let bodyshop = null; let job = null; try { if (!rid) throw new Error("jobid required for finalize"); const ns = getTransactionType(rid); const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); bodyshop = await getBodyshopForSocket({ bodyshopId, socket }); job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData); if (!job) job = await QueryJobData({ redisHelpers }, rid); const pending = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO); const advisorNo = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo); const selectedCustomerNo = await redisHelpers.getSessionTransactionData( socket.id, ns, RRCacheEnums.SelectedCustomer ); if (!advisorNo) throw new Error("Advisor missing in session"); if (!selectedCustomerNo) throw new Error("Customer number missing in session"); // Prefer cached outsdRoNo; fall back to our deterministic external number const outsdRoNo = pending?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null; // Prefer DMS RO for update, but finalize() will safely fall back to Outsd if missing const dmsRoNo = pending?.dmsRoNo ?? pending?.roNo ?? null; CreateRRLogEvent(socket, "DEBUG", `{6} Finalizing RR RO`, { jobId: rid, outsdRoNo, dmsRoNo, advisorNo, customerNo: selectedCustomerNo }); const finalizeResult = await finalizeRRRepairOrder({ bodyshop, job, advisorNo: String(advisorNo), customerNo: String(selectedCustomerNo), roNo: dmsRoNo, // ✅ RR requires roNo; finalize() will fall back to outsdRoNo if this is absent vin: pending?.vin, socket }); if (finalizeResult?.success) { CreateRRLogEvent(socket, "INFO", `{7} Finalize success; marking exported`, { dmsRoNo, outsdRoNo }); // ✅ Mark exported + success log await markRRExportSuccess({ socket, jobId: rid, job, bodyshop, result: finalizeResult }); // Clean pending key try { await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO, null, 1); } catch { // } socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus }); ack?.({ ok: true, result: finalizeResult }); } else { const vendorStatusCode = Number( finalizeResult?.roStatus?.statusCode ?? finalizeResult?.roStatus?.StatusCode ?? finalizeResult?.statusBlocks?.transaction?.statusCode ); const cls = classifyRRVendorError({ code: vendorStatusCode, message: finalizeResult?.roStatus?.message ?? finalizeResult?.roStatus?.Message ?? finalizeResult?.error ?? "RR finalize failed" }); await insertRRFailedExportLog({ socket, jobId: rid, job, bodyshop, error: new Error(cls.friendlyMessage || finalizeResult?.error || "RR finalize failed"), classification: cls, result: finalizeResult }); socket.emit("export-failed", { vendor: "rr", jobId: rid, error: cls?.friendlyMessage || finalizeResult?.error || "RR finalize failed", ...cls }); ack?.({ ok: false, error: cls.friendlyMessage || "RR finalize failed", classification: cls }); } } catch (error) { const cls = classifyRRVendorError(error); CreateRRLogEvent(socket, "ERROR", `Error during RR finalize`, { error: error.message, vendorStatusCode: cls.vendorStatusCode, code: cls.errorCode, friendly: cls.friendlyMessage, stack: error.stack, jobid: rid }); try { if (!bodyshop || !job) { const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket); bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket })); job = job || (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData)); } } catch { // } await insertRRFailedExportLog({ socket, jobId: rid, job, bodyshop, error, classification: cls }); try { socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message, ...cls }); } catch { // } ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls }); } }); socket.on("rr-calculate-allocations", async (jobid, cb) => { try { CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: begin", { jobid }); const allocations = await CdkCalculateAllocations(socket, jobid); cb?.(allocations); socket.emit("rr-calculate-allocations:result", allocations); CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: success", { items: allocations?.length }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "rr-calculate-allocations: failed", { error: e.message, jobid }); cb?.({ ok: false, error: e.message }); } }); }; module.exports = registerRREvents;