648 lines
22 KiB
JavaScript
648 lines
22 KiB
JavaScript
const { buildRRRepairOrderPayload, buildMinimalRolaborFromJob } = require("./rr-job-helpers");
|
|
const { buildClientAndOpts } = require("./rr-lookup");
|
|
const CreateRRLogEvent = require("./rr-logger-event");
|
|
const { withRRRequestXml } = require("./rr-log-xml");
|
|
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
|
|
};
|
|
};
|
|
|
|
const resolveRROpCode = (bodyshop, txEnvelope = {}) => {
|
|
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
|
|
let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
|
|
|
if (!opCodeOverride) {
|
|
const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
|
|
const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
|
|
const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
|
|
|
|
if (opPrefix || opBase || opSuffix) {
|
|
const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
|
|
if (combined) {
|
|
opCodeOverride = combined;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!opCodeOverride && !resolvedBaseOpCode) return null;
|
|
return String(opCodeOverride || resolvedBaseOpCode).trim() || null;
|
|
};
|
|
|
|
/**
|
|
* Early RO Creation: Create a minimal RR Repair Order with basic info (customer, advisor, mileage, story).
|
|
* Used when creating RO from convert button or admin page before full job export.
|
|
* @param args
|
|
* @returns {Promise<{success: boolean, data: *, roStatus: {status: *, statusCode: *|undefined, message}, statusBlocks: *|{}, customerNo: string, svId: *, roNo: *, xml: *}>}
|
|
*/
|
|
const createMinimalRRRepairOrder = async (args) => {
|
|
const { bodyshop, job, advisorNo, selectedCustomer, txEnvelope, socket, svId } = args || {};
|
|
|
|
if (!bodyshop) throw new Error("createMinimalRRRepairOrder: bodyshop is required");
|
|
if (!job) throw new Error("createMinimalRRRepairOrder: job is required");
|
|
if (advisorNo == null || String(advisorNo).trim() === "") {
|
|
throw new Error("createMinimalRRRepairOrder: advisorNo is required for RR");
|
|
}
|
|
|
|
// Resolve customer number (accept multiple shapes)
|
|
const selected = selectedCustomer?.customerNo || selectedCustomer?.custNo;
|
|
if (!selected) throw new Error("createMinimalRRRepairOrder: selectedCustomer.custNo/customerNo is required");
|
|
|
|
const { client, opts } = buildClientAndOpts(bodyshop);
|
|
|
|
// For early RO creation we always "Insert" (create minimal RO)
|
|
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;
|
|
|
|
// Build minimal RO payload for early review mode.
|
|
// We keep it lightweight, but include a single labor row when we can so Ignite
|
|
// exposes the labor subsection for editing.
|
|
const cleanVin =
|
|
(job?.v_vin || "")
|
|
.toString()
|
|
.replace(/[^A-Za-z0-9]/g, "")
|
|
.toUpperCase()
|
|
.slice(0, 17) || undefined;
|
|
|
|
// Resolve mileage - must be a positive number
|
|
let mileageIn = txEnvelope?.kmin ?? job?.kmin ?? null;
|
|
if (mileageIn != null) {
|
|
mileageIn = parseInt(mileageIn, 10);
|
|
if (isNaN(mileageIn) || mileageIn < 0) {
|
|
mileageIn = null;
|
|
}
|
|
}
|
|
|
|
CreateRRLogEvent(socket, "DEBUG", "Resolved mileage for early RO", {
|
|
txEnvelopeKmin: txEnvelope?.kmin,
|
|
jobKmin: job?.kmin,
|
|
resolvedMileageIn: mileageIn
|
|
});
|
|
|
|
const earlyRoOpCode = resolveRROpCode(bodyshop, txEnvelope);
|
|
const earlyRoLabor = buildMinimalRolaborFromJob(job, {
|
|
opCode: earlyRoOpCode,
|
|
payType: "Cust"
|
|
});
|
|
|
|
const payload = {
|
|
customerNo: String(selected),
|
|
advisorNo: String(advisorNo),
|
|
vin: cleanVin,
|
|
departmentType: "B",
|
|
outsdRoNo: job?.ro_number || job?.id || undefined,
|
|
estimate: {
|
|
parts: "0",
|
|
labor: "0",
|
|
total: "0.00"
|
|
}
|
|
};
|
|
|
|
// Only add mileageIn if we have a valid value
|
|
if (mileageIn != null && mileageIn >= 0) {
|
|
payload.mileageIn = mileageIn;
|
|
}
|
|
|
|
// Add optional fields if present
|
|
if (story) {
|
|
payload.roComment = story;
|
|
}
|
|
if (makeOverride) {
|
|
payload.makeOverride = makeOverride;
|
|
}
|
|
if (earlyRoLabor) {
|
|
payload.rolabor = earlyRoLabor;
|
|
}
|
|
|
|
CreateRRLogEvent(socket, "INFO", "Creating minimal RR Repair Order (early creation)", {
|
|
payload,
|
|
earlyRoOpCode,
|
|
hasRolabor: !!earlyRoLabor
|
|
});
|
|
|
|
const response = await client.createRepairOrder(payload, finalOpts);
|
|
|
|
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", withRRRequestXml(response, { 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 for later updates
|
|
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
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Full Data Update: Update an existing RR Repair Order with complete job data (allocations, parts, labor).
|
|
* Used during DMS post form when an early RO was already created.
|
|
* @param args
|
|
* @returns {Promise<{success: boolean, data: *, roStatus: {status: *, statusCode: *|undefined, message}, statusBlocks: *|{}, customerNo: string, svId: *, roNo: *, xml: *}>}
|
|
*/
|
|
const updateRRRepairOrderWithFullData = async (args) => {
|
|
const { bodyshop, job, advisorNo, selectedCustomer, txEnvelope, socket, svId, roNo } = args || {};
|
|
|
|
if (!bodyshop) throw new Error("updateRRRepairOrderWithFullData: bodyshop is required");
|
|
if (!job) throw new Error("updateRRRepairOrderWithFullData: job is required");
|
|
if (advisorNo == null || String(advisorNo).trim() === "") {
|
|
throw new Error("updateRRRepairOrderWithFullData: advisorNo is required for RR");
|
|
}
|
|
if (!roNo) throw new Error("updateRRRepairOrderWithFullData: roNo is required for update");
|
|
|
|
// Resolve customer number (accept multiple shapes)
|
|
const selected = selectedCustomer?.customerNo || selectedCustomer?.custNo;
|
|
if (!selected) throw new Error("updateRRRepairOrderWithFullData: selectedCustomer.custNo/customerNo is required");
|
|
|
|
const { client, opts } = buildClientAndOpts(bodyshop);
|
|
|
|
// For full data update after early RO, use the RR update route.
|
|
const finalOpts = {
|
|
...opts,
|
|
envelope: {
|
|
...(opts?.envelope || {}),
|
|
sender: {
|
|
...(opts?.envelope?.sender || {}),
|
|
task: "BSMRO",
|
|
referenceId: "Update"
|
|
}
|
|
}
|
|
};
|
|
|
|
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;
|
|
const opCode = resolveRROpCode(bodyshop, txEnvelope);
|
|
|
|
// 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, "INFO", "RR allocations resolved for update", {
|
|
hasAllocations: allocations.length > 0,
|
|
count: allocations.length,
|
|
allocationsPreview: allocations.slice(0, 2).map((a) => ({
|
|
type: a?.type,
|
|
code: a?.code,
|
|
laborSale: a?.laborSale,
|
|
laborCost: a?.laborCost,
|
|
partsSale: a?.partsSale,
|
|
partsCost: a?.partsCost
|
|
})),
|
|
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 update if allocations fail.
|
|
allocations = [];
|
|
}
|
|
|
|
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
|
|
opCode,
|
|
baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop)
|
|
});
|
|
|
|
// Build full RO payload for update with allocations
|
|
const payload = buildRRRepairOrderPayload({
|
|
bodyshop,
|
|
job,
|
|
selectedCustomer: { customerNo: String(selected), custNo: String(selected) },
|
|
advisorNo: String(advisorNo),
|
|
story,
|
|
makeOverride,
|
|
allocations,
|
|
opCode
|
|
});
|
|
|
|
// Update the existing RO created during the early RO step.
|
|
payload.finalUpdate = "N";
|
|
payload.roNo = String(roNo);
|
|
payload.outsdRoNo = job?.ro_number || job?.id || undefined;
|
|
|
|
// RR update rejects placeholder non-labor ROLABOR rows with zero labor prices.
|
|
// Keep only the actual labor jobs in ROLABOR and let ROGOG carry parts/extras.
|
|
if (payload.rolabor?.ops?.length && payload.rogg?.ops?.length) {
|
|
const laborJobNos = new Set(
|
|
payload.rogg.ops
|
|
.filter((op) => op?.segmentKind === "laborTaxable" || op?.segmentKind === "laborNonTaxable")
|
|
.map((op) => String(op.jobNo))
|
|
);
|
|
|
|
payload.rolabor.ops = payload.rolabor.ops.filter((op) => laborJobNos.has(String(op?.jobNo)));
|
|
|
|
if (!payload.rolabor.ops.length) {
|
|
delete payload.rolabor;
|
|
}
|
|
}
|
|
|
|
CreateRRLogEvent(socket, "INFO", "Preparing full data update for existing RR RO", {
|
|
roNo: String(roNo),
|
|
hasRolabor: !!payload.rolabor,
|
|
rolaborCount: payload.rolabor?.ops?.length || 0,
|
|
hasRogg: !!payload.rogg,
|
|
payload
|
|
});
|
|
|
|
const response = await client.updateRepairOrder(payload, finalOpts);
|
|
|
|
CreateRRLogEvent(
|
|
socket,
|
|
"INFO",
|
|
"RR full data update sent for existing RO",
|
|
withRRRequestXml(response, {
|
|
roNo: String(roNo),
|
|
hasRolabor: !!payload.rolabor,
|
|
rolaborCount: payload.rolabor?.ops?.length || 0,
|
|
hasRogg: !!payload.rogg,
|
|
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) {
|
|
success = !["FAILURE", "ERROR"].includes(statusUpper);
|
|
} else if (typeof response?.success === "boolean") {
|
|
success = response.success;
|
|
} else if (roStatus?.status) {
|
|
success = String(roStatus.status).toUpperCase() === "SUCCESS";
|
|
}
|
|
|
|
return {
|
|
success,
|
|
data,
|
|
roStatus,
|
|
statusBlocks,
|
|
customerNo: String(selected),
|
|
svId,
|
|
roNo: String(roNo),
|
|
xml: response?.xml
|
|
};
|
|
};
|
|
|
|
/**
|
|
* LEGACY: Step 1: Export a job to RR as a new Repair Order with full data.
|
|
* This is the original function - kept for backward compatibility if shops don't use early RO creation.
|
|
* @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;
|
|
const opCode = resolveRROpCode(bodyshop, txEnvelope);
|
|
|
|
// 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 = [];
|
|
}
|
|
|
|
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
|
|
opCode,
|
|
baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop)
|
|
});
|
|
|
|
// 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", withRRRequestXml(response, { 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",
|
|
withRRRequestXml(rrRes, {
|
|
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,
|
|
createMinimalRRRepairOrder,
|
|
updateRRRepairOrderWithFullData,
|
|
finalizeRRRepairOrder,
|
|
deriveRRStatus
|
|
};
|