/** * @file rr-job-export.js * @description End-to-end export of a Hasura "job" to Reynolds & Reynolds (Rome). * Orchestrates Customer (insert/update), optional Vehicle insert, and RO (create/update), * mirroring behavior of PBS/Fortellis exporters for parity. */ const RRLogger = require("./rr-logger"); const { RrApiError } = require("./rr-error"); const customerApi = require("./rr-customer"); const roApi = require("./rr-repair-orders"); const { MakeRRCall } = require("./rr-helpers"); // for optional vehicle insert const { mapServiceVehicle } = require("./rr-mappers"); /** * Decide if we should CREATE or UPDATE an entity in Rome based on external IDs */ function decideAction({ customer, vehicle, job }) { const hasCustId = !!(customer?.external_id || customer?.rr_customer_id); const hasVehId = !!(vehicle?.external_id || vehicle?.rr_vehicle_id); const hasRoId = !!(job?.external_id || job?.rr_repair_order_id || job?.dms_repair_order_id); return { customerAction: hasCustId ? "update" : "insert", vehicleAction: hasVehId ? "skip" : "insert", // Rome often generates vehicle IDs on RO create; we insert only if we have enough data and no id repairOrderAction: hasRoId ? "update" : "create" }; } /** * Normalize a stage result to a consistent structure. */ function stageOk(name, extra = {}) { return { stage: name, success: true, ...extra }; } function stageFail(name, error) { return { stage: name, success: false, error: error?.message || String(error) }; } /** * Export a job into Rome (Customer → Vehicle → RepairOrder). * @param {Socket} socket - logging context (may be null in batch) * @param {Object} job - Hasura job object (must include customer, vehicle, lines, totals) * @param {Object} bodyshopConfig - per-shop RR config (dealer/store/branch + creds) * @param {Object} options - { insertVehicleIfMissing: boolean } * @returns {Promise} normalized result */ async function exportJobToRome(socket, job, bodyshopConfig, options = {}) { const { customer = {}, vehicle = {} } = job || {}; const { insertVehicleIfMissing = true } = options; const actions = decideAction({ customer, vehicle, job }); const stages = []; const summary = { dms: "Rome", jobid: job?.id, ro_action: actions.repairOrderAction, customer_action: actions.customerAction, vehicle_action: insertVehicleIfMissing ? actions.vehicleAction : "skip" }; RRLogger(socket, "info", `RR Export start`, summary); // ---- 1) Customer ---- try { if (actions.customerAction === "insert") { const res = await customerApi.insertCustomer(socket, customer, bodyshopConfig); stages.push(stageOk("customer.insert")); summary.customer_xml = res.xml; } else { const res = await customerApi.updateCustomer(socket, customer, bodyshopConfig); stages.push(stageOk("customer.update")); summary.customer_xml = res.xml; } } catch (error) { stages.push(stageFail(`customer.${actions.customerAction}`, error)); RRLogger(socket, "error", `RR customer ${actions.customerAction} failed`, { jobid: job?.id, error: error.message }); throw new RrApiError(`Customer ${actions.customerAction} failed: ${error.message}`, "RR_CUSTOMER_ERROR"); } // ---- 2) Vehicle (optional explicit insert) ---- if (insertVehicleIfMissing && actions.vehicleAction === "insert") { try { // Only insert when we have at least VIN or plate+state/year const hasMinimumIdentity = !!(vehicle?.vin || (vehicle?.license_plate && vehicle?.license_state)); if (hasMinimumIdentity) { const data = mapServiceVehicle(vehicle, customer, bodyshopConfig); const xml = await MakeRRCall({ action: "InsertServiceVehicle", body: { template: "InsertServiceVehicle", data }, socket, dealerConfig: bodyshopConfig, jobid: job?.id }); stages.push(stageOk("vehicle.insert")); summary.vehicle_xml = xml; } else { stages.push(stageOk("vehicle.skip", { reason: "insufficient_identity" })); } } catch (error) { stages.push(stageFail("vehicle.insert", error)); RRLogger(socket, "error", `RR vehicle insert failed`, { jobid: job?.id, error: error.message }); // Non-fatal for the overall export — many flows let RO creation create/associate vehicle. } } else { stages.push(stageOk("vehicle.skip", { reason: actions.vehicleAction === "skip" ? "already_has_id" : "disabled" })); } // ---- 3) Repair Order ---- try { let res; if (actions.repairOrderAction === "create") { res = await roApi.createRepairOrder(socket, job, bodyshopConfig); stages.push(stageOk("ro.create")); } else { res = await roApi.updateRepairOrder(socket, job, bodyshopConfig); stages.push(stageOk("ro.update")); } summary.ro_xml = res.xml; } catch (error) { stages.push(stageFail(`ro.${actions.repairOrderAction}`, error)); RRLogger(socket, "error", `RR RO ${actions.repairOrderAction} failed`, { jobid: job?.id, error: error.message }); throw new RrApiError(`RepairOrder ${actions.repairOrderAction} failed: ${error.message}`, "RR_RO_ERROR"); } const result = { success: true, ...summary, stages }; RRLogger(socket, "info", `RR Export finished`, { jobid: job?.id, result: { success: result.success, customer_action: summary.customer_action, vehicle_action: summary.vehicle_action, ro_action: summary.ro_action } }); return result; } module.exports = { exportJobToRome };