303 lines
10 KiB
JavaScript
303 lines
10 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;
|
|
const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
|
|
/**
|
|
* 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 = [];
|
|
}
|
|
|
|
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
|
|
|
|
const opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
|
|
|
if (opCodeOverride || resolvedBaseOpCode) {
|
|
opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
|
|
}
|
|
|
|
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
|
|
opCode,
|
|
baseFromConfig: resolvedBaseOpCode
|
|
});
|
|
|
|
// 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 };
|