feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration / RRScratch2 / Checkpoint
This commit is contained in:
@@ -54,7 +54,33 @@ const asN2 = (dineroLike) => {
|
||||
|
||||
/**
|
||||
* Build RO.GOG structure for the reynolds-rome-client `createRepairOrder` payload
|
||||
* from CDK allocations.
|
||||
* from allocations.
|
||||
*
|
||||
* Supports the new allocation shape:
|
||||
* {
|
||||
* center,
|
||||
* partsSale,
|
||||
* laborTaxableSale,
|
||||
* laborNonTaxableSale,
|
||||
* extrasSale,
|
||||
* totalSale,
|
||||
* cost,
|
||||
* profitCenter,
|
||||
* costCenter
|
||||
* }
|
||||
*
|
||||
* For each center, we can emit up to 3 GOG *segments*:
|
||||
* - parts+extras (uses profitCenter.rr_cust_txbl_flag)
|
||||
* - taxable labor (CustTxblNTxblFlag="T")
|
||||
* - non-tax labor (CustTxblNTxblFlag="N")
|
||||
*
|
||||
* IMPORTANT CHANGE:
|
||||
* Each segment becomes its OWN JobNo / AllGogOpCodeInfo, with exactly one
|
||||
* AllGogLineItmInfo inside. This makes the count of:
|
||||
* - <AllGogOpCodeInfo> (ROGOG)
|
||||
* - <OpCodeLaborInfo> (ROLABOR)
|
||||
* match 1:1, and ensures taxable/non-taxable flags line up by JobNo.
|
||||
*
|
||||
* @param {Array} allocations
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.opCode - RR OpCode for the job (global, overridable)
|
||||
@@ -67,45 +93,125 @@ 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;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const asMoneyLike = (amountCents) => ({
|
||||
amount: amountCents || 0,
|
||||
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;
|
||||
const itemType = pc.rr_item_type;
|
||||
|
||||
// Only centers that have been configured for RR GOG are included
|
||||
// Only centers configured for RR GOG are included
|
||||
if (!breakOut || !itemType) continue;
|
||||
|
||||
const saleN2 = asN2(alloc.sale);
|
||||
const costN2 = asN2(alloc.cost);
|
||||
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 itemDesc = pc.accountdesc || pc.accountname || alloc.center || "";
|
||||
const jobNo = String(ops.length + 1); // 1-based JobNo
|
||||
// Parts + extras share a single segment
|
||||
const partsExtrasSale = addMoney(partsSale, extrasSale);
|
||||
|
||||
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
|
||||
}
|
||||
const segments = [];
|
||||
|
||||
// 1) Parts + extras segment (respect center's default tax flag)
|
||||
if (partsExtrasSale && typeof partsExtrasSale.isZero === "function" && !partsExtrasSale.isZero()) {
|
||||
segments.push({
|
||||
kind: "partsExtras",
|
||||
sale: partsExtrasSale,
|
||||
txFlag: pc.rr_cust_txbl_flag || "T"
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Taxable labor segment -> "T"
|
||||
if (laborTaxableSale && typeof laborTaxableSale.isZero === "function" && !laborTaxableSale.isZero()) {
|
||||
segments.push({
|
||||
kind: "laborTaxable",
|
||||
sale: laborTaxableSale,
|
||||
txFlag: "T"
|
||||
});
|
||||
}
|
||||
|
||||
// 3) Non-taxable labor segment -> "N"
|
||||
if (laborNonTaxableSale && typeof laborNonTaxableSale.isZero === "function" && !laborNonTaxableSale.isZero()) {
|
||||
segments.push({
|
||||
kind: "laborNonTaxable",
|
||||
sale: laborNonTaxableSale,
|
||||
txFlag: "N"
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
let remainingCostCents = totalCostCents;
|
||||
|
||||
segments.forEach((seg, idx) => {
|
||||
let costCents = 0;
|
||||
|
||||
if (totalCostCents > 0 && totalSaleCents > 0) {
|
||||
if (idx === segments.length - 1) {
|
||||
// Last segment gets the remainder to avoid rounding drift
|
||||
costCents = remainingCostCents;
|
||||
} else {
|
||||
const segSaleCents = cents(seg.sale);
|
||||
costCents = Math.round((segSaleCents / totalSaleCents) * totalCostCents);
|
||||
remainingCostCents -= costCents;
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
seg.costCents = costCents;
|
||||
});
|
||||
|
||||
const itemDescBase = pc.accountdesc || pc.accountname || alloc.center || "";
|
||||
|
||||
// NEW: each segment becomes its own op / JobNo with a single line
|
||||
segments.forEach((seg) => {
|
||||
const jobNo = String(ops.length + 1); // global, 1-based JobNo across all centers/segments
|
||||
|
||||
const line = {
|
||||
breakOut,
|
||||
itemType,
|
||||
itemDesc: itemDescBase,
|
||||
custQty: "1.0",
|
||||
custPayTypeFlag: "C",
|
||||
custTxblNTxblFlag: seg.txFlag || "T",
|
||||
amount: {
|
||||
payType,
|
||||
amtType: "Unit",
|
||||
custPrice: asN2(seg.sale),
|
||||
dlrCost: asN2(asMoneyLike(seg.costCents))
|
||||
}
|
||||
};
|
||||
|
||||
ops.push({
|
||||
opCode,
|
||||
jobNo,
|
||||
lines: [line] // exactly one AllGogLineItmInfo per AllGogOpCodeInfo
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,16 +237,19 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
|
||||
const ops = rogg.ops.map((op) => {
|
||||
const firstLine = op.lines?.[0] || {};
|
||||
|
||||
// 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 linePayType = firstLine.custPayTypeFlag || "C";
|
||||
|
||||
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,
|
||||
custPayTypeFlag: linePayType,
|
||||
// This is the property the Mustache template uses for <CustTxblNTxblFlag>
|
||||
custTxblNtxblFlag: txFlag,
|
||||
bill: {
|
||||
payType,
|
||||
jobTotalHrs: "0",
|
||||
@@ -277,24 +386,6 @@ const buildRRRepairOrderPayload = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
@@ -400,23 +491,49 @@ 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}}
|
||||
* Build split labor lines from job allocations.
|
||||
* @param jobAllocations
|
||||
* @returns {*[]}
|
||||
*/
|
||||
const buildSplitLaborLinesFromAllocations = (jobAllocations) => {
|
||||
const lines = [];
|
||||
|
||||
for (const alloc of jobAllocations || []) {
|
||||
const { center, laborTaxableSale, laborNonTaxableSale, profitCenter, costCenter } = alloc;
|
||||
|
||||
// Taxable labor
|
||||
if (laborTaxableSale && !laborTaxableSale.isZero()) {
|
||||
lines.push({
|
||||
centerName: center,
|
||||
profitCenter,
|
||||
costCenter,
|
||||
amount: laborTaxableSale,
|
||||
isTaxable: true,
|
||||
source: "labor"
|
||||
});
|
||||
}
|
||||
|
||||
// Non-taxable labor
|
||||
if (laborNonTaxableSale && !laborNonTaxableSale.isZero()) {
|
||||
lines.push({
|
||||
centerName: center,
|
||||
profitCenter,
|
||||
costCenter,
|
||||
amount: laborNonTaxableSale,
|
||||
isTaxable: false,
|
||||
source: "labor"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
QueryJobData,
|
||||
buildRRRepairOrderPayload,
|
||||
makeCustomerSearchPayloadFromJob,
|
||||
buildSplitLaborLinesFromAllocations,
|
||||
makeVehicleSearchPayloadFromJob,
|
||||
normalizeCustomerCandidates,
|
||||
normalizeVehicleCandidates,
|
||||
|
||||
Reference in New Issue
Block a user