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

298 lines
9.8 KiB
JavaScript

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 {
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_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 };