diff --git a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx index c04f2a62a..a1cfd3f10 100644 --- a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx +++ b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx @@ -18,7 +18,21 @@ export default connect(mapStateToProps, mapDispatchToProps)(RrAllocationsSummary /** * Normalize job allocations into a flat list for display / preview building. * @param ack - * @returns {{center: *, sale, partsSale, laborTaxableSale, laborNonTaxableSale, extrasSale, cost, profitCenter, costCenter}[]|*[]} + * @returns {{ + * center: *, + * sale: *, + * partsSale: *, + * partsTaxableSale: *, + * partsNonTaxableSale: *, + * laborTaxableSale: *, + * laborNonTaxableSale: *, + * extrasSale: *, + * extrasTaxableSale: *, + * extrasNonTaxableSale: *, + * cost: *, + * profitCenter: *, + * costCenter: * + * }[]|*[]} */ function normalizeJobAllocations(ack) { if (!ack || !Array.isArray(ack.jobAllocations)) return []; @@ -31,9 +45,13 @@ function normalizeJobAllocations(ack) { // bucketed sales used to build split ROGOG/ROLABOR partsSale: row.partsSale || null, + partsTaxableSale: row.partsTaxableSale || null, + partsNonTaxableSale: row.partsNonTaxableSale || null, laborTaxableSale: row.laborTaxableSale || null, laborNonTaxableSale: row.laborNonTaxableSale || null, extrasSale: row.extrasSale || null, + extrasTaxableSale: row.extrasTaxableSale || null, + extrasNonTaxableSale: row.extrasNonTaxableSale || null, cost: row.cost || null, profitCenter: row.profitCenter || null, @@ -111,9 +129,12 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat }, [fetchAllocations]); const segmentLabelMap = { - partsExtras: "Parts/Extras", - laborTaxable: "Taxable Labor", - laborNonTaxable: "Non-Taxable Labor" + partsTaxable: "Parts Taxable", + partsNonTaxable: "Parts Non-Taxable", + extrasTaxable: "Extras Taxable", + extrasNonTaxable: "Extras Non-Taxable", + laborTaxable: "Labor Taxable", + laborNonTaxable: "Labor Non-Taxable" }; const roggRows = useMemo(() => { @@ -149,7 +170,7 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat }); }); return rows; - }, [roggPreview, opCode]); + }, [roggPreview, opCode, segmentLabelMap]); const rolaborRows = useMemo(() => { if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return []; @@ -231,7 +252,8 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat <> OpCode: {effectiveOpCode}. Only centers with RR GOG mapping (rr_gogcode & rr_item_type) - are included. Totals below reflect exactly what will be sent in ROGOG. + are included. Totals below reflect exactly what will be sent in ROGOG, with parts, extras, and labor split + into taxable / non-taxable segments. (v == null ? 0 : Number(v) || 0); + + const federalTaxRate = toNumber(job.federal_tax_rate); + const stateTaxRate = toNumber(job.state_tax_rate); + const localTaxRate = toNumber(job.local_tax_rate); + + const hasFederalRate = federalTaxRate > 0; + const hasState = stateTaxRate > 0; + const hasLocal = localTaxRate > 0; + + // "hasFederal" kept for backwards compatibility / logging (Canada only) + const hasFederal = isImex && hasFederalRate; + + // Canada: if ANY of federal / state / local > 0, treat the job as + // "everything taxable by default", then let line-level flags override + // for parts where applicable. + const globalAllTaxCanada = isImex && (hasFederalRate || hasState || hasLocal); + + const hasAnySalesTax = hasFederalRate || hasState || hasLocal; + + // Parts tax rate map (PAA/PAC/…) + let partTaxRates = job.part_tax_rates || job.parts_tax_rates || {}; + if (typeof partTaxRates === "string") { + try { + partTaxRates = JSON.parse(partTaxRates); + } catch { + partTaxRates = {}; + } + } + if (!partTaxRates || typeof partTaxRates !== "object") { + partTaxRates = {}; + } + + const tax_lbr_rt = toNumber(job.tax_lbr_rt); // labour + const tax_paint_mat_rt = toNumber(job.tax_paint_mat_rt || job.tax_paint_mt_rate); // MAPA + const tax_shop_mat_rt = toNumber(job.tax_shop_mat_rt); // MASH + const tax_tow_rt = toNumber(job.tax_tow_rt); // towing + const tax_sub_rt = toNumber(job.tax_sub_rt); // sublet (rarely used directly) + + const hasAnyPartsWithTax = Object.values(partTaxRates).some( + (entry) => entry && entry.prt_tax_in && toNumber(entry.prt_tax_rt) > 0 + ); + + const hasAnyTax = + hasAnySalesTax || + tax_lbr_rt > 0 || + tax_paint_mat_rt > 0 || + tax_shop_mat_rt > 0 || + tax_tow_rt > 0 || + tax_sub_rt > 0 || + hasAnyPartsWithTax; + + return { + isImex, + isRome, + + federalTaxRate, + stateTaxRate, + localTaxRate, + + hasFederal, + hasState, + hasLocal, + hasAnySalesTax, + globalAllTaxCanada, + + partTaxRates, + tax_lbr_rt, + tax_paint_mat_rt, + tax_shop_mat_rt, + tax_tow_rt, + tax_sub_rt, + hasAnyPartsWithTax, + hasAnyTax + }; +} + +/** + * Resolve the "PA" / part-type code (PAA/PAC/…) from a job line. + */ +function resolvePartType(line = {}) { + return line.part_type || line.partType || line.pa_code || line.pa || null; } /** * Decide if a *part* line is taxable vs non-taxable for RR. - * For now we mirror the same flag; this can be extended with CAD-specific - * logic (federal_tax_rate, parts_tax_rate, prt_tax_in, etc.) later. + * + * Rules: + * - Canada (IMEX): + * - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0 + * => everything is taxable by default (globalAllTaxCanada), + * unless tax_part is explicitly false. + * - Otherwise, use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0), + * with tax_part as final override. + * - US (ROME): + * - Use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0), + * with tax_part as final override. + * + * - line.tax_part is treated as the *final* check: + * - tax_part === false => always non-taxable. + * - tax_part === true => always taxable, even if we have no table entry. */ -function isPartTaxable(line) { - return line.tax_part; +function isPartTaxable(line = {}, taxCtx) { + if (!taxCtx) return !!line.tax_part; + + const { globalAllTaxCanada, partTaxRates } = taxCtx; + + // Explicit per-line override to *not* tax. + if (typeof line.tax_part === "boolean" && line.tax_part === false) { + return false; + } + + // Canada: any federal/state/local tax rate set => all parts taxable, + // unless explicitly turned off above. + if (globalAllTaxCanada) { + return true; + } + + let taxable = false; + + const partType = resolvePartType(line); + if (partType && partTaxRates && partTaxRates[partType]) { + const entry = partTaxRates[partType]; + const rate = Number(entry?.prt_tax_rt || 0); + const indicator = !!entry?.prt_tax_in; + taxable = indicator && rate > 0; + } + + // tax_part === true is treated as "final yes" even if we didn't find + // a matching part_tax_rate entry. + if (typeof line.tax_part === "boolean" && line.tax_part === true) { + taxable = true; + } + + return taxable; +} + +/** + * Decide if *labour* for this job is taxable. + * + * - Canada (IMEX): + * - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0 + * (globalAllTaxCanada) => all labour is taxable. + * - Else if tax_lbr_rt > 0 => labour taxable. + * - Else => non-taxable. + * - US (ROME): + * - tax_lbr_rt > 0 => labour taxable, otherwise not. + */ +function isLaborTaxable(_line, taxCtx) { + if (!taxCtx) return false; + const { isImex, globalAllTaxCanada, tax_lbr_rt } = taxCtx; + + if (isImex && globalAllTaxCanada) return true; + return tax_lbr_rt > 0; +} + +/** + * Taxability helpers for "extras" buckets. + * These are all job-level decisions; there are no per-line flags for them + * in the data we currently work with. + * + * Canada: if globalAllTaxCanada is true, we treat these as taxable. + */ +function isMapaTaxable(taxCtx) { + if (!taxCtx) return false; + const { isImex, globalAllTaxCanada, tax_paint_mat_rt } = taxCtx; + if (isImex && globalAllTaxCanada) return true; + return tax_paint_mat_rt > 0; +} + +function isMashTaxable(taxCtx) { + if (!taxCtx) return false; + const { isImex, globalAllTaxCanada, tax_shop_mat_rt } = taxCtx; + if (isImex && globalAllTaxCanada) return true; + return tax_shop_mat_rt > 0; +} + +function isTowTaxable(taxCtx) { + if (!taxCtx) return false; + const { isImex, globalAllTaxCanada, tax_tow_rt } = taxCtx; + if (isImex && globalAllTaxCanada) return true; + return tax_tow_rt > 0; +} + +/** + * Helper to push an "extra" (MAPA/MASH/towing/PAO/etc) amount into the + * appropriate taxable / non-taxable buckets for a given center. + */ +function addExtras(bucket, dineroAmount, isTaxable) { + if (!bucket || !dineroAmount || typeof dineroAmount.add !== "function") return; + bucket.extrasSale = bucket.extrasSale.add(dineroAmount); + if (isTaxable) { + bucket.extrasTaxableSale = bucket.extrasTaxableSale.add(dineroAmount); + } else { + bucket.extrasNonTaxableSale = bucket.extrasNonTaxableSale.add(dineroAmount); + } } /** * Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence. * Now stores *buckets* instead of a single Dinero per center. */ -function buildProfitCenterHash(job, debugLog) { +function buildProfitCenterHash(job, debugLog, taxContext) { let hasMapaLine = false; let hasMashLine = false; @@ -231,7 +435,7 @@ function buildProfitCenterHash(job, debugLog) { amount = amount.add(discount); } - const taxable = isPartTaxable(val); + const taxable = isPartTaxable(val, taxContext); if (taxable) { bucket.partsTaxableSale = bucket.partsTaxableSale.add(amount); @@ -254,7 +458,7 @@ function buildProfitCenterHash(job, debugLog) { amount: Math.round(rate * 100) }).multiply(val.mod_lb_hrs); - if (isLaborTaxable(val)) { + if (isLaborTaxable(val, taxContext)) { bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount); } else { bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount); @@ -274,7 +478,9 @@ function buildProfitCenterHash(job, debugLog) { partsNonTaxable: summarizeMoney(b.partsNonTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), - extras: summarizeMoney(b.extrasSale) + extras: summarizeMoney(b.extrasSale), + extrasTaxable: summarizeMoney(b.extrasTaxableSale), + extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale) })) }); @@ -353,39 +559,48 @@ function applyMapaMashManualLines({ profitCenterHash, hasMapaLine, hasMashLine, - debugLog + debugLog, + taxContext }) { - // MAPA + // MAPA (paint materials) if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) { const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA; const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName); if (mapaAccount) { + const amount = Dinero(job.job_totals.rates.mapa.total); + const taxable = isMapaTaxable(taxContext); + debugLog("Adding MAPA Line Manually", { mapaAccountName, - amount: summarizeMoney(Dinero(job.job_totals.rates.mapa.total)) + amount: summarizeMoney(amount), + taxable }); const bucket = ensureCenterBucket(profitCenterHash, mapaAccountName); - bucket.extrasSale = bucket.extrasSale.add(Dinero(job.job_totals.rates.mapa.total)); + addExtras(bucket, amount, taxable); } else { debugLog("NO MAPA ACCOUNT FOUND!!", { mapaAccountName }); } } - // MASH + // MASH (shop materials) if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) { const mashAccountName = selectedDmsAllocationConfig.profits.MASH; const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName); if (mashAccount) { + const amount = Dinero(job.job_totals.rates.mash.total); + const taxable = isMashTaxable(taxContext); + debugLog("Adding MASH Line Manually", { mashAccountName, - amount: summarizeMoney(Dinero(job.job_totals.rates.mash.total)) + amount: summarizeMoney(amount), + taxable }); const bucket = ensureCenterBucket(profitCenterHash, mashAccountName); - bucket.extrasSale = bucket.extrasSale.add(Dinero(job.job_totals.rates.mash.total)); + addExtras(bucket, amount, taxable); } else { debugLog("NO MASH ACCOUNT FOUND!!", { mashAccountName }); } @@ -467,9 +682,17 @@ function applyMaterialsCosting({ job, bodyshop, selectedDmsAllocationConfig, cos /** * Apply non-tax extras (PVRT, towing, storage, PAO). - * Extras go into the extrasSale bucket. + * Extras go into the extrasSale bucket (split taxable / non-taxable). */ -function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterHash, taxAllocations, debugLog }) { +function applyExtras({ + job, + bodyshop, + selectedDmsAllocationConfig, + profitCenterHash, + taxAllocations, + debugLog, + taxContext +}) { const { ca_bc_pvrt } = job; // BC PVRT -> state tax @@ -485,17 +708,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName); if (towAccount) { + const amount = Dinero({ + amount: Math.round((job.towing_payable || 0) * 100) + }); + const taxable = isTowTaxable(taxContext); + debugLog("Adding towing_payable to TOW account", { towAccountName, - towing_payable: job.towing_payable + towing_payable: job.towing_payable, + taxable }); const bucket = ensureCenterBucket(profitCenterHash, towAccountName); - bucket.extrasSale = bucket.extrasSale.add( - Dinero({ - amount: Math.round((job.towing_payable || 0) * 100) - }) - ); + addExtras(bucket, amount, taxable); } else { debugLog("NO TOW ACCOUNT FOUND!!", { towAccountName }); } @@ -507,17 +732,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName); if (towAccount) { + const amount = Dinero({ + amount: Math.round((job.storage_payable || 0) * 100) + }); + const taxable = isTowTaxable(taxContext); + debugLog("Adding storage_payable to TOW account", { storageAccountName, - storage_payable: job.storage_payable + storage_payable: job.storage_payable, + taxable }); const bucket = ensureCenterBucket(profitCenterHash, storageAccountName); - bucket.extrasSale = bucket.extrasSale.add( - Dinero({ - amount: Math.round((job.storage_payable || 0) * 100) - }) - ); + addExtras(bucket, amount, taxable); } else { debugLog("NO STORAGE/TOW ACCOUNT FOUND!!", { storageAccountName }); } @@ -529,17 +756,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName); if (otherAccount) { + const amount = Dinero({ + amount: Math.round((job.adjustment_bottom_line || 0) * 100) + }); + const taxable = !!(taxContext && taxContext.hasAnyTax); + debugLog("Adding adjustment_bottom_line to PAO", { otherAccountName, - adjustment_bottom_line: job.adjustment_bottom_line + adjustment_bottom_line: job.adjustment_bottom_line, + taxable }); const bucket = ensureCenterBucket(profitCenterHash, otherAccountName); - bucket.extrasSale = bucket.extrasSale.add( - Dinero({ - amount: Math.round((job.adjustment_bottom_line || 0) * 100) - }) - ); + addExtras(bucket, amount, taxable); } else { debugLog("NO PAO ACCOUNT FOUND!!", { otherAccountName }); } @@ -558,7 +787,8 @@ function applyRomeProfileAdjustments({ selectedDmsAllocationConfig, profitCenterHash, debugLog, - connectionData + connectionData, + taxContext }) { // Only relevant for Rome instances if (!InstanceManager({ rome: true })) return profitCenterHash; @@ -576,6 +806,8 @@ function applyRomeProfileAdjustments({ rateKeys: Object.keys(rateMap) }); + const extrasTaxable = !!(taxContext && taxContext.hasAnyTax); + // Parts adjustments Object.keys(partsAdjustments).forEach((key) => { const accountName = selectedDmsAllocationConfig.profits[key]; @@ -585,12 +817,13 @@ function applyRomeProfileAdjustments({ const bucket = ensureCenterBucket(profitCenterHash, accountName); const adjMoney = Dinero(partsAdjustments[key]); - bucket.extrasSale = bucket.extrasSale.add(adjMoney); + addExtras(bucket, adjMoney, extrasTaxable); debugLog("Added parts adjustment", { key, accountName, - adjustment: summarizeMoney(adjMoney) + adjustment: summarizeMoney(adjMoney), + taxable: extrasTaxable }); } else { CreateRRLogEvent( @@ -619,12 +852,13 @@ function applyRomeProfileAdjustments({ // Note: we intentionally use `rate.adjustments` here to mirror CDK behaviour const adjMoney = Dinero(rate.adjustments); - bucket.extrasSale = bucket.extrasSale.add(adjMoney); + addExtras(bucket, adjMoney, extrasTaxable); debugLog("Added rate adjustment", { key, accountName, - adjustment: summarizeMoney(adjMoney) + adjustment: summarizeMoney(adjMoney), + taxable: extrasTaxable }); } else { CreateRRLogEvent( @@ -651,6 +885,8 @@ function applyRomeProfileAdjustments({ * laborTaxableSale, * laborNonTaxableSale, * extrasSale, + * extrasTaxableSale, + * extrasNonTaxableSale, * totalSale, * cost, * profitCenter, @@ -663,10 +899,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo const jobAllocations = centers.map((center) => { const bucket = profitCenterHash[center] || emptyCenterBucket(); - const totalSale = bucket.partsSale - .add(bucket.laborTaxableSale) - .add(bucket.laborNonTaxableSale) - .add(bucket.extrasSale); + const extrasSale = bucket.extrasSale; + const totalSale = bucket.partsSale.add(bucket.laborTaxableSale).add(bucket.laborNonTaxableSale).add(extrasSale); const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === center); const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === center); @@ -684,7 +918,9 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo laborNonTaxableSale: bucket.laborNonTaxableSale, // Extras - extrasSale: bucket.extrasSale, + extrasSale, + extrasTaxableSale: bucket.extrasTaxableSale, + extrasNonTaxableSale: bucket.extrasNonTaxableSale, totalSale, @@ -705,6 +941,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo laborTaxable: summarizeMoney(row.laborTaxableSale), laborNonTaxable: summarizeMoney(row.laborNonTaxableSale), extras: summarizeMoney(row.extrasSale), + extrasTaxable: summarizeMoney(row.extrasTaxableSale), + extrasNonTaxable: summarizeMoney(row.extrasNonTaxableSale), totalSale: summarizeMoney(row.totalSale), cost: summarizeMoney(row.cost) })) @@ -812,12 +1050,35 @@ function calculateAllocations(connectionData, job) { timetickets: Array.isArray(job.timetickets) ? job.timetickets.length : 0 }); + const taxContext = buildTaxContext(job); + debugLog("Tax context initialised", { + isImex: taxContext.isImex, + isRome: taxContext.isRome, + federalTaxRate: taxContext.federalTaxRate, + stateTaxRate: taxContext.stateTaxRate, + localTaxRate: taxContext.localTaxRate, + hasFederal: taxContext.hasFederal, + hasState: taxContext.hasState, + hasLocal: taxContext.hasLocal, + globalAllTaxCanada: taxContext.globalAllTaxCanada, + tax_lbr_rt: taxContext.tax_lbr_rt, + tax_paint_mat_rt: taxContext.tax_paint_mat_rt, + tax_shop_mat_rt: taxContext.tax_shop_mat_rt, + tax_tow_rt: taxContext.tax_tow_rt, + hasAnyPartsWithTax: taxContext.hasAnyPartsWithTax, + hasAnyTax: taxContext.hasAnyTax + }); + // 1) Tax allocations let taxAllocations = buildTaxAllocations(bodyshop, job); debugLog("Initial taxAllocations", summarizeTaxAllocations(taxAllocations)); // 2) Profit centers from job lines + MAPA/MASH detection - const { profitCenterHash: initialProfitHash, hasMapaLine, hasMashLine } = buildProfitCenterHash(job, debugLog); + const { + profitCenterHash: initialProfitHash, + hasMapaLine, + hasMashLine + } = buildProfitCenterHash(job, debugLog, taxContext); // 3) DMS allocation config const selectedDmsAllocationConfig = @@ -842,7 +1103,8 @@ function calculateAllocations(connectionData, job) { profitCenterHash: initialProfitHash, hasMapaLine, hasMashLine, - debugLog + debugLog, + taxContext }); // 6) Materials costing (MAPA/MASH cost side) @@ -861,7 +1123,8 @@ function calculateAllocations(connectionData, job) { selectedDmsAllocationConfig, profitCenterHash, taxAllocations, - debugLog + debugLog, + taxContext })); // 8) Rome-only profile-level adjustments @@ -871,7 +1134,8 @@ function calculateAllocations(connectionData, job) { selectedDmsAllocationConfig, profitCenterHash, debugLog, - connectionData + connectionData, + taxContext }); debugLog("profitCenterHash before jobAllocations build", { @@ -882,7 +1146,9 @@ function calculateAllocations(connectionData, job) { partsNonTaxable: summarizeMoney(b.partsNonTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), - extras: summarizeMoney(b.extrasSale) + extras: summarizeMoney(b.extrasSale), + extrasTaxable: summarizeMoney(b.extrasTaxableSale), + extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale) })) }); debugLog("costCenterHash before jobAllocations build", { diff --git a/server/rr/rr-job-helpers.js b/server/rr/rr-job-helpers.js index d9fc523a8..1d415bb72 100644 --- a/server/rr/rr-job-helpers.js +++ b/server/rr/rr-job-helpers.js @@ -56,7 +56,7 @@ const asN2 = (dineroLike) => { * Build RO.GOG structure for the reynolds-rome-client `createRepairOrder` payload * from allocations. * - * Supports the new allocation shape: + * Supports the allocation shape: * { * center, * partsSale, @@ -65,18 +65,21 @@ const asN2 = (dineroLike) => { * laborTaxableSale, * laborNonTaxableSale, * extrasSale, + * extrasTaxableSale, + * extrasNonTaxableSale, * totalSale, * cost, * profitCenter, * costCenter * } * - * For each center, we can emit up to 5 GOG *segments*: - * - taxable parts (CustTxblNTxblFlag="T") - * - non-taxable parts (CustTxblNTxblFlag="N") - * - extras (uses profitCenter.rr_cust_txbl_flag) - * - taxable labor (CustTxblNTxblFlag="T") - * - non-tax labor (CustTxblNTxblFlag="N") + * For each center, we can emit up to 6 GOG *segments*: + * - taxable parts (CustTxblNTxblFlag="T") + * - non-taxable parts (CustTxblNTxblFlag="N") + * - taxable extras (CustTxblNTxblFlag="T") + * - non-taxable extras (CustTxblNTxblFlag="N") + * - taxable labor (CustTxblNTxblFlag="T") + * - non-taxable labor (CustTxblNTxblFlag="N") * * IMPORTANT: * Each segment becomes its OWN JobNo / AllGogOpCodeInfo, with exactly one @@ -153,7 +156,8 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo const partsTaxableCents = toCents(alloc.partsTaxableSale); const partsNonTaxableCents = toCents(alloc.partsNonTaxableSale); - const extrasCents = toCents(alloc.extrasSale); + const extrasTaxableCents = toCents(alloc.extrasTaxableSale); + const extrasNonTaxableCents = toCents(alloc.extrasNonTaxableSale); const laborTaxableCents = toCents(alloc.laborTaxableSale); const laborNonTaxableCents = toCents(alloc.laborNonTaxableSale); const costCents = toCents(alloc.cost); @@ -178,16 +182,25 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo }); } - // 3) Extras segment (respect center's default tax flag) - if (extrasCents !== 0) { + // 3) Taxable extras -> "T" + if (extrasTaxableCents !== 0) { segments.push({ - kind: "extras", - saleCents: extrasCents, - txFlag: pc.rr_cust_txbl_flag || "N" + kind: "extrasTaxable", + saleCents: extrasTaxableCents, + txFlag: "T" }); } - // 4) Taxable labor segment -> "T" + // 4) Non-taxable extras -> "N" + if (extrasNonTaxableCents !== 0) { + segments.push({ + kind: "extrasNonTaxable", + saleCents: extrasNonTaxableCents, + txFlag: "N" + }); + } + + // 5) Taxable labor segment -> "T" if (laborTaxableCents !== 0) { segments.push({ kind: "laborTaxable", @@ -196,7 +209,7 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo }); } - // 5) Non-taxable labor segment -> "N" + // 6) Non-tax labor segment -> "N" if (laborNonTaxableCents !== 0) { segments.push({ kind: "laborNonTaxable", @@ -356,10 +369,8 @@ const QueryJobData = async (ctx = {}, jobId) => { * @param advisorNo * @param story * @param makeOverride - * @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 = ({ @@ -370,7 +381,6 @@ const buildRRRepairOrderPayload = ({ makeOverride, allocations, opCode - // taxCode } = {}) => { const customerNo = selectedCustomer?.customerNo ? String(selectedCustomer.customerNo).trim() @@ -421,7 +431,6 @@ const buildRRRepairOrderPayload = ({ 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