import { Add, Divide, Multiply, Subtract } from "../shared/math-operations"; import { TrUtils } from "../utils/tr-utils"; import { TransactionCalculationEngine } from "./transaction-calculation-engine"; export function DiscountAndTaxCalculation(recordData: any, isTaxable: boolean, taxcodes: any = []) { // ---------- STEP 0: FETCH TAX CODES ---------- // const taxcodes:any = isTaxable // ? await this.laborCompSrv.GetTaxCodesByPromise() // : []; // ---------- STEP 1: INITIALIZE DEFAULTS ---------- recordData.LDisc = recordData.LDisc || 0; recordData.PDisc = recordData.PDisc || 0; recordData.Disc = recordData.Disc || 0; recordData.LPerc = recordData.LPerc || 0; // Labor-level % recordData.PPerc = recordData.PPerc || 0; // Parts-level % recordData.Perc = recordData.Perc || 0; // Global % // ---------- STEP 2: CALCULATE NET AMOUNTS ---------- // Services (labor) const servicesWithNet = (recordData.Ops || []).map((labor: any) => { const gross = labor.Amt || 0; const discount = labor.Disc || 0; const net = Subtract(gross, discount); return { ...labor, net }; }); // Inventory items (parts) const itemsWithNet = (recordData.Items || []).map((item: any) => { const qty = item.Qty || 0; const unitPrice = item.UnPr || 0; const gross = item.UnAmt || Multiply(qty, unitPrice); const discount = item.Disc || 0; const net = Subtract(gross, discount); return { ...item, net, UnAmt: gross }; }); // ---------- STEP 3: FILTER GROUPS ---------- const nonDiscountableItems = itemsWithNet.filter((val: any) => val.NoDisc); const discountableItems = itemsWithNet.filter((val: any) => !val.NoDisc); const nonDiscountableOps = servicesWithNet.filter((val: any) => val.NoDisc); const discountableOps = servicesWithNet.filter((val: any) => !val.NoDisc); const itemTotalNet = discountableItems.reduce((sum: number, item: any) => Add(sum, item.net), 0); const serviceTotalNet = discountableOps.reduce((sum: number, service: any) => Add(sum, service.net), 0); // ---------- STEP 4: HANDLE LEVEL DISCOUNTS (LPerc, PPerc) ---------- if (recordData.LPerc && Number(recordData.LPerc) > 0) { const percValue = Number(recordData.LPerc); recordData.LDisc = Multiply(serviceTotalNet, Divide(percValue, 100)); } if (recordData.PPerc && Number(recordData.PPerc) > 0) { const percValue = Number(recordData.PPerc); recordData.PDisc = Multiply(itemTotalNet, Divide(percValue, 100)); } const totalNetAfterLevelDiscounts = Subtract( Add(itemTotalNet, serviceTotalNet), Add(recordData.LDisc, recordData.PDisc) ); // ---------- STEP 5: HANDLE GLOBAL DISCOUNT (Perc OR Disc) ---------- if (recordData.Perc && Number(recordData.Perc) > 0) { const percValue = Number(recordData.Perc); recordData.Disc = Multiply(totalNetAfterLevelDiscounts, Divide(percValue, 100)); } else { recordData.Disc = recordData.Disc || 0; } // ---------- STEP 6: TAX CALCULATION HELPER ---------- // ---------- STEP 7: PROCESS LABOR ITEMS ---------- const finalServices = discountableOps.map((labor: any) => { if (labor.NoDisc) return labor; labor.Pr = TrUtils.SetValueToZeroIfNull(labor.Pr); labor.Disc = TrUtils.SetValueToZeroIfNull(labor.Disc); const laborLevelDiscShare = serviceTotalNet > 0 ? Multiply(Divide(labor.net, serviceTotalNet), recordData.LDisc) : 0; const netAfterLDisc = Subtract(labor.net, laborLevelDiscShare); let globalDiscShare = 0; if (totalNetAfterLevelDiscounts > 0) { globalDiscShare = Multiply( Divide(netAfterLDisc, totalNetAfterLevelDiscounts), recordData.Disc ); } const totalTransactionDiscount = Add(laborLevelDiscShare, globalDiscShare); labor.RecDisc = totalTransactionDiscount; const laborafterDiscPrice = Subtract(labor.Amt, labor.Disc); const afterDisc = Subtract(laborafterDiscPrice, totalTransactionDiscount); labor.Taxes = recalculateTaxes(afterDisc, labor.TCode, taxcodes, labor.Taxes, labor.Qty || 1); return labor; }); // ---------- STEP 8: PROCESS INVENTORY ITEMS ---------- const finalItems = discountableItems.map((item: any) => { if (item.NoDisc) return item; item.UnPr = TrUtils.SetValueToZeroIfNull(item.UnPr); item.Disc = TrUtils.SetValueToZeroIfNull(item.Disc); const partLevelDiscShare = itemTotalNet > 0 ? Multiply(Divide(item.net, itemTotalNet), recordData.PDisc) : 0; const netAfterPDisc = Subtract(item.net, partLevelDiscShare); let globalDiscShare = 0; if (totalNetAfterLevelDiscounts > 0) { globalDiscShare = Multiply( Divide(netAfterPDisc, totalNetAfterLevelDiscounts), recordData.Disc ); } const totalTransactionDiscount = Add(partLevelDiscShare, globalDiscShare); item.RecDisc = totalTransactionDiscount; const ItemafterDiscPrice = Subtract(item.UnAmt, item.Disc); const afterDisc = Subtract(ItemafterDiscPrice, totalTransactionDiscount); item.Taxes = recalculateTaxes(afterDisc, item.TCode, taxcodes, item.Taxes, item.Qty); return item; }); // ---------- STEP 9: FINALIZE ---------- recordData.Items = [...finalItems, ...nonDiscountableItems]; recordData.Ops = [...finalServices, ...nonDiscountableOps]; return recordData; } function recalculateTaxes(amount: any, taxCode: any, taxCodes: any[] = [], existingTaxes: any[] = [], qty: any = 1): any[] { const normalizedTaxCode = TrUtils.IsNull(taxCode) ? null : Number(taxCode); const defaultRates = getDefaultTaxRates(taxCode, taxCodes); const normalizedQty = toFiniteNumber(qty, 1); return (Array.isArray(existingTaxes) ? existingTaxes : []).map((tax: any) => { const code = tax?.Code; const rate = Number(tax?.Rate); const normalizedRate = Number.isFinite(rate) ? rate : (defaultRates[code] || 0); const calcMethod = normalizeCalcMethod(tax?.CalcMethod); return { ...tax, Rate: normalizedRate, Amt: calculateComponentTaxAmount(amount, normalizedRate, tax, normalizedQty), TaxCodeId: !TrUtils.IsNull(tax?.TaxCodeId) ? Number(tax.TaxCodeId) : normalizedTaxCode, PerUnitAmt: tax?.PerUnitAmt || undefined, CalcMethod: calcMethod, Unit: tax?.Unit ?? tax?.Units ?? undefined, }; }); } function calculateComponentTaxAmount(amount: any, rate: number, tax: any, qty: number): number { const baseAmt = toFiniteNumber(amount); const calcMethod = normalizeCalcMethod(tax?.CalcMethod); const perUnitAmt = toFiniteNumber(tax?.PerUnitAmt); const perUnitResult = Multiply(perUnitAmt, qty); const percentResult = Multiply(baseAmt, Divide(rate, 100)); switch (calcMethod) { case 'PerUnit': return perUnitResult; case 'PerUnitPlusPercent': return Add(perUnitResult, percentResult); case 'MaxOfPercentOrPerUnit': return Math.max(perUnitResult, percentResult); case 'Percent': default: return percentResult; } } function normalizeCalcMethod(calcMethod: any): string { return calcMethod === 'Percentage' || TrUtils.IsNull(calcMethod) ? 'Percent' : calcMethod; } function toFiniteNumber(value: any, fallback = 0): number { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : fallback; } function getDefaultTaxRates(taxCode: any, taxCodes: any[] = []): any { const normalizedTaxCode = TrUtils.IsNull(taxCode) ? null : taxCode; const taxCodeRow = Array.isArray(taxCodes) ? taxCodes.find((code: any) => code?._id === normalizedTaxCode) : null; const components = Array.isArray(taxCodeRow?.Components) ? taxCodeRow.Components : []; const getComponentRate = (code: string): number => { const match = components.find((component: any) => { const compCode = component?.Code ?? component?.Type ?? component?.Name; return compCode === code; }); const rate = Number(match?.Rate); return Number.isFinite(rate) ? rate : 0; }; let CGST = getComponentRate('CGST'); let SGST = getComponentRate('SGST'); let IGST = getComponentRate('IGST'); const CESS = getComponentRate('CESS'); if (CGST === 0 && SGST === 0 && IGST === 0) { const gstRateSum = TransactionCalculationEngine.getGSTRateSumByTaxCode( normalizedTaxCode, taxCodes, ); if (components.length === 1) { IGST = gstRateSum; } else if (components.length === 2) { CGST = gstRateSum / 2; SGST = gstRateSum / 2; } } return { CGST, SGST, IGST, CESS }; } /** * Calculates the record-level discount amount (RecDisc) for a single line. * * @param invoiceData Full invoice data containing Items, Ops and discount fields. * @param currentLine Normalised data for the line being edited. * @param lineType 'item' for parts/inventory lines, 'op' for service/labor lines. */ export function calculateRecordDiscountForLine( invoiceData: any, currentLine: { _id: any; noDisc: boolean; /** Item-only: quantity */ qty?: number; /** Item-only: unit price */ unpr?: number; /** Op-only: gross amount */ amt?: number; disc: number; }, lineType: 'item' | 'op', ): number { if (!!currentLine?.noDisc) { return 0; } const toNum = (v: any): number => { const n = Number(v); return Number.isFinite(n) ? n : 0; }; const rawItems: any[] = Array.isArray(invoiceData?.Items) ? invoiceData.Items : []; const rawOps: any[] = Array.isArray(invoiceData?.Ops) ? invoiceData.Ops : []; // --- Build normalised items list, patching the current line --- const items = rawItems.map((item: any) => ({ _id: item?._id, qty: toNum(item?.Qty), unpr: toNum(item?.UnPr), disc: toNum(item?.Disc), noDisc: !!item?.NoDisc, })); if (lineType === 'item') { const currentItem = { _id: currentLine._id, qty: toNum(currentLine.qty), unpr: toNum(currentLine.unpr), disc: toNum(currentLine.disc), noDisc: currentLine.noDisc, }; const idx = currentLine._id != null ? items.findIndex((i) => i._id === currentLine._id) : -1; if (idx !== -1) { items[idx] = currentItem; } else { items.push(currentItem); } } // --- Build normalised ops list, patching the current line --- const ops = rawOps.map((op: any) => ({ _id: op?._id, amt: toNum(op?.Amt ?? op?.amount ?? op?.Total ?? op?.total), disc: toNum(op?.Disc ?? op?.disc), noDisc: !!op?.NoDisc, })); if (lineType === 'op') { const currentOp = { _id: currentLine._id, amt: toNum(currentLine.amt), disc: toNum(currentLine.disc), noDisc: currentLine.noDisc, }; const idx = currentLine._id != null ? ops.findIndex((o) => o._id === currentLine._id) : -1; if (idx !== -1) { ops[idx] = currentOp; } else { ops.push(currentOp); } } // --- Compute net amounts --- const discountableItems = items.filter((i) => !i.noDisc); const itemNets = discountableItems.map((item) => { const unAmt = TrUtils.FixedTo(toNum(item.qty) * toNum(item.unpr)); const net = TrUtils.FixedTo(unAmt - toNum(item.disc)); return { ...item, net }; }); const itemTotalNet = itemNets.reduce((sum, i) => Add(sum, i.net), 0); const discountableOps = ops.filter((o) => !o.noDisc); const opNets = discountableOps.map((op) => { const net = TrUtils.FixedTo(toNum(op.amt) - toNum(op.disc)); return { ...op, net }; }); const serviceTotalNet = opNets.reduce((sum, o) => Add(sum, o.net), 0); // --- Level discount amounts --- const pperc = toNum(invoiceData?.PPerc); const lperc = toNum(invoiceData?.LPerc); const globalPerc = toNum(invoiceData?.Perc); const pDiscAmount = pperc > 0 ? Multiply(itemTotalNet, Divide(pperc, 100)) : toNum(invoiceData?.PDisc); const lDiscAmount = lperc > 0 ? Multiply(serviceTotalNet, Divide(lperc, 100)) : toNum(invoiceData?.LDisc); const totalNetAfterLevelDiscounts = Subtract( Add(itemTotalNet, serviceTotalNet), Add(pDiscAmount, lDiscAmount), ); const globalDiscAmount = globalPerc > 0 ? Multiply(totalNetAfterLevelDiscounts, Divide(globalPerc, 100)) : toNum(invoiceData?.Disc); // --- Compute share for the current line --- if (lineType === 'item') { const currentNetRow = itemNets.find((i) => i._id === currentLine._id) ?? itemNets[itemNets.length - 1]; const currentNet = toNum(currentNetRow?.net); const partLevelShare = itemTotalNet > 0 ? Multiply(Divide(currentNet, itemTotalNet), pDiscAmount) : 0; const netAfterPDisc = Subtract(currentNet, partLevelShare); const globalShare = totalNetAfterLevelDiscounts > 0 ? Multiply(Divide(netAfterPDisc, totalNetAfterLevelDiscounts), globalDiscAmount) : 0; return TrUtils.FixedTo(Math.max(0, Add(partLevelShare, globalShare))); } else { const currentNetRow = opNets.find((o) => o._id === currentLine._id) ?? opNets[opNets.length - 1]; const currentNet = toNum(currentNetRow?.net); const laborLevelShare = serviceTotalNet > 0 ? Multiply(Divide(currentNet, serviceTotalNet), lDiscAmount) : 0; const netAfterLDisc = Subtract(currentNet, laborLevelShare); const globalShare = totalNetAfterLevelDiscounts > 0 ? Multiply(Divide(netAfterLDisc, totalNetAfterLevelDiscounts), globalDiscAmount) : 0; return TrUtils.FixedTo(Math.max(0, Add(laborLevelShare, globalShare))); } }