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 */
|
/** Compact metadata for RR */
|
||||||
const buildRRExportMeta = ({ result, extra = {} }) => {
|
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 {
|
return {
|
||||||
provider: "rr",
|
provider: "rr",
|
||||||
success: Boolean(result?.success || roStatus?.status === "Success"),
|
success: Boolean(
|
||||||
|
result?.success || (roStatus && String(roStatus.status || roStatus.Status).toUpperCase() === "SUCCESS")
|
||||||
|
),
|
||||||
customerNo: result?.customerNo,
|
customerNo: result?.customerNo,
|
||||||
svId: result?.svId,
|
svId: result?.svId,
|
||||||
roStatus: roStatus && {
|
roStatus: roStatus && {
|
||||||
@@ -55,7 +69,12 @@ const buildMessageJSONString = ({ error, classification, result, fallback }) =>
|
|||||||
else if (error?.message) push(error.message);
|
else if (error?.message) push(error.message);
|
||||||
|
|
||||||
// RR status 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
|
// Fallback
|
||||||
push(fallback || "RR export failed");
|
push(fallback || "RR export failed");
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// server/rr/rr-job-exports.js
|
|
||||||
|
|
||||||
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
||||||
const { buildClientAndOpts } = require("./rr-lookup");
|
const { buildClientAndOpts } = require("./rr-lookup");
|
||||||
const CreateRRLogEvent = require("./rr-logger-event");
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
@@ -7,26 +5,59 @@ const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers"
|
|||||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 1: Export a job to Reynolds & Reynolds as a *new* Repair Order.
|
* Derive RR status information from response object.
|
||||||
*
|
* @param rrRes
|
||||||
* This is the "create" phase only:
|
* @returns {{status: *, statusCode: *|undefined, message}|null}
|
||||||
* - We always call client.createRepairOrder(payload, opts)
|
*/
|
||||||
* - Any follow-up / finalUpdate is handled by finalizeRRRepairOrder
|
const deriveRRStatus = (rrRes = {}) => {
|
||||||
*
|
const data = rrRes.data || {};
|
||||||
* When in RR mode (bodyshop.rr_dealerid truthy), we also:
|
const tx = rrRes.statusBlocks && rrRes.statusBlocks.transaction;
|
||||||
* - Extract responsibility center config (for logging / debugging)
|
|
||||||
* - Run CdkCalculateAllocations to produce the allocations array
|
const pick = (obj, ...keys) => {
|
||||||
* (with profitCenter/costCenter + rr_gogcode / rr_item_type / rr_cust_txbl_flag)
|
if (!obj) return undefined;
|
||||||
* - Derive a default RR OpCode + TaxCode (if configured)
|
for (const k of keys) {
|
||||||
* - Pass bodyshop, allocations, opCode, taxCode into buildRRRepairOrderPayload
|
if (obj[k] != null) return obj[k];
|
||||||
* so it can create the payload fields used by reynolds-rome-client:
|
}
|
||||||
* - header fields (CustNo, AdvNo, DeptType, Vin, etc.)
|
return undefined;
|
||||||
* - RO.rolabor (ops[])
|
};
|
||||||
* - RO.rogg (ops[])
|
|
||||||
* - RO.tax (TaxCodeInfo)
|
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
|
* @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 exportJobToRR = async (args) => {
|
||||||
const { bodyshop, job, advisorNo, selectedCustomer, txEnvelope, socket, svId } = args || {};
|
const { bodyshop, job, advisorNo, selectedCustomer, txEnvelope, socket, svId } = args || {};
|
||||||
@@ -63,77 +94,57 @@ const exportJobToRR = async (args) => {
|
|||||||
let rrCentersConfig = null;
|
let rrCentersConfig = null;
|
||||||
let allocations = null;
|
let allocations = null;
|
||||||
let opCode = null;
|
let opCode = null;
|
||||||
let taxCode = null;
|
// let taxCode = null;
|
||||||
|
|
||||||
if (bodyshop.rr_dealerid) {
|
// 1) Responsibility center config (for visibility / debugging)
|
||||||
// 1) Responsibility center config (for visibility / debugging)
|
try {
|
||||||
try {
|
rrCentersConfig = extractRrResponsibilityCenters(bodyshop);
|
||||||
rrCentersConfig = extractRrResponsibilityCenters(bodyshop);
|
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "SILLY", "RR responsibility centers resolved", {
|
CreateRRLogEvent(socket, "SILLY", "RR responsibility centers resolved", {
|
||||||
hasCenters: !!bodyshop.md_responsibility_centers,
|
hasCenters: !!bodyshop.md_responsibility_centers,
|
||||||
profitCenters: Object.keys(rrCentersConfig?.profitsByName || {}),
|
profitCenters: Object.keys(rrCentersConfig?.profitsByName || {}),
|
||||||
costCenters: Object.keys(rrCentersConfig?.costsByName || {}),
|
costCenters: Object.keys(rrCentersConfig?.costsByName || {}),
|
||||||
dmsCostDefaults: rrCentersConfig?.dmsCostDefaults || {},
|
dmsCostDefaults: rrCentersConfig?.dmsCostDefaults || {},
|
||||||
dmsProfitDefaults: rrCentersConfig?.dmsProfitDefaults || {}
|
dmsProfitDefaults: rrCentersConfig?.dmsProfitDefaults || {}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
CreateRRLogEvent(socket, "ERROR", "Failed to resolve RR responsibility centers", {
|
CreateRRLogEvent(socket, "ERROR", "Failed to resolve RR responsibility centers", {
|
||||||
message: e?.message,
|
message: e?.message,
|
||||||
stack: e?.stack
|
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Build RO payload for create.
|
||||||
//
|
//
|
||||||
// NOTE:
|
// NOTE:
|
||||||
@@ -147,8 +158,7 @@ const exportJobToRR = async (args) => {
|
|||||||
story,
|
story,
|
||||||
makeOverride,
|
makeOverride,
|
||||||
allocations,
|
allocations,
|
||||||
opCode,
|
opCode
|
||||||
taxCode
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await client.createRepairOrder(payload, finalOpts);
|
const response = await client.createRepairOrder(payload, finalOpts);
|
||||||
@@ -159,32 +169,42 @@ const exportJobToRR = async (args) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = response?.data || null;
|
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
|
// Extract canonical roNo you'll need for finalize step
|
||||||
const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null;
|
const roNo = data?.dmsRoNo ?? data?.outsdRoNo ?? roStatus?.dmsRoNo ?? null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: response?.success === true || roStatus?.status === "Success",
|
success,
|
||||||
data,
|
data,
|
||||||
roStatus,
|
roStatus,
|
||||||
statusBlocks: response?.statusBlocks || [],
|
statusBlocks,
|
||||||
customerNo: String(selected),
|
customerNo: String(selected),
|
||||||
// svId comes from the earlier ensureRRServiceVehicle call (if the caller passes it)
|
|
||||||
svId,
|
svId,
|
||||||
roNo
|
roNo,
|
||||||
|
xml: response?.xml // expose XML for logging/diagnostics
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 2: Finalize an RR Repair Order by sending finalUpdate: "Y".
|
* Step 2: Finalize an existing RR Repair Order (previously created).
|
||||||
* 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).
|
|
||||||
*
|
|
||||||
* @param args
|
* @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 finalizeRRRepairOrder = async (args) => {
|
||||||
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
|
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
|
||||||
@@ -251,14 +271,27 @@ const finalizeRRRepairOrder = async (args) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = rrRes?.data || null;
|
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 {
|
return {
|
||||||
success: rrRes?.success === true || roStatus?.status === "Success",
|
success,
|
||||||
data,
|
data,
|
||||||
roStatus,
|
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 };
|
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.
|
* Query job data by ID from GraphQL API.
|
||||||
* @param ctx
|
* @param ctx
|
||||||
@@ -301,8 +207,8 @@ const buildRRRepairOrderPayload = ({
|
|||||||
story,
|
story,
|
||||||
makeOverride,
|
makeOverride,
|
||||||
allocations,
|
allocations,
|
||||||
opCode,
|
opCode
|
||||||
taxCode
|
// taxCode
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const customerNo = selectedCustomer?.customerNo
|
const customerNo = selectedCustomer?.customerNo
|
||||||
? String(selectedCustomer.customerNo).trim()
|
? String(selectedCustomer.customerNo).trim()
|
||||||
@@ -353,7 +259,7 @@ const buildRRRepairOrderPayload = ({
|
|||||||
|
|
||||||
if (haveAllocations) {
|
if (haveAllocations) {
|
||||||
const effectiveOpCode = (opCode && String(opCode).trim()) || null;
|
const effectiveOpCode = (opCode && String(opCode).trim()) || null;
|
||||||
const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
|
// const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
|
||||||
|
|
||||||
if (effectiveOpCode) {
|
if (effectiveOpCode) {
|
||||||
// Build RO.GOG and RO.LABOR in the new normalized shape
|
// 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 = {
|
module.exports = {
|
||||||
QueryJobData,
|
QueryJobData,
|
||||||
buildRRRepairOrderPayload,
|
buildRRRepairOrderPayload,
|
||||||
@@ -500,9 +420,6 @@ module.exports = {
|
|||||||
makeVehicleSearchPayloadFromJob,
|
makeVehicleSearchPayloadFromJob,
|
||||||
normalizeCustomerCandidates,
|
normalizeCustomerCandidates,
|
||||||
normalizeVehicleCandidates,
|
normalizeVehicleCandidates,
|
||||||
// exporting these so you can unit-test them directly if you want
|
|
||||||
buildRogogFromAllocations,
|
buildRogogFromAllocations,
|
||||||
buildTaxFromAllocations,
|
|
||||||
buildRolaborSkeleton,
|
|
||||||
buildRolaborFromRogog
|
buildRolaborFromRogog
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -644,17 +644,28 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// classify & fail (no finalize)
|
// classify & fail (no finalize)
|
||||||
|
const tx = result?.statusBlocks?.transaction;
|
||||||
|
|
||||||
const vendorStatusCode = Number(
|
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({
|
const cls = classifyRRVendorError({
|
||||||
code: vendorStatusCode,
|
code: vendorStatusCode,
|
||||||
message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed"
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
|
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
|
||||||
roStatus: result?.roStatus,
|
roStatus: result?.roStatus,
|
||||||
|
statusBlocks: result?.statusBlocks,
|
||||||
classification: cls
|
classification: cls
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -802,18 +813,26 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus });
|
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus });
|
||||||
ack?.({ ok: true, result: finalizeResult });
|
ack?.({ ok: true, result: finalizeResult });
|
||||||
} else {
|
} else {
|
||||||
|
const tx = finalizeResult?.statusBlocks?.transaction;
|
||||||
|
|
||||||
const vendorStatusCode = Number(
|
const vendorStatusCode = Number(
|
||||||
finalizeResult?.roStatus?.statusCode ??
|
finalizeResult?.roStatus?.statusCode ??
|
||||||
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({
|
const cls = classifyRRVendorError({
|
||||||
code: vendorStatusCode,
|
code: vendorStatusCode,
|
||||||
message:
|
message: vendorMessage
|
||||||
finalizeResult?.roStatus?.message ??
|
|
||||||
finalizeResult?.roStatus?.Message ??
|
|
||||||
finalizeResult?.error ??
|
|
||||||
"RR finalize failed"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
|
|||||||
@@ -64,6 +64,47 @@ const isAlreadyExistsError = (e) => {
|
|||||||
return msg.includes("ALREADY EXISTS") || msg.includes("VEHICLE ALREADY EXISTS");
|
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.
|
* Ensure/create a Service Vehicle in RR for the given VIN + customer.
|
||||||
* Args may contain:
|
* Args may contain:
|
||||||
@@ -150,10 +191,57 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
|||||||
// IMPORTANT: The current RR lib build validates `vehicleServInfo.customerNo`.
|
// IMPORTANT: The current RR lib build validates `vehicleServInfo.customerNo`.
|
||||||
// To be future-proof, we also include top-level `customerNo`.
|
// To be future-proof, we also include top-level `customerNo`.
|
||||||
const insertPayload = {
|
const insertPayload = {
|
||||||
vin: vinStr,
|
// === Core Vehicle Identity (MANDATORY for success) ===
|
||||||
customerNo: custNoStr, // fallback form (some builds accept this)
|
vin: vinStr.toUpperCase(), // "1FDWX34Y28EB01395"
|
||||||
vehicleServInfo: { customerNo: custNoStr }, // primary form expected by the lib
|
|
||||||
vehicleDetail: license ? { licNo: String(license).trim() } : undefined
|
// 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 = {
|
const insertOpts = {
|
||||||
|
|||||||
Reference in New Issue
Block a user