feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration / RRScratch2 / Checkpoint

This commit is contained in:
Dave
2025-11-25 14:13:10 -05:00
parent ae7d150a6c
commit 2b1836d450
4 changed files with 172 additions and 301 deletions

View File

@@ -81,6 +81,9 @@ const asN2 = (dineroLike) => {
* - <OpCodeLaborInfo> (ROLABOR)
* match 1:1, and ensures taxable/non-taxable flags line up by JobNo.
*
* We now also attach segmentKind/segmentIndex/segmentCount metadata on each op
* for UI/debug purposes. The XML templates can safely ignore these.
*
* @param {Array} allocations
* @param {Object} opts
* @param {string} opts.opCode - RR OpCode for the job (global, overridable)
@@ -93,10 +96,41 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
const ops = [];
const cents = (money) => {
if (!money) return 0;
if (typeof money.getAmount === "function") return money.getAmount();
if (typeof money === "object" && typeof money.amount === "number") return money.amount;
/**
* Normalize various "money-like" shapes to integer cents.
* Supports:
* - Dinero instances (getAmount / toUnit)
* - { cents }
* - { amount, precision }
* - plain numbers (treated as units, e.g. dollars)
*/
const toCents = (value) => {
if (!value) return 0;
if (typeof value.getAmount === "function") {
return value.getAmount();
}
if (typeof value.toUnit === "function") {
const unit = value.toUnit();
return Number.isFinite(unit) ? Math.round(unit * 100) : 0;
}
if (typeof value.cents === "number") {
return value.cents;
}
if (typeof value.amount === "number") {
const precision = typeof value.precision === "number" ? value.precision : 2;
if (precision === 2) return value.amount;
const factor = Math.pow(10, 2 - precision);
return Math.round(value.amount * factor);
}
if (typeof value === "number") {
return Math.round(value * 100);
}
return 0;
};
@@ -105,16 +139,6 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
precision: 2
});
const addMoney = (...ms) => {
let acc = null;
for (const m of ms) {
if (!m) continue;
if (!acc) acc = m;
else if (typeof acc.add === "function") acc = acc.add(m);
}
return acc;
};
for (const alloc of allocations) {
const pc = alloc?.profitCenter || {};
const breakOut = pc.rr_gogcode;
@@ -123,40 +147,40 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
// Only centers configured for RR GOG are included
if (!breakOut || !itemType) continue;
const partsSale = alloc.partsSale || null;
const extrasSale = alloc.extrasSale || null;
const laborTaxableSale = alloc.laborTaxableSale || null;
const laborNonTaxableSale = alloc.laborNonTaxableSale || null;
const costMoney = alloc.cost || null;
const partsCents = toCents(alloc.partsSale);
const extrasCents = toCents(alloc.extrasSale);
const laborTaxableCents = toCents(alloc.laborTaxableSale);
const laborNonTaxableCents = toCents(alloc.laborNonTaxableSale);
const costCents = toCents(alloc.cost);
// Parts + extras share a single segment
const partsExtrasSale = addMoney(partsSale, extrasSale);
const partsExtrasCents = partsCents + extrasCents;
const segments = [];
// 1) Parts + extras segment (respect center's default tax flag)
if (partsExtrasSale && typeof partsExtrasSale.isZero === "function" && !partsExtrasSale.isZero()) {
if (partsExtrasCents !== 0) {
segments.push({
kind: "partsExtras",
sale: partsExtrasSale,
txFlag: pc.rr_cust_txbl_flag || "T"
saleCents: partsExtrasCents,
txFlag: pc.rr_cust_txbl_flag || "N"
});
}
// 2) Taxable labor segment -> "T"
if (laborTaxableSale && typeof laborTaxableSale.isZero === "function" && !laborTaxableSale.isZero()) {
if (laborTaxableCents !== 0) {
segments.push({
kind: "laborTaxable",
sale: laborTaxableSale,
saleCents: laborTaxableCents,
txFlag: "T"
});
}
// 3) Non-taxable labor segment -> "N"
if (laborNonTaxableSale && typeof laborNonTaxableSale.isZero === "function" && !laborNonTaxableSale.isZero()) {
if (laborNonTaxableCents !== 0) {
segments.push({
kind: "laborNonTaxable",
sale: laborNonTaxableSale,
saleCents: laborNonTaxableCents,
txFlag: "N"
});
}
@@ -164,32 +188,32 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
if (!segments.length) continue;
// Proportionally split cost across segments based on their sale amounts
const totalCostCents = cents(costMoney);
const totalSaleCents = segments.reduce((sum, seg) => sum + cents(seg.sale), 0);
const totalCostCents = costCents;
const totalSaleCents = segments.reduce((sum, seg) => sum + seg.saleCents, 0);
let remainingCostCents = totalCostCents;
segments.forEach((seg, idx) => {
let costCents = 0;
let segCost = 0;
if (totalCostCents > 0 && totalSaleCents > 0) {
if (idx === segments.length - 1) {
// Last segment gets the remainder to avoid rounding drift
costCents = remainingCostCents;
segCost = remainingCostCents;
} else {
const segSaleCents = cents(seg.sale);
costCents = Math.round((segSaleCents / totalSaleCents) * totalCostCents);
remainingCostCents -= costCents;
segCost = Math.round((seg.saleCents / totalSaleCents) * totalCostCents);
remainingCostCents -= segCost;
}
}
seg.costCents = costCents;
seg.costCents = segCost;
});
const itemDescBase = pc.accountdesc || pc.accountname || alloc.center || "";
const segmentCount = segments.length;
// NEW: each segment becomes its own op / JobNo with a single line
segments.forEach((seg) => {
// Each segment becomes its own op / JobNo with a single line
segments.forEach((seg, idx) => {
const jobNo = String(ops.length + 1); // global, 1-based JobNo across all centers/segments
const line = {
@@ -198,11 +222,11 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
itemDesc: itemDescBase,
custQty: "1.0",
custPayTypeFlag: "C",
custTxblNTxblFlag: seg.txFlag || "T",
custTxblNtxblFlag: seg.txFlag || "N",
amount: {
payType,
amtType: "Unit",
custPrice: asN2(seg.sale),
custPrice: asN2(asMoneyLike(seg.saleCents)),
dlrCost: asN2(asMoneyLike(seg.costCents))
}
};
@@ -210,7 +234,11 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
ops.push({
opCode,
jobNo,
lines: [line] // exactly one AllGogLineItmInfo per AllGogOpCodeInfo
lines: [line], // exactly one AllGogLineItmInfo per AllGogOpCodeInfo
// Extra metadata for UI / debugging
segmentKind: seg.kind,
segmentIndex: idx,
segmentCount
});
});
}
@@ -240,7 +268,7 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
// Pull tax flag from the GOG line.
// Prefer the property we set in buildRogogFromAllocations (custTxblNTxblFlag),
// but also accept custTxblNtxblFlag in case we ever change naming.
const txFlag = firstLine.custTxblNTxblFlag ?? firstLine.custTxblNtxblFlag ?? "N";
const txFlag = firstLine.custTxblNtxblFlag ?? "N";
const linePayType = firstLine.custPayTypeFlag || "C";
@@ -248,7 +276,6 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
opCode: op.opCode,
jobNo: op.jobNo,
custPayTypeFlag: linePayType,
// This is the property the Mustache template uses for <CustTxblNTxblFlag>
custTxblNtxblFlag: txFlag,
bill: {
payType,