Files
bodyshop/server/rr/rr-job-export.js

174 lines
5.7 KiB
JavaScript

const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
const { buildClientAndOpts } = require("./rr-lookup");
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
const RRLogger = require("./rr-logger");
/**
* Export a job to Reynolds & Reynolds as a Repair Order (create or update).
* @param args
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, xml: *, parsed: any, customerNo: string, svId: null, roNo: *}>}
*/
const exportJobToRR = async (args) => {
const { bodyshop, job, advisorNo, selectedCustomer, existing, socket } = args || {};
const log = RRLogger(socket, { ns: "rr-export" });
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);
const finalOpts = {
...opts,
envelope: {
...(opts?.envelope || {}),
sender: {
...(opts?.envelope?.sender || {}),
task: "BSMRO",
// If we have an existing RO number we'll be updating, otherwise inserting
referenceId: existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId ? "Update" : "Insert"
}
}
};
// Ensure service vehicle for create flows (best-effort)
let svId = null;
if (!(existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId)) {
try {
const svRes = await ensureRRServiceVehicle({
bodyshop,
job,
overrides: {},
customerNo: String(selected),
socket
});
svId = svRes?.svId || null;
log("info", "RR service vehicle ensured", { created: svRes?.created, svId });
} catch (e) {
log("warn", "RR ensure service vehicle failed; continuing", { error: e?.message });
}
}
// Build RO payload for create/update
const payload = buildRRRepairOrderPayload({
job,
selectedCustomer: { customerNo: String(selected), custNo: String(selected) },
advisorNo: String(advisorNo)
});
// Canonical update key is "roNo" (prefer DMS RO number); accept fallbacks from "existing"
const roNoForUpdate = existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId || null;
const rrRes = roNoForUpdate
? await client.updateRepairOrder({ ...payload, roNo: String(roNoForUpdate) }, finalOpts) // ✅ use roNo on update
: await client.createRepairOrder(payload, finalOpts);
const data = rrRes?.data || null;
const roStatus = data?.roStatus || null;
// Extract canonical roNo you'll need for finalize step
const roNo =
data?.dmsRoNo ??
data?.outsdRoNo ??
roStatus?.dmsRoNo ??
roStatus?.DMSRoNo ??
roStatus?.outsdRoNo ??
roStatus?.OutsdRoNo ??
null;
return {
success: rrRes?.success === true || roStatus?.status === "Success",
data,
roStatus,
statusBlocks: rrRes?.statusBlocks || [],
xml: rrRes?.xml,
parsed: rrRes?.parsed,
customerNo: String(selected),
svId,
roNo
};
};
/**
* Finalize an RR Repair Order by sending finalUpdate: "Y".
* @param args
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, xml: *, parsed: any}>}
*/
const finalizeRRRepairOrder = async (args) => {
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
const log = RRLogger(socket, { ns: "rr-finalize" });
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,
mileageIn: job?.kmin,
mileageOut: job?.kmout,
estimate: { estimateType: "Final" }
};
log("info", "RR finalize updateRepairOrder", {
roNo: roNoToSend,
outsdRoNo: String(externalRo),
customerNo: String(customerNo),
advisorNo: String(advisorNo)
});
const rrRes = await client.updateRepairOrder(payload, finalOpts);
const data = rrRes?.data || null;
const roStatus = data?.roStatus || null;
return {
success: rrRes?.success === true || roStatus?.status === "Success",
data,
roStatus,
statusBlocks: rrRes?.statusBlocks || [],
xml: rrRes?.xml,
parsed: rrRes?.parsed
};
};
module.exports = { exportJobToRR, finalizeRRRepairOrder };