feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration -Full Flow verified
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// server/rr/rr-job-exports.js
|
||||
|
||||
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
||||
const { buildClientAndOpts } = require("./rr-lookup");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
@@ -7,26 +5,59 @@ const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers"
|
||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
|
||||
/**
|
||||
* Step 1: Export a job to Reynolds & Reynolds as a *new* Repair Order.
|
||||
*
|
||||
* This is the "create" phase only:
|
||||
* - We always call client.createRepairOrder(payload, opts)
|
||||
* - Any follow-up / finalUpdate is handled by finalizeRRRepairOrder
|
||||
*
|
||||
* When in RR mode (bodyshop.rr_dealerid truthy), we also:
|
||||
* - Extract responsibility center config (for logging / debugging)
|
||||
* - Run CdkCalculateAllocations to produce the allocations array
|
||||
* (with profitCenter/costCenter + rr_gogcode / rr_item_type / rr_cust_txbl_flag)
|
||||
* - Derive a default RR OpCode + TaxCode (if configured)
|
||||
* - Pass bodyshop, allocations, opCode, taxCode into buildRRRepairOrderPayload
|
||||
* so it can create the payload fields used by reynolds-rome-client:
|
||||
* - header fields (CustNo, AdvNo, DeptType, Vin, etc.)
|
||||
* - RO.rolabor (ops[])
|
||||
* - RO.rogg (ops[])
|
||||
* - RO.tax (TaxCodeInfo)
|
||||
*
|
||||
* 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, data: *, roStatus: *, statusBlocks, customerNo: string, svId: string|null, roNo: *}>}
|
||||
* @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 || {};
|
||||
@@ -63,77 +94,57 @@ const exportJobToRR = async (args) => {
|
||||
let rrCentersConfig = null;
|
||||
let allocations = null;
|
||||
let opCode = null;
|
||||
let taxCode = null;
|
||||
// let taxCode = null;
|
||||
|
||||
if (bodyshop.rr_dealerid) {
|
||||
// 1) Responsibility center config (for visibility / debugging)
|
||||
try {
|
||||
rrCentersConfig = extractRrResponsibilityCenters(bodyshop);
|
||||
// 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_default_opcode ||
|
||||
bodyshop.rr_opcode ||
|
||||
rrCentersConfig?.defaultOpCode ||
|
||||
rrCentersConfig?.rrDefaultOpCode ||
|
||||
"51DOZ"; // 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;
|
||||
}
|
||||
|
||||
// 4) TaxCode (for header-level tax, e.g. state/prov tax)
|
||||
const baseTaxCode =
|
||||
bodyshop.rr_default_taxcode ||
|
||||
bodyshop.rr_tax_code ||
|
||||
rrCentersConfig?.defaultTaxCode ||
|
||||
rrCentersConfig?.rrDefaultTaxCode ||
|
||||
"TEST"; // TODO Change / implement default handling policy
|
||||
|
||||
if (baseTaxCode) {
|
||||
taxCode = String(baseTaxCode).trim() || null;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR op/tax config resolved", {
|
||||
opCode,
|
||||
taxCode
|
||||
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:
|
||||
@@ -147,8 +158,7 @@ const exportJobToRR = async (args) => {
|
||||
story,
|
||||
makeOverride,
|
||||
allocations,
|
||||
opCode,
|
||||
taxCode
|
||||
opCode
|
||||
});
|
||||
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
@@ -159,32 +169,42 @@ const exportJobToRR = async (args) => {
|
||||
});
|
||||
|
||||
const data = response?.data || null;
|
||||
const roStatus = data?.roStatus || 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: response?.success === true || roStatus?.status === "Success",
|
||||
success,
|
||||
data,
|
||||
roStatus,
|
||||
statusBlocks: response?.statusBlocks || [],
|
||||
statusBlocks,
|
||||
customerNo: String(selected),
|
||||
// svId comes from the earlier ensureRRServiceVehicle call (if the caller passes it)
|
||||
svId,
|
||||
roNo
|
||||
roNo,
|
||||
xml: response?.xml // expose XML for logging/diagnostics
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Step 2: Finalize an RR Repair Order by sending finalUpdate: "Y".
|
||||
* This is the *update* phase.
|
||||
*
|
||||
* We intentionally do NOT send Rogog/Rolabor here — all of that is pushed on
|
||||
* create; finalize is just a header-level update (FinalUpdate + estimateType).
|
||||
*
|
||||
* Step 2: Finalize an existing RR Repair Order (previously created).
|
||||
* @param args
|
||||
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks}>}
|
||||
* @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 || {};
|
||||
@@ -251,14 +271,27 @@ const finalizeRRRepairOrder = async (args) => {
|
||||
});
|
||||
|
||||
const data = rrRes?.data || null;
|
||||
const roStatus = data?.roStatus || 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: rrRes?.success === true || roStatus?.status === "Success",
|
||||
success,
|
||||
data,
|
||||
roStatus,
|
||||
statusBlocks: rrRes?.statusBlocks || []
|
||||
statusBlocks,
|
||||
xml: rrRes?.xml
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { exportJobToRR, finalizeRRRepairOrder };
|
||||
module.exports = { exportJobToRR, finalizeRRRepairOrder, deriveRRStatus };
|
||||
|
||||
Reference in New Issue
Block a user