Checkpoint

This commit is contained in:
Dave
2025-11-17 17:30:52 -05:00
parent 43dc760c95
commit e20ef4374c
3 changed files with 533 additions and 14 deletions

View File

@@ -34,6 +34,225 @@ const blocksFromCombinedSearchResult = (res) => {
return Array.isArray(data) ? data : [];
};
/**
* Convert a Dinero.js object or number into an "N2" string ("123.45").
* @param value
* @returns {string}
*/
const asN2 = (dineroLike) => {
if (!dineroLike) return "0.00";
// Handle Dinero v1/v2-ish or raw objects
if (typeof dineroLike.toUnit === "function") {
return dineroLike.toUnit().toFixed(2);
}
const precision = dineroLike.precision ?? 2;
const amount = (dineroLike.amount ?? 0) / Math.pow(10, precision);
return amount.toFixed(2);
};
/**
* Build RO.GOG structure for the reynolds-rome-client `createRepairOrder` payload
* from CDK allocations.
* @param {Array} allocations
* @param {Object} opts
* @param {string} opts.opCode - RR OpCode for the job (global, overridable)
* @param {string} [opts.payType="Cust"] - PayType (always "Cust" per Marc)
* @param {string} [opts.roNo] - Optional RoNo to echo on <Rogog RoNo="">
* @returns {null|{roNo?: string, ops: Array}}
*/
const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo } = {}) => {
if (!Array.isArray(allocations) || !allocations.length || !opCode) return null;
const ops = [];
for (const alloc of allocations) {
const pc = alloc?.profitCenter || {};
const breakOut = pc.rr_gogcode;
const itemType = pc.rr_item_type;
// Only centers that have been configured for RR GOG are included
if (!breakOut || !itemType) continue;
const saleN2 = asN2(alloc.sale);
const costN2 = asN2(alloc.cost);
const itemDesc = pc.accountdesc || pc.accountname || alloc.center || "";
const jobNo = String(ops.length + 1); // 1-based JobNo
ops.push({
opCode,
jobNo,
lines: [
{
breakOut,
itemType,
itemDesc,
custQty: "1.0",
// warrQty: "0.0",
// intrQty: "0.0",
custPayTypeFlag: "C",
// warrPayTypeFlag: "W",
// intrPayTypeFlag: "I",
custTxblNtxblFlag: pc.rr_cust_txbl_flag || "T",
// warrTxblNtxblFlag: "N",
// intrTxblNtxblFlag: "N",
amount: {
payType,
amtType: "Unit",
custPrice: saleN2,
dlrCost: costN2
}
}
]
});
}
if (!ops.length) return null;
return {
roNo,
ops
};
};
/**
* Build RO.ROLABOR structure for the reynolds-rome-client `createRepairOrder` payload
* from an already-built RO.GOG structure.
* @param {Object} rogg - result of buildRogogFromAllocations
* @param {Object} opts
* @param {string} [opts.payType="Cust"]
* @returns {null|{ops: Array}}
*/
const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
if (!rogg || !Array.isArray(rogg.ops)) return null;
const ops = rogg.ops.map((op) => {
const firstLine = op.lines?.[0] || {};
return {
opCode: op.opCode,
jobNo: op.jobNo,
custPayTypeFlag: firstLine.custPayTypeFlag || "C",
// warrPayTypeFlag: firstLine.warrPayTypeFlag || "W",
// intrPayTypeFlag: firstLine.intrPayTypeFlag || "I",
custTxblNtxblFlag: firstLine.custTxblNtxblFlag || "N",
// warrTxblNtxblFlag: firstLine.warrTxblNtxblFlag || "N",
// intrTxblNtxblFlag: firstLine.intrTxblNtxblFlag || "N",
// vlrCode: undefined,
bill: {
payType,
jobTotalHrs: "0",
billTime: "0",
billRate: "0"
},
amount: {
payType,
amtType: "Job",
custPrice: "0",
totalAmt: "0"
}
};
});
if (!ops.length) return null;
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 ----------
/**
@@ -63,25 +282,38 @@ const QueryJobData = async (ctx = {}, jobId) => {
/**
* Build Repair Order payload for RR from job and customer data.
* @param {Object} args
* @param job
* @param selectedCustomer
* @param advisorNo
* @param story
* @param makeOverride
* @returns {{outsdRoNo: string, repairOrderNumber: string, departmentType: string, vin: string, customerNo: string, advisorNo: string, mileageIn: *|null}}
* @param bodyshop
* @param allocations
* @param {string} [opCode] - RR OpCode for this RO (global default / override)
* @param {string} [taxCode] - RR tax code for header tax (e.g. state/prov code)
* @returns {Object}
*/
const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo, story }) => {
const buildRRRepairOrderPayload = ({
job,
selectedCustomer,
advisorNo,
story,
makeOverride,
allocations,
opCode,
taxCode
} = {}) => {
const customerNo = selectedCustomer?.customerNo
? String(selectedCustomer.customerNo).trim()
: selectedCustomer?.custNo
? String(selectedCustomer.custNo).trim()
: null;
if (!customerNo) throw new Error("No RR customer selected (customerNo/CustNo missing)");
if (!customerNo) throw new Error("No RR customer selected (customerNo/custNo missing)");
const adv = advisorNo != null && String(advisorNo).trim() !== "" ? String(advisorNo).trim() : null;
if (!adv) throw new Error("advisorNo is required for RR export");
if (!adv) throw new Error("advisorNo is required for RR export");
const vinRaw = job?.v_vin;
const vin =
@@ -98,9 +330,9 @@ const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo, story })
const roStr = String(ro);
const output = {
// Base payload shape expected by reynolds-rome-client (buildCreateRepairOrder)
const payload = {
outsdRoNo: roStr,
repairOrderNumber: roStr,
departmentType: "B",
vin,
customerNo: String(customerNo),
@@ -109,10 +341,57 @@ const buildRRRepairOrderPayload = ({ job, selectedCustomer, advisorNo, story })
};
if (story) {
output.roComment = String(story).trim();
payload.roComment = String(story).trim();
}
return output;
if (makeOverride) {
// Passed through so the template can override DMS Make if needed
payload.makeOverride = String(makeOverride).trim();
}
const haveAllocations = Array.isArray(allocations) && allocations.length > 0;
if (haveAllocations) {
const effectiveOpCode = (opCode && String(opCode).trim()) || null;
const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
if (effectiveOpCode) {
// Build RO.GOG and RO.LABOR in the new normalized shape
const rogg = buildRogogFromAllocations(allocations, {
opCode: effectiveOpCode,
payType: "Cust"
});
if (rogg) {
payload.rogg = rogg;
const rolabor = buildRolaborFromRogog(rogg, { payType: "Cust" });
if (rolabor) {
payload.rolabor = rolabor;
}
}
}
// --- TAX HEADER TEMPORARILY DISABLED ---
// We intentionally do NOT attach payload.tax right now so that the Mustache
// section that renders <TaxCodeInfo> stays false and no TaxCodeInfo is sent.
//
// Keeping this commented-out for future enablement once RR confirms header
// tax handling behaviour.
//
// if (effectiveTaxCode) {
// const taxInfo = buildTaxFromAllocations(allocations, {
// taxCode: effectiveTaxCode,
// payType: "Cust"
// });
//
// if (taxInfo) {
// payload.tax = taxInfo;
// }
// }
}
return payload;
};
/**
@@ -220,5 +499,10 @@ module.exports = {
makeCustomerSearchPayloadFromJob,
makeVehicleSearchPayloadFromJob,
normalizeCustomerCandidates,
normalizeVehicleCandidates
normalizeVehicleCandidates,
// exporting these so you can unit-test them directly if you want
buildRogogFromAllocations,
buildTaxFromAllocations,
buildRolaborSkeleton,
buildRolaborFromRogog
};