feature/IO-3558-Reynolds-Part-2 - Initial
This commit is contained in:
@@ -56,7 +56,319 @@ const deriveRRStatus = (rrRes = {}) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Step 1: Export a job to RR as a new Repair Order.
|
||||
* 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 - just header, no allocations/parts/labor
|
||||
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 payload = {
|
||||
customerNo: String(selected),
|
||||
advisorNo: String(advisorNo),
|
||||
vin: cleanVin,
|
||||
departmentType: "B",
|
||||
outsdRoNo: job?.ro_number || job?.id || undefined
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "Creating minimal RR Repair Order (early creation)", {
|
||||
payload
|
||||
});
|
||||
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "RR minimal 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 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, we still use "Insert" referenceId
|
||||
// because we're inserting the job operations for the first time
|
||||
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;
|
||||
|
||||
// Optional RR OpCode segments coming from the FE (RRPostForm)
|
||||
const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
|
||||
const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
|
||||
const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
|
||||
|
||||
// RR-only extras
|
||||
let rrCentersConfig = null;
|
||||
let allocations = null;
|
||||
let opCode = 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, "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 = [];
|
||||
}
|
||||
|
||||
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
|
||||
|
||||
let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
||||
|
||||
// If the FE only sends segments, combine them here.
|
||||
if (!opCodeOverride && (opPrefix || opBase || opSuffix)) {
|
||||
const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
|
||||
if (combined) {
|
||||
opCodeOverride = combined;
|
||||
}
|
||||
}
|
||||
|
||||
if (opCodeOverride || resolvedBaseOpCode) {
|
||||
opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
|
||||
opCode,
|
||||
baseFromConfig: resolvedBaseOpCode,
|
||||
opPrefix,
|
||||
opBase,
|
||||
opSuffix
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// Add roNo for linking to existing RO
|
||||
payload.roNo = String(roNo);
|
||||
payload.outsdRoNo = job?.ro_number || job?.id || undefined;
|
||||
|
||||
// Keep rolabor - it's needed to register the job/OpCode accounts in Reynolds
|
||||
// Without this, Reynolds won't recognize the OpCode when we send rogg operations
|
||||
// The rolabor section tells Reynolds "these jobs exist" even with minimal data
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "Sending full data for early RO (using create with roNo)", {
|
||||
roNo: String(roNo),
|
||||
hasRolabor: !!payload.rolabor,
|
||||
hasRogg: !!payload.rogg,
|
||||
payload
|
||||
});
|
||||
|
||||
// Use createRepairOrder (not update) with the roNo to link to the existing early RO
|
||||
// Reynolds will merge this with the existing RO header
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "RR Repair Order full data sent", {
|
||||
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: *}>}
|
||||
*/
|
||||
@@ -315,4 +627,10 @@ const finalizeRRRepairOrder = async (args) => {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { exportJobToRR, finalizeRRRepairOrder, deriveRRStatus };
|
||||
module.exports = {
|
||||
exportJobToRR,
|
||||
createMinimalRRRepairOrder,
|
||||
updateRRRepairOrderWithFullData,
|
||||
finalizeRRRepairOrder,
|
||||
deriveRRStatus
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user