feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration / RRScratch2 / Checkpoint
This commit is contained in:
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
@@ -15,54 +14,6 @@ const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RrAllocationsSummary);
|
||||
|
||||
function normalizeDineroJson(d) {
|
||||
if (!d) return null;
|
||||
|
||||
// If it's already a Dinero instance, leave it to the caller
|
||||
if (typeof d.toUnit === "function") return d;
|
||||
|
||||
// New server shape: { cents: 54144, currency?: "USD" }
|
||||
if (typeof d.cents === "number") {
|
||||
return {
|
||||
amount: d.cents,
|
||||
precision: 2,
|
||||
currency: d.currency || "USD"
|
||||
};
|
||||
}
|
||||
|
||||
// Classic Dinero JSON: { amount, precision, currency }
|
||||
if (typeof d.amount === "number") {
|
||||
return {
|
||||
amount: d.amount,
|
||||
precision: d.precision ?? 2,
|
||||
currency: d.currency || "USD"
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Dinero-like object into an "N2" string ("123.45").
|
||||
* Works with real Dinero instances or plain JSON objects
|
||||
* that have { amount, precision }.
|
||||
*/
|
||||
function dineroToN2(dineroLike) {
|
||||
if (!dineroLike) return "0.00";
|
||||
|
||||
// If it's an actual Dinero instance
|
||||
if (typeof dineroLike.toUnit === "function") {
|
||||
return dineroLike.toUnit().toFixed(2);
|
||||
}
|
||||
|
||||
const normalized = normalizeDineroJson(dineroLike);
|
||||
if (!normalized) return "0.00";
|
||||
|
||||
const { amount, precision = 2 } = normalized;
|
||||
const value = amount / Math.pow(10, precision);
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize job allocations into a flat list for display / preview building.
|
||||
* @param ack
|
||||
@@ -89,209 +40,20 @@ function normalizeJobAllocations(ack) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal ROGOG preview from job allocation rows.
|
||||
* Mirrors the backend buildRogogFromAllocations logic:
|
||||
* - parts+extras segment
|
||||
* - taxable labor segment
|
||||
* - non-taxable labor segment
|
||||
* Each segment becomes its *own* JobNo with a single GOG line.
|
||||
*/
|
||||
function buildRogogPreviewFromJobRows(jobRows, opCode) {
|
||||
if (!Array.isArray(jobRows) || !jobRows.length || !opCode) return null;
|
||||
|
||||
const ops = [];
|
||||
|
||||
const toDinero = (d) => {
|
||||
if (!d) return Dinero();
|
||||
if (typeof d.toUnit === "function") return d;
|
||||
|
||||
const normalized = normalizeDineroJson(d);
|
||||
return normalized ? Dinero(normalized) : Dinero();
|
||||
};
|
||||
|
||||
const cents = (d) => toDinero(d).getAmount();
|
||||
|
||||
const segmentLabelMap = {
|
||||
partsExtras: "Parts/Extras",
|
||||
laborTaxable: "Taxable Labor",
|
||||
laborNonTaxable: "Non-Taxable Labor"
|
||||
};
|
||||
|
||||
for (const row of jobRows) {
|
||||
const pc = row.profitCenter || {};
|
||||
const breakOut = pc.rr_gogcode;
|
||||
const itemType = pc.rr_item_type;
|
||||
|
||||
// Only centers configured for RR GOG should appear
|
||||
if (!breakOut || !itemType) continue;
|
||||
|
||||
// Bucketed sales (Dinero-like objects coming from the backend)
|
||||
const partsSale = toDinero(row.partsSale);
|
||||
const extrasSale = toDinero(row.extrasSale);
|
||||
const laborTaxableSale = toDinero(row.laborTaxableSale);
|
||||
const laborNonTaxableSale = toDinero(row.laborNonTaxableSale);
|
||||
const costMoney = toDinero(row.cost);
|
||||
|
||||
const partsExtrasSale = partsSale.add(extrasSale);
|
||||
|
||||
const segments = [];
|
||||
|
||||
// 1) Parts + extras segment (uses center's default tax flag)
|
||||
if (!partsExtrasSale.isZero()) {
|
||||
segments.push({
|
||||
kind: "partsExtras",
|
||||
sale: partsExtrasSale,
|
||||
txFlag: pc.rr_cust_txbl_flag || "T"
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Taxable labor -> always "T"
|
||||
if (!laborTaxableSale.isZero()) {
|
||||
segments.push({
|
||||
kind: "laborTaxable",
|
||||
sale: laborTaxableSale,
|
||||
txFlag: "T"
|
||||
});
|
||||
}
|
||||
|
||||
// 3) Non-taxable labor -> always "N"
|
||||
if (!laborNonTaxableSale.isZero()) {
|
||||
segments.push({
|
||||
kind: "laborNonTaxable",
|
||||
sale: laborNonTaxableSale,
|
||||
txFlag: "N"
|
||||
});
|
||||
}
|
||||
|
||||
if (!segments.length) continue;
|
||||
|
||||
// Proportionally split cost across segments (same logic as backend)
|
||||
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 || row.center || "";
|
||||
|
||||
// 🔑 Each segment becomes its own JobNo with a single GOG line
|
||||
segments.forEach((seg, segIndex) => {
|
||||
const jobNo = String(ops.length + 1); // 1-based, global across all centers
|
||||
const segmentCount = segments.length;
|
||||
const segmentKind = seg.kind;
|
||||
const segmentLabel = segmentLabelMap[segmentKind] || segmentKind;
|
||||
|
||||
// If there is a split, annotate the description so it’s obvious which segment this is
|
||||
const displayDesc = segmentCount > 1 ? `${itemDescBase} (${segmentLabel})` : itemDescBase;
|
||||
|
||||
const saleN2 = dineroToN2(seg.sale);
|
||||
const costN2 = dineroToN2({
|
||||
amount: seg.costCents || 0,
|
||||
precision: 2
|
||||
});
|
||||
|
||||
ops.push({
|
||||
opCode,
|
||||
jobNo,
|
||||
segmentKind,
|
||||
segmentIndex: segIndex,
|
||||
segmentCount,
|
||||
lines: [
|
||||
{
|
||||
breakOut,
|
||||
itemType,
|
||||
itemDesc: displayDesc,
|
||||
custQty: "1.0",
|
||||
custPayTypeFlag: "C",
|
||||
// canonical property name used on the server
|
||||
custTxblNTxblFlag: seg.txFlag || "T",
|
||||
// legacy alias used by the table
|
||||
custTxblNtxblFlag: seg.txFlag || "T",
|
||||
amount: {
|
||||
payType: "Cust",
|
||||
amtType: "Unit",
|
||||
custPrice: saleN2,
|
||||
dlrCost: costN2
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!ops.length) return null;
|
||||
|
||||
return {
|
||||
roNo: null, // preview only
|
||||
ops
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal ROLABOR preview from a ROGOG preview.
|
||||
* Mirrors server-side buildRolaborFromRogog.
|
||||
*/
|
||||
function buildRolaborPreviewFromRogog(rogg) {
|
||||
if (!rogg || !Array.isArray(rogg.ops)) return null;
|
||||
|
||||
const ops = rogg.ops.map((op) => {
|
||||
const firstLine = op.lines?.[0] || {};
|
||||
|
||||
// Prefer the server-side property name, but fall back to legacy
|
||||
const txFlag = firstLine.custTxblNTxblFlag ?? firstLine.custTxblNtxblFlag ?? "N";
|
||||
const payFlag = firstLine.custPayTypeFlag || "C";
|
||||
|
||||
return {
|
||||
opCode: op.opCode,
|
||||
jobNo: op.jobNo,
|
||||
custPayTypeFlag: payFlag,
|
||||
// this is what the table uses
|
||||
custTxblNtxblFlag: txFlag,
|
||||
bill: {
|
||||
payType: "Cust",
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
},
|
||||
amount: {
|
||||
payType: "Cust",
|
||||
amtType: "Job",
|
||||
custPrice: "0",
|
||||
totalAmt: "0"
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (!ops.length) return null;
|
||||
return { ops };
|
||||
}
|
||||
|
||||
/**
|
||||
* RR-specific DMS Allocations Summary
|
||||
* Focused on what we actually send to RR:
|
||||
* - ROGOG (split by taxable / non-taxable segments)
|
||||
* - ROLABOR shell
|
||||
*
|
||||
* The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags)
|
||||
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
|
||||
* This component just renders the preview from `ack.rogg` / `ack.rolabor`.
|
||||
*/
|
||||
export function RrAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
const { t } = useTranslation();
|
||||
const [jobRows, setJobRows] = useState([]);
|
||||
const [roggPreview, setRoggPreview] = useState(null);
|
||||
const [rolaborPreview, setRolaborPreview] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchAllocations = useCallback(() => {
|
||||
@@ -300,7 +62,8 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
try {
|
||||
socket.emit("rr-calculate-allocations", jobId, (ack) => {
|
||||
if (ack && ack.ok === false) {
|
||||
setJobRows([]);
|
||||
setRoggPreview(null);
|
||||
setRolaborPreview(null);
|
||||
setError(ack.error || t("dms.labels.allocations_error"));
|
||||
if (socket) {
|
||||
socket.allocationsSummary = [];
|
||||
@@ -311,7 +74,8 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
|
||||
const jobAllocRows = normalizeJobAllocations(ack);
|
||||
|
||||
setJobRows(jobAllocRows);
|
||||
setRoggPreview(ack?.rogg || null);
|
||||
setRolaborPreview(ack?.rolabor || null);
|
||||
setError(null);
|
||||
|
||||
if (socket) {
|
||||
@@ -320,7 +84,8 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
setJobRows([]);
|
||||
setRoggPreview(null);
|
||||
setRolaborPreview(null);
|
||||
setError(t("dms.labels.allocations_error"));
|
||||
if (socket) {
|
||||
socket.allocationsSummary = [];
|
||||
@@ -334,29 +99,38 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
|
||||
const opCode = bodyshop?.rr_configuration?.baseOpCode || "28TOZ";
|
||||
|
||||
const roggPreview = useMemo(() => buildRogogPreviewFromJobRows(jobRows, opCode), [jobRows, opCode]);
|
||||
const rolaborPreview = useMemo(() => buildRolaborPreviewFromRogog(roggPreview), [roggPreview]);
|
||||
const segmentLabelMap = {
|
||||
partsExtras: "Parts/Extras",
|
||||
laborTaxable: "Taxable Labor",
|
||||
laborNonTaxable: "Non-Taxable Labor"
|
||||
};
|
||||
|
||||
const roggRows = useMemo(() => {
|
||||
if (!roggPreview || !Array.isArray(roggPreview.ops)) return [];
|
||||
const rows = [];
|
||||
roggPreview.ops.forEach((op) => {
|
||||
(op.lines || []).forEach((line, idx) => {
|
||||
const baseDesc = line.itemDesc;
|
||||
const segmentKind = op.segmentKind;
|
||||
const segmentCount = op.segmentCount || 0;
|
||||
const segmentLabel = segmentLabelMap[segmentKind] || segmentKind;
|
||||
const displayDesc = segmentCount > 1 && segmentLabel ? `${baseDesc} (${segmentLabel})` : baseDesc;
|
||||
|
||||
rows.push({
|
||||
key: `${op.jobNo}-${idx}`,
|
||||
opCode: op.opCode,
|
||||
jobNo: op.jobNo,
|
||||
breakOut: line.breakOut,
|
||||
itemType: line.itemType,
|
||||
itemDesc: line.itemDesc,
|
||||
itemDesc: displayDesc,
|
||||
custQty: line.custQty,
|
||||
custPayTypeFlag: line.custPayTypeFlag,
|
||||
custTxblNtxblFlag: line.custTxblNtxblFlag,
|
||||
custPrice: line.amount?.custPrice,
|
||||
dlrCost: line.amount?.dlrCost,
|
||||
// segment metadata for visual styling
|
||||
segmentKind: op.segmentKind,
|
||||
segmentCount: op.segmentCount
|
||||
segmentKind,
|
||||
segmentCount
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
// server/rr/rr-calculate-allocations.js
|
||||
/**
|
||||
* THIS IS A COPY of CDKCalculateAllocations, modified to:
|
||||
* - Only calculate allocations needed for Reynolds & RR exports
|
||||
* - Keep sales broken down into buckets (parts, taxable labor, non-taxable labor, extras)
|
||||
* - Add extra logging for easier debugging
|
||||
*
|
||||
* Original comments follow.
|
||||
*/
|
||||
|
||||
const { GraphQLClient } = require("graphql-request");
|
||||
const Dinero = require("dinero.js");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
|
||||
const { QueryJobData } = require("./rr-job-helpers");
|
||||
const { QueryJobData, buildRogogFromAllocations, buildRolaborFromRogog } = require("./rr-job-helpers");
|
||||
const { exportJobToRR, finalizeRRRepairOrder } = require("./rr-job-export");
|
||||
const CdkCalculateAllocations = require("./rr-calculate-allocations").default;
|
||||
const RRCalculateAllocations = require("./rr-calculate-allocations").default;
|
||||
const { createRRCustomer } = require("./rr-customers");
|
||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||
const { classifyRRVendorError } = require("./rr-errors");
|
||||
@@ -898,10 +898,73 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
socket.on("rr-calculate-allocations", async (jobid, cb) => {
|
||||
try {
|
||||
CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: begin", { jobid });
|
||||
const allocations = await CdkCalculateAllocations(socket, jobid);
|
||||
cb?.(allocations);
|
||||
socket.emit("rr-calculate-allocations:result", allocations);
|
||||
CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: success", { items: allocations?.length });
|
||||
|
||||
const raw = await RRCalculateAllocations(socket, jobid);
|
||||
|
||||
// If the helper returns an explicit error shape, just pass it through.
|
||||
if (raw && raw.ok === false) {
|
||||
cb?.(raw);
|
||||
socket.emit("rr-calculate-allocations:result", raw);
|
||||
CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: helper returned error", {
|
||||
jobid,
|
||||
error: raw.error
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let ack;
|
||||
let jobAllocations;
|
||||
|
||||
if (Array.isArray(raw)) {
|
||||
// Legacy shape: plain allocations array
|
||||
jobAllocations = raw;
|
||||
ack = { jobAllocations: raw };
|
||||
} else {
|
||||
ack = raw || {};
|
||||
jobAllocations = Array.isArray(ack.jobAllocations) ? ack.jobAllocations : [];
|
||||
}
|
||||
|
||||
// Try to derive OpCode from bodyshop; fall back to default
|
||||
let opCode = "28TOZ";
|
||||
|
||||
try {
|
||||
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
|
||||
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
|
||||
opCode = bodyshop?.rr_configuration?.baseOpCode || opCode;
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "WARN", "rr-calculate-allocations: bodyshop lookup failed, using default OpCode", {
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
|
||||
let rogg = null;
|
||||
let rolabor = null;
|
||||
|
||||
try {
|
||||
rogg = buildRogogFromAllocations(jobAllocations, { opCode, payType: "Cust" });
|
||||
if (rogg) {
|
||||
rolabor = buildRolaborFromRogog(rogg, { payType: "Cust" });
|
||||
}
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "WARN", "rr-calculate-allocations: failed to build ROGOG/ROLABOR preview", {
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
|
||||
const enriched = {
|
||||
...ack,
|
||||
rogg,
|
||||
rolabor
|
||||
};
|
||||
|
||||
cb?.(enriched);
|
||||
socket.emit("rr-calculate-allocations:result", enriched);
|
||||
CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: success", {
|
||||
jobid,
|
||||
jobAllocations: jobAllocations.length,
|
||||
hasRogg: !!rogg,
|
||||
hasRolabor: !!rolabor
|
||||
});
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "ERROR", "rr-calculate-allocations: failed", { error: e.message, jobid });
|
||||
cb?.({ ok: false, error: e.message });
|
||||
|
||||
Reference in New Issue
Block a user