feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration -Full Flow verified
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,10 +8,24 @@ const getAuthToken = (socket) =>
|
||||
|
||||
/** Compact metadata for RR */
|
||||
const buildRRExportMeta = ({ result, extra = {} }) => {
|
||||
const roStatus = result?.roStatus || result?.data?.roStatus || null;
|
||||
const tx = result?.statusBlocks?.transaction;
|
||||
const rawRoStatus = result?.roStatus || result?.data?.roStatus || null;
|
||||
|
||||
const roStatus =
|
||||
rawRoStatus ||
|
||||
(tx
|
||||
? {
|
||||
status: tx.status ?? tx.Status,
|
||||
statusCode: tx.statusCode ?? tx.StatusCode,
|
||||
message: tx.message ?? tx.Message
|
||||
}
|
||||
: null);
|
||||
|
||||
return {
|
||||
provider: "rr",
|
||||
success: Boolean(result?.success || roStatus?.status === "Success"),
|
||||
success: Boolean(
|
||||
result?.success || (roStatus && String(roStatus.status || roStatus.Status).toUpperCase() === "SUCCESS")
|
||||
),
|
||||
customerNo: result?.customerNo,
|
||||
svId: result?.svId,
|
||||
roStatus: roStatus && {
|
||||
@@ -55,7 +69,12 @@ const buildMessageJSONString = ({ error, classification, result, fallback }) =>
|
||||
else if (error?.message) push(error.message);
|
||||
|
||||
// RR status message
|
||||
push(result?.roStatus?.message ?? result?.roStatus?.Message);
|
||||
push(
|
||||
result?.roStatus?.message ??
|
||||
result?.roStatus?.Message ??
|
||||
result?.statusBlocks?.transaction?.message ??
|
||||
result?.statusBlocks?.transaction?.Message
|
||||
);
|
||||
|
||||
// Fallback
|
||||
push(fallback || "RR export failed");
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -161,100 +161,6 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
|
||||
return { ops };
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a header-level TaxCodeInfo payload from allocations (e.g. PROVINCIAL SALES TAX line).
|
||||
*
|
||||
* Shape returned matches what `buildCreateRepairOrder` expects for:
|
||||
*
|
||||
* payload.tax = {
|
||||
* payType,
|
||||
* taxCode,
|
||||
* txblGrossAmt,
|
||||
* grossTaxAmt
|
||||
* }
|
||||
*
|
||||
* NOTE: We are currently NOT wiring this into the payload (see buildRRRepairOrderPayload)
|
||||
* so that TaxCodeInfo is suppressed in the XML, but we keep this helper around for
|
||||
* future use.
|
||||
*
|
||||
* @param {Array} allocations
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.taxCode - RR tax code (configured per dealer)
|
||||
* @param {string} [opts.payType="Cust"]
|
||||
* @returns {null|{payType, taxCode, txblGrossAmt, grossTaxAmt}}
|
||||
*/
|
||||
const buildTaxFromAllocations = (allocations, { taxCode, payType = "Cust" } = {}) => {
|
||||
if (!taxCode || !Array.isArray(allocations) || !allocations.length) return null;
|
||||
|
||||
const taxAlloc = allocations.find((a) => a && a.tax);
|
||||
if (!taxAlloc || !taxAlloc.sale) return null;
|
||||
|
||||
const grossTaxNum = parseFloat(asN2(taxAlloc.sale));
|
||||
if (!Number.isFinite(grossTaxNum)) return null;
|
||||
|
||||
const rate = typeof taxAlloc.profitCenter?.rate === "number" ? taxAlloc.profitCenter.rate : null;
|
||||
|
||||
let taxableGrossNum = grossTaxNum;
|
||||
if (rate && rate > 0) {
|
||||
const r = rate / 100;
|
||||
taxableGrossNum = grossTaxNum / r;
|
||||
}
|
||||
|
||||
return {
|
||||
payType,
|
||||
taxCode,
|
||||
txblGrossAmt: taxableGrossNum.toFixed(2),
|
||||
grossTaxAmt: grossTaxNum.toFixed(2)
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a minimal Rolabor structure in the new normalized shape.
|
||||
*
|
||||
* Useful for tests or for scenarios where you want a single zero-dollar
|
||||
* Rolabor op but don't have GOG data. Shape matches payload.rolabor for the
|
||||
* reynolds-rome-client builders.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.opCode
|
||||
* @param {number|string} [opts.jobNo=1]
|
||||
* @param {string} [opts.payType="Cust"]
|
||||
* @returns {null|{ops: Array}}
|
||||
*/
|
||||
const buildRolaborSkeleton = ({ opCode, jobNo = 1, payType = "Cust" } = {}) => {
|
||||
if (!opCode) return null;
|
||||
|
||||
return {
|
||||
ops: [
|
||||
{
|
||||
opCode,
|
||||
jobNo: String(jobNo),
|
||||
custPayTypeFlag: "C",
|
||||
warrPayTypeFlag: "W",
|
||||
intrPayTypeFlag: "I",
|
||||
custTxblNtxblFlag: "N",
|
||||
warrTxblNtxblFlag: "N",
|
||||
intrTxblNtxblFlag: "N",
|
||||
vlrCode: undefined,
|
||||
bill: {
|
||||
payType,
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
},
|
||||
amount: {
|
||||
payType,
|
||||
amtType: "Job",
|
||||
custPrice: "0",
|
||||
totalAmt: "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
// ---------- Public API ----------
|
||||
|
||||
/**
|
||||
* Query job data by ID from GraphQL API.
|
||||
* @param ctx
|
||||
@@ -301,8 +207,8 @@ const buildRRRepairOrderPayload = ({
|
||||
story,
|
||||
makeOverride,
|
||||
allocations,
|
||||
opCode,
|
||||
taxCode
|
||||
opCode
|
||||
// taxCode
|
||||
} = {}) => {
|
||||
const customerNo = selectedCustomer?.customerNo
|
||||
? String(selectedCustomer.customerNo).trim()
|
||||
@@ -353,7 +259,7 @@ const buildRRRepairOrderPayload = ({
|
||||
|
||||
if (haveAllocations) {
|
||||
const effectiveOpCode = (opCode && String(opCode).trim()) || null;
|
||||
const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
|
||||
// const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
|
||||
|
||||
if (effectiveOpCode) {
|
||||
// Build RO.GOG and RO.LABOR in the new normalized shape
|
||||
@@ -493,6 +399,20 @@ const normalizeVehicleCandidates = (res) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a minimal Rolabor structure in the new normalized shape.
|
||||
*
|
||||
* Useful for tests or for scenarios where you want a single zero-dollar
|
||||
* Rolabor op but don't have GOG data. Shape matches payload.rolabor for the
|
||||
* reynolds-rome-client builders.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.opCode
|
||||
* @param {number|string} [opts.jobNo=1]
|
||||
* @param {string} [opts.payType="Cust"]
|
||||
* @returns {null|{ops: Array}}
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
QueryJobData,
|
||||
buildRRRepairOrderPayload,
|
||||
@@ -500,9 +420,6 @@ module.exports = {
|
||||
makeVehicleSearchPayloadFromJob,
|
||||
normalizeCustomerCandidates,
|
||||
normalizeVehicleCandidates,
|
||||
// exporting these so you can unit-test them directly if you want
|
||||
buildRogogFromAllocations,
|
||||
buildTaxFromAllocations,
|
||||
buildRolaborSkeleton,
|
||||
buildRolaborFromRogog
|
||||
};
|
||||
|
||||
@@ -644,17 +644,28 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
});
|
||||
} else {
|
||||
// classify & fail (no finalize)
|
||||
const tx = result?.statusBlocks?.transaction;
|
||||
|
||||
const vendorStatusCode = Number(
|
||||
result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode
|
||||
result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? tx?.statusCode ?? tx?.StatusCode
|
||||
);
|
||||
|
||||
const vendorMessage =
|
||||
result?.roStatus?.message ??
|
||||
result?.roStatus?.Message ??
|
||||
tx?.message ??
|
||||
tx?.Message ??
|
||||
result?.error ??
|
||||
"RR export failed";
|
||||
|
||||
const cls = classifyRRVendorError({
|
||||
code: vendorStatusCode,
|
||||
message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed"
|
||||
message: vendorMessage
|
||||
});
|
||||
|
||||
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
|
||||
roStatus: result?.roStatus,
|
||||
statusBlocks: result?.statusBlocks,
|
||||
classification: cls
|
||||
});
|
||||
|
||||
@@ -802,18 +813,26 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus });
|
||||
ack?.({ ok: true, result: finalizeResult });
|
||||
} else {
|
||||
const tx = finalizeResult?.statusBlocks?.transaction;
|
||||
|
||||
const vendorStatusCode = Number(
|
||||
finalizeResult?.roStatus?.statusCode ??
|
||||
finalizeResult?.roStatus?.StatusCode ??
|
||||
finalizeResult?.statusBlocks?.transaction?.statusCode
|
||||
tx?.statusCode ??
|
||||
tx?.StatusCode
|
||||
);
|
||||
|
||||
const vendorMessage =
|
||||
finalizeResult?.roStatus?.message ??
|
||||
finalizeResult?.roStatus?.Message ??
|
||||
tx?.message ??
|
||||
tx?.Message ??
|
||||
finalizeResult?.error ??
|
||||
"RR finalize failed";
|
||||
|
||||
const cls = classifyRRVendorError({
|
||||
code: vendorStatusCode,
|
||||
message:
|
||||
finalizeResult?.roStatus?.message ??
|
||||
finalizeResult?.roStatus?.Message ??
|
||||
finalizeResult?.error ??
|
||||
"RR finalize failed"
|
||||
message: vendorMessage
|
||||
});
|
||||
|
||||
await insertRRFailedExportLog({
|
||||
|
||||
@@ -64,6 +64,47 @@ const isAlreadyExistsError = (e) => {
|
||||
return msg.includes("ALREADY EXISTS") || msg.includes("VEHICLE ALREADY EXISTS");
|
||||
};
|
||||
|
||||
function deriveMakeCode(makeDesc) {
|
||||
if (!makeDesc) return "FR"; // safe default
|
||||
|
||||
const map = {
|
||||
Ford: "FO",
|
||||
FORD: "FO",
|
||||
Lincoln: "LN",
|
||||
Mercury: "MY",
|
||||
Chevrolet: "CH",
|
||||
GMC: "GM",
|
||||
Cadillac: "CA",
|
||||
Buick: "BU",
|
||||
Pontiac: "PO",
|
||||
Oldsmobile: "OL",
|
||||
Saturn: "SA",
|
||||
Dodge: "DO",
|
||||
Chrysler: "CH",
|
||||
Jeep: "JE",
|
||||
Plymouth: "PL",
|
||||
Toyota: "TY",
|
||||
Lexus: "LX",
|
||||
Honda: "HO",
|
||||
Acura: "AC",
|
||||
Nissan: "NI",
|
||||
Infiniti: "IN",
|
||||
Hyundai: "HY",
|
||||
Kia: "KI",
|
||||
Subaru: "SU",
|
||||
Mazda: "MZ",
|
||||
Volkswagen: "VW",
|
||||
Audi: "AU",
|
||||
BMW: "BM",
|
||||
Mercedes: "MB",
|
||||
"Mercedes-Benz": "MB",
|
||||
Volvo: "VO",
|
||||
Ram: "RA" // post-2010
|
||||
};
|
||||
|
||||
return map[makeDesc.trim().toUpperCase()] || "FR"; // TODO Default set
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure/create a Service Vehicle in RR for the given VIN + customer.
|
||||
* Args may contain:
|
||||
@@ -150,10 +191,57 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
||||
// IMPORTANT: The current RR lib build validates `vehicleServInfo.customerNo`.
|
||||
// To be future-proof, we also include top-level `customerNo`.
|
||||
const insertPayload = {
|
||||
vin: vinStr,
|
||||
customerNo: custNoStr, // fallback form (some builds accept this)
|
||||
vehicleServInfo: { customerNo: custNoStr }, // primary form expected by the lib
|
||||
vehicleDetail: license ? { licNo: String(license).trim() } : undefined
|
||||
// === Core Vehicle Identity (MANDATORY for success) ===
|
||||
vin: vinStr.toUpperCase(), // "1FDWX34Y28EB01395"
|
||||
|
||||
// Required: 2-character make code (from v_make_desc → known mapping)
|
||||
vehicleMake: deriveMakeCode(job.v_make_desc), // → "FR" for Ford
|
||||
|
||||
// Required: 2-digit year (last 2 digits of v_model_yr)
|
||||
year: job?.v_model_yr || undefined,
|
||||
|
||||
// Required: Model number — fallback strategy per ERA behavior
|
||||
// Most Ford trucks use "T" = Truck. Some systems accept actual code.
|
||||
// CAN BE (P)assenger , (T)ruck, (O)ther
|
||||
mdlNo: undefined,
|
||||
|
||||
// === Descriptive Fields (highly recommended) ===
|
||||
modelDesc: job?.v_model_desc?.trim() || undefined, // "F-350 SD"
|
||||
carline: job?.v_model_desc?.trim() || undefined, // Series line
|
||||
extClrDesc: job?.v_color?.trim() || undefined, // "Red"
|
||||
|
||||
// Optional but helpful
|
||||
accentClr: undefined,
|
||||
|
||||
// === VehicleDetail Flags (CRITICAL — cause silent fails or error 303 if missing) ===
|
||||
aircond: undefined, // "Y", // Nearly all modern vehicles have A/C
|
||||
pwrstr: undefined, // "Y", // Power steering = yes on 99% of vehicles post-1990
|
||||
transm: undefined, // "A", // Default to Automatic — change to "M" only if known manual
|
||||
turbo: undefined, //"N", // 2008 F-350 6.4L Power Stroke has turbo, but field is optional
|
||||
engineConfig: undefined, //"V8", // or "6.4L Diesel" — optional but nice
|
||||
trim: undefined, //"XLT", // You don't have this — safe to omit or guess
|
||||
|
||||
// License plate
|
||||
licNo: license ? String(license).trim() : undefined,
|
||||
|
||||
// === VehicleServInfo (attributes on the element) ===
|
||||
customerNo: custNoStr, // fallback (some builds read this)
|
||||
stockId: job.ro_number || undefined, // Use RO as stock# — common pattern
|
||||
|
||||
vehicleServInfo: {
|
||||
customerNo: custNoStr, // REQUIRED — this is what toServiceVehicleView() validates
|
||||
// Optional but increases success rate
|
||||
salesmanNo: undefined, // You don't have advisor yet — omit
|
||||
inServiceDate: undefined,
|
||||
// Optional — safe to include if you want
|
||||
productionDate: undefined,
|
||||
modelMaintCode: undefined,
|
||||
teamCode: undefined,
|
||||
// Extended warranty — omit unless you sell contracts
|
||||
vehExtWarranty: undefined,
|
||||
// Advisor — omit unless you know who the service advisor is
|
||||
advisor: undefined
|
||||
}
|
||||
};
|
||||
|
||||
const insertOpts = {
|
||||
|
||||
Reference in New Issue
Block a user