// server/rr/rr-job-exports.js 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("../cdk/cdk-calculate-allocations").default; /** * Step 1: Export a job to Reynolds & Reynolds as a *new* Repair Order. * * This is the "create" phase only: * - We always call client.createRepairOrder(payload, opts) * - Any follow-up / finalUpdate is handled by finalizeRRRepairOrder * * When in RR mode (bodyshop.rr_dealerid truthy), we also: * - Extract responsibility center config (for logging / debugging) * - Run CdkCalculateAllocations to produce the allocations array * (with profitCenter/costCenter + rr_gogcode / rr_item_type / rr_cust_txbl_flag) * - Derive a default RR OpCode + TaxCode (if configured) * - Pass bodyshop, allocations, opCode, taxCode into buildRRRepairOrderPayload * so it can create the payload fields used by reynolds-rome-client: * - header fields (CustNo, AdvNo, DeptType, Vin, etc.) * - RO.rolabor (ops[]) * - RO.rogg (ops[]) * - RO.tax (TaxCodeInfo) * * @param args * @returns {Promise<{success, data: *, roStatus: *, statusBlocks, customerNo: string, svId: string|null, roNo: *}>} */ 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; if (bodyshop.rr_dealerid) { // 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 { allocations = await CdkCalculateAllocations(socket, job.id); CreateRRLogEvent(socket, "SILLY", "RR allocations resolved", { hasAllocations: Array.isArray(allocations), count: Array.isArray(allocations) ? allocations.length : 0 }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "Failed to calculate RR allocations", { message: e?.message, stack: e?.stack }); allocations = null; // We still proceed with a header-only RO if this fails. } // 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_default_opcode || bodyshop.rr_opcode || rrCentersConfig?.defaultOpCode || rrCentersConfig?.rrDefaultOpCode || "51DOZ"; // 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; } // 4) TaxCode (for header-level tax, e.g. state/prov tax) const baseTaxCode = bodyshop.rr_default_taxcode || bodyshop.rr_tax_code || rrCentersConfig?.defaultTaxCode || rrCentersConfig?.rrDefaultTaxCode || "TEST"; // TODO Change / implement default handling policy if (baseTaxCode) { taxCode = String(baseTaxCode).trim() || null; } CreateRRLogEvent(socket, "SILLY", "RR op/tax config resolved", { opCode, taxCode }); } // 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, taxCode }); const response = await client.createRepairOrder(payload, finalOpts); CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", { payload, response }); const data = response?.data || null; const roStatus = data?.roStatus || null; // Extract canonical roNo you'll need for finalize step const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null; return { success: response?.success === true || roStatus?.status === "Success", data, roStatus, statusBlocks: response?.statusBlocks || [], customerNo: String(selected), // svId comes from the earlier ensureRRServiceVehicle call (if the caller passes it) svId, roNo }; }; /** * Step 2: Finalize an RR Repair Order by sending finalUpdate: "Y". * This is the *update* phase. * * We intentionally do NOT send Rogog/Rolabor here — all of that is pushed on * create; finalize is just a header-level update (FinalUpdate + estimateType). * * @param args * @returns {Promise<{success, data: *, roStatus: *, statusBlocks}>} */ 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 roStatus = data?.roStatus || null; return { success: rrRes?.success === true || roStatus?.status === "Success", data, roStatus, statusBlocks: rrRes?.statusBlocks || [] }; }; module.exports = { exportJobToRR, finalizeRRRepairOrder };