265 lines
8.9 KiB
JavaScript
265 lines
8.9 KiB
JavaScript
// server/rr/rr-job-exports.js
|
|
|
|
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
|
const { buildClientAndOpts } = require("./rr-lookup");
|
|
const CreateRRLogEvent = require("./rr-logger-event");
|
|
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)
|
|
*
|
|
* @param args
|
|
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks, customerNo: string, svId: string|null, roNo: *}>}
|
|
*/
|
|
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;
|
|
let opCode = null;
|
|
let taxCode = null;
|
|
|
|
if (bodyshop.rr_dealerid) {
|
|
// 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
|
|
});
|
|
}
|
|
|
|
// 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,
|
|
taxCode
|
|
});
|
|
|
|
const response = await client.createRepairOrder(payload, finalOpts);
|
|
|
|
CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", {
|
|
payload,
|
|
response
|
|
});
|
|
|
|
const data = response?.data || null;
|
|
const roStatus = data?.roStatus || null;
|
|
|
|
// 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",
|
|
data,
|
|
roStatus,
|
|
statusBlocks: response?.statusBlocks || [],
|
|
customerNo: String(selected),
|
|
// svId comes from the earlier ensureRRServiceVehicle call (if the caller passes it)
|
|
svId,
|
|
roNo
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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).
|
|
*
|
|
* @param args
|
|
* @returns {Promise<{success, data: *, roStatus: *, statusBlocks}>}
|
|
*/
|
|
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", {
|
|
payload,
|
|
response: rrRes
|
|
});
|
|
|
|
const data = rrRes?.data || null;
|
|
const roStatus = data?.roStatus || null;
|
|
|
|
return {
|
|
success: rrRes?.success === true || roStatus?.status === "Success",
|
|
data,
|
|
roStatus,
|
|
statusBlocks: rrRes?.statusBlocks || []
|
|
};
|
|
};
|
|
|
|
module.exports = { exportJobToRR, finalizeRRRepairOrder };
|