const { buildRRRepairOrderPayload } = require("./rr-job-helpers"); const { buildClientAndOpts } = require("./rr-lookup"); const CreateRRLogEvent = require("./rr-logger-event"); const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers"); const CdkCalculateAllocations = require("./rr-calculate-allocations").default; /** * Derive RR status information from response object. * @param rrRes * @returns {{status: *, statusCode: *|undefined, message}|null} */ const deriveRRStatus = (rrRes = {}) => { const data = rrRes.data || {}; const tx = rrRes.statusBlocks && rrRes.statusBlocks.transaction; const pick = (obj, ...keys) => { if (!obj) return undefined; for (const k of keys) { if (obj[k] != null) return obj[k]; } return undefined; }; let status = pick(data.roStatus, "status", "Status") || pick(data, "status", "Status") || pick(tx, "status", "Status"); let statusCode = pick(data.roStatus, "statusCode", "StatusCode") || pick(data, "statusCode", "StatusCode") || pick(tx, "statusCode", "StatusCode"); let message = pick(data.roStatus, "message", "Message") || data.errorMessage || pick(tx, "message", "Message"); // Last resort: parse from XML if present if ((!status || !message) && typeof rrRes.xml === "string") { const m = rrRes.xml.match(/<(?:GenTransStatus|TransStatus)\b([^>]*)>([^<]*)<\/(?:GenTransStatus|TransStatus)>/i); if (m) { const attrs = m[1] || ""; const body = (m[2] || "").trim(); const statusMatch = attrs.match(/\bStatus="([^"]*)"/i); const codeMatch = attrs.match(/\bStatusCode="([^"]*)"/i); if (!status && statusMatch) status = statusMatch[1]; if (!statusCode && codeMatch) statusCode = codeMatch[1]; if (!message && body) message = body; } } if (!status && !statusCode && !message) return null; return { status, statusCode: statusCode != null && statusCode !== "" ? statusCode : undefined, message: message || undefined }; }; /** * Step 1: Export a job to RR as a new Repair Order. * @param args * @returns {Promise<{success: boolean, data: *, roStatus: {status: *, statusCode: *|undefined, message}, statusBlocks: *|{}, customerNo: string, svId: *, roNo: *, xml: *}>} */ const exportJobToRR = async (args) => { const { bodyshop, job, advisorNo, selectedCustomer, txEnvelope, socket, svId } = args || {}; if (!bodyshop) throw new Error("exportJobToRR: bodyshop is required"); if (!job) throw new Error("exportJobToRR: job is required"); if (advisorNo == null || String(advisorNo).trim() === "") { throw new Error("exportJobToRR: advisorNo is required for RR"); } // Resolve customer number (accept multiple shapes) const selected = selectedCustomer?.customerNo || selectedCustomer?.custNo; if (!selected) throw new Error("exportJobToRR: selectedCustomer.custNo/customerNo is required"); const { client, opts } = buildClientAndOpts(bodyshop); // For step 1 we always "Insert" (create). Finalize handles the update. const finalOpts = { ...opts, envelope: { ...(opts?.envelope || {}), sender: { ...(opts?.envelope?.sender || {}), task: "BSMRO", referenceId: "Insert" } } }; const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null; const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null; // RR-only extras let rrCentersConfig = null; let allocations = null; let opCode = null; // let taxCode = null; // 1) Responsibility center config (for visibility / debugging) try { rrCentersConfig = extractRrResponsibilityCenters(bodyshop); CreateRRLogEvent(socket, "SILLY", "RR responsibility centers resolved", { hasCenters: !!bodyshop.md_responsibility_centers, profitCenters: Object.keys(rrCentersConfig?.profitsByName || {}), costCenters: Object.keys(rrCentersConfig?.costsByName || {}), dmsCostDefaults: rrCentersConfig?.dmsCostDefaults || {}, dmsProfitDefaults: rrCentersConfig?.dmsProfitDefaults || {} }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "Failed to resolve RR responsibility centers", { message: e?.message, stack: e?.stack }); } // 2) Allocations (sales + cost by center, with rr_* metadata already attached) try { const allocResult = await CdkCalculateAllocations(socket, job.id); // We only need the per-center job allocations for RO.GOG / ROLABOR. allocations = Array.isArray(allocResult?.jobAllocations) ? allocResult.jobAllocations : []; CreateRRLogEvent(socket, "SILLY", "RR allocations resolved", { hasAllocations: allocations.length > 0, count: allocations.length, taxAllocCount: Array.isArray(allocResult?.taxAllocArray) ? allocResult.taxAllocArray.length : 0, ttlAdjCount: Array.isArray(allocResult?.ttlAdjArray) ? allocResult.ttlAdjArray.length : 0, ttlTaxAdjCount: Array.isArray(allocResult?.ttlTaxAdjArray) ? allocResult.ttlTaxAdjArray.length : 0 }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "Failed to calculate RR allocations", { message: e?.message, stack: e?.stack }); // Proceed with a header-only RO if allocations fail. allocations = []; } // 3) OpCode (global, but overridable) // - baseOpCode can come from bodyshop or rrCentersConfig (you'll map it in onboarding) // - txEnvelope can carry an explicit override field (opCode/opcode/op_code) const baseOpCode = bodyshop?.rr_configuration?.baseOpCode || "28TOZ"; // TODO Change / implement default handling policy const opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null; if (opCodeOverride || baseOpCode) { opCode = String(opCodeOverride || baseOpCode).trim() || null; } CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", { opCode }); // Build RO payload for create. // // NOTE: // - bodyshop + allocations + opCode + taxCode are used only to build the // value object expected by reynolds-rome-client (header + rogg + rolabor + tax). const payload = buildRRRepairOrderPayload({ bodyshop, job, selectedCustomer: { customerNo: String(selected), custNo: String(selected) }, advisorNo: String(advisorNo), story, makeOverride, allocations, opCode }); const response = await client.createRepairOrder(payload, finalOpts); CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", { payload, response }); const data = response?.data || null; const statusBlocks = response?.statusBlocks || {}; const roStatus = deriveRRStatus(response); const statusUpper = roStatus?.status ? String(roStatus.status).toUpperCase() : null; let success = false; if (statusUpper) { // Treat explicit FAILURE / ERROR as hard failures success = !["FAILURE", "ERROR"].includes(statusUpper); } else if (typeof response?.success === "boolean") { // Fallback to library boolean if no explicit status success = response.success; } else if (roStatus?.status) { success = String(roStatus.status).toUpperCase() === "SUCCESS"; } // Extract canonical roNo you'll need for finalize step const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null; return { success, data, roStatus, statusBlocks, customerNo: String(selected), svId, roNo, xml: response?.xml // expose XML for logging/diagnostics }; }; /** * Step 2: Finalize an existing RR Repair Order (previously created). * @param args * @returns {Promise<{success: boolean, data: *, roStatus: {status: *, statusCode: *|undefined, message}, statusBlocks: *|{}, xml: *}>} */ const finalizeRRRepairOrder = async (args) => { const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {}; if (!bodyshop) throw new Error("finalizeRRRepairOrder: bodyshop is required"); if (!job) throw new Error("finalizeRRRepairOrder: job is required"); if (!advisorNo) throw new Error("finalizeRRRepairOrder: advisorNo is required"); if (!customerNo) throw new Error("finalizeRRRepairOrder: customerNo is required"); // The external (Outsd) RO is our deterministic fallback and correlation id. const externalRo = job?.ro_number ?? job?.id; if (externalRo == null) { throw new Error("finalizeRRRepairOrder: outsdRoNo (job.ro_number/id) is required"); } // Prefer DMS RO for update; fall back to external when DMS RO isn't known const roNoToSend = roNo ? String(roNo) : String(externalRo); const { client, opts } = buildClientAndOpts(bodyshop); const finalOpts = { ...opts, envelope: { ...(opts?.envelope || {}), sender: { ...(opts?.envelope?.sender || {}), task: "BSMRO", referenceId: "Update" } } }; const cleanVin = (job?.v_vin || vin || "") .toString() .replace(/[^A-Za-z0-9]/g, "") .toUpperCase() .slice(0, 17) || undefined; // IMPORTANT: include "roNo" on updates (RR requires it). Also send outsdRoNo for correlation. const payload = { roNo: roNoToSend, // ✅ REQUIRED BY RR on update outsdRoNo: String(externalRo), finalUpdate: "Y", departmentType: "B", customerNo: String(customerNo), advisorNo: String(advisorNo), vin: cleanVin, mileageOut: job?.kmout, estimate: { estimateType: "Final" } }; CreateRRLogEvent(socket, "INFO", "Finalizing RR Repair Order", { roNo: roNoToSend, outsdRoNo: String(externalRo), customerNo: String(customerNo), advisorNo: String(advisorNo) }); const rrRes = await client.updateRepairOrder(payload, finalOpts); CreateRRLogEvent(socket, "SILLY", "RR Repair Order finalized", { payload, response: rrRes }); const data = rrRes?.data || null; const statusBlocks = rrRes?.statusBlocks || {}; const roStatus = deriveRRStatus(rrRes); const statusUpper = roStatus?.status ? String(roStatus.status).toUpperCase() : null; let success = false; if (statusUpper) { success = !["FAILURE", "ERROR"].includes(statusUpper); } else if (typeof rrRes?.success === "boolean") { success = rrRes.success; } else if (roStatus?.status) { success = String(roStatus.status).toUpperCase() === "SUCCESS"; } return { success, data, roStatus, statusBlocks, xml: rrRes?.xml }; }; module.exports = { exportJobToRR, finalizeRRRepairOrder, deriveRRStatus };