import Big from "big.js"; export const PosReturnPolicyId = "IN_GST_POS_RETURN_1"; function paymentNet(payment: any): Big { const amount = (value: any): Big => { if ((typeof value !== "number" && typeof value !== "string") || !Number.isFinite(Number(value))) throw new Error("Payment amounts and passed fees must be finite numbers"); return new Big(value); }; const gross = amount(payment.Amt); return payment.Pass === true ? gross.minus(amount(payment.MFee ?? 0)).minus(amount(payment.MFeeTax ?? 0)) : gross; } /** Passed card fees are included in Amt, but do not settle the receipt's Total. */ export function PosPaymentNet(payment: any): number { return paymentNet(payment).toNumber(); } /** Preserve payment signs and sum in decimal arithmetic. */ export function PosSettlementTotal(payments: readonly any[] = []): number { return payments.reduce((sum: Big, payment: any) => sum.plus(paymentNet(payment)), new Big(0)).toNumber(); } /** Pure allocation shared by the cashier preview and the authoritative transaction. */ export function AllocatePosSourceReturns(source: any, requests: readonly any[], exponent = 2): Map { const decimal = (value: any): Big => { if ((typeof value !== "number" && typeof value !== "string") || !Number.isFinite(Number(value))) throw new Error("Invalid source number"); return new Big(value); }; const money = (value: any): Big => { const parsed = decimal(value); if (!parsed.eq(parsed.round(exponent))) throw new Error("Historical money is not in currency minor units"); return parsed; }; const component = (value: any): Big => { const parsed = money(value ?? 0); if (parsed.lt(0)) throw new Error("Invalid original sale component"); return parsed; }; if (source?.Settings?.Dum === true) throw new Error("Dummy receipts are not POS return sources"); if (source?.Sts !== "Closed" || !decimal(source.WrtOff ?? 0).eq(0)) throw new Error("Return source must be closed without write-off"); const total = money(source.Total); const paid = (source.Pay ?? []).reduce((sum: Big, payment: any) => sum.plus(money(PosPaymentNet(payment))), new Big(0)); if (!paid.eq(total)) throw new Error("Return source settlement does not match its total"); const lines = (source.Items ?? []).filter((line: any) => line.Ret !== true && decimal(line.Qty).gt(0)); const byId = new Map(lines.map((line: any) => [String(line._id), line])); if (byId.size !== lines.length || new Set(requests.map(line => String(line._id))).size !== requests.length) throw new Error("Duplicate receipt line identity"); const returned = new Map(); const lineNet = (line: any) => component(line.UnAmt).minus(component(line.Disc)).minus(component(line.RecDisc)); const taxEntries = (line: any): any[] => { const entries = Array.isArray(line.Taxes) ? line.Taxes : []; if (!entries.length && [line.CGST, line.SGST, line.IGST].some(amount => Number(amount ?? 0) !== 0)) throw new Error("Historical tax components need explicit reconciliation"); return entries; }; const base = lines.reduce((sum: Big, line: any) => { const net = lineNet(line); if (net.lt(0)) throw new Error("Original discounts exceed the sale amount"); if (decimal(line.UnPr).lt(0)) throw new Error("Invalid original unit price"); const quantity = decimal(line.Qty); const previous = decimal(line.RetQty ?? 0); if (previous.lt(0) || previous.gt(quantity)) throw new Error("Invalid historical returned quantity"); returned.set(String(line._id), previous); return sum.plus(net).plus(taxEntries(line).reduce((sum: Big, tax: any) => sum.plus(component(tax.Amt)), new Big(0))); }, new Big(0)); const funded = (source.Items ?? []).filter((line: any) => line.Ret === true).reduce((sum: Big, line: any) => { if (line.RAmt == null) throw new Error("Historical exchange credit is unknown"); const credit = money(line.RAmt); if (credit.gt(0)) throw new Error("Invalid historical exchange credit"); return sum.minus(credit); }, total); if (funded.lt(0) || (base.eq(0) && !funded.eq(0))) throw new Error("Unsupported source funding basis"); const prefix = (): Big => lines.reduce((sum: Big, line: any) => { const ratio = returned.get(String(line._id))!.div(decimal(line.Qty)); return sum.plus(lineNet(line).times(ratio).round(exponent)).plus(taxEntries(line).reduce((sum: Big, tax: any) => sum.plus(component(tax.Amt).times(ratio).round(exponent)), new Big(0))); }, new Big(0)); const before = prefix(); const output = new Map(); const ordered = [...requests].sort((a, b) => String(a._id).localeCompare(String(b._id))); for (const request of ordered) { const original = byId.get(String(request.ORec?.ItemId)); if (!original) throw new Error("Original sale line does not exist"); if (request.Ret !== true || !decimal(request.Qty).lt(0)) throw new Error("A return needs negative quantity and Ret=true"); if (!decimal(original.OfQty ?? 0).eq(0) || !decimal(request.OfQty ?? 0).eq(0)) throw new Error("POS free-goods returns are unsupported"); const id = String(original._id); const quantity = decimal(original.Qty); const old = returned.get(id)!; const next = old.plus(decimal(request.Qty).abs()); if (next.gt(quantity)) throw new Error("Returned quantity exceeds the remaining sale quantity"); const allocate = (amount: any): number => Number(component(amount).times(next).div(quantity).round(exponent).minus(component(amount).times(old).div(quantity).round(exponent)).neg().toString()); const net = allocate(lineNet(original).toString()); const disc = allocate(original.Disc ?? 0); const receiptDisc = allocate(original.RecDisc ?? 0); const gross = Number(new Big(net).plus(disc).plus(receiptDisc).toString()); const taxes = taxEntries(original).map((tax: any) => ({ ...tax, Amt: allocate(tax.Amt) })); const weight = new Big(net).plus(taxes.reduce((sum: Big, tax: any) => sum.plus(tax.Amt), new Big(0))).neg(); output.set(String(request._id), { Qty: Number(decimal(request.Qty).toString()), UnPr: Number(decimal(original.UnPr).toString()), UnAmt: gross, Disc: disc, RecDisc: receiptDisc, NetAmt: net, Taxes: taxes, RAmt: 0, Weight: weight.toString() }); returned.set(id, next); } const credit = base.eq(0) ? new Big(0) : funded.times(prefix()).div(base).round(exponent).minus(funded.times(before).div(base).round(exponent)); if (credit.lt(0)) throw new Error("Return credit cannot be negative"); const scale = new Big(10).pow(exponent); const creditMinor = credit.times(scale); const weightTotal = [...output.values()].reduce((sum: Big, line: any) => sum.plus(line.Weight), new Big(0)); const shares = [...output.entries()].map(([id, line]) => { const exact = weightTotal.eq(0) ? creditMinor.div(output.size || 1) : creditMinor.times(line.Weight).div(weightTotal); return { id, minor: exact.round(0, Big.roundDown), remainder: exact.minus(exact.round(0, Big.roundDown)) }; }); let remaining = creditMinor.minus(shares.reduce((sum, share) => sum.plus(share.minor), new Big(0))).toNumber(); shares.sort((a, b) => b.remainder.cmp(a.remainder) || a.id.localeCompare(b.id)); for (const share of shares) { if (remaining > 0) { share.minor = share.minor.plus(1); remaining--; } output.get(share.id)!.RAmt = Number(share.minor.div(scale).neg().toString()); delete output.get(share.id)!.Weight; } return output; } /** Sourced credits retain paise; only new goods use the receipt's normal payable rounding. */ export function CalculatePosReturnPayable(lines: readonly any[], adjust = 0, precision = 2, exponent = 2): { Total: number; Round: number; Adjust: number } { let sale = new Big(0), rawReturn = new Big(0), credit = new Big(0); for (const line of lines) { const amount = new Big(line.UnAmt ?? 0).minus(line.Disc ?? 0).minus(line.RecDisc ?? 0).plus((line.Taxes ?? []).reduce((sum: Big, tax: any) => sum.plus(tax.Amt), new Big(0))); if (line.Ret === true) { if (line.RAmt == null) throw new Error("Return credit allocation is required"); rawReturn = rawReturn.plus(amount); credit = credit.plus(line.RAmt); } else sale = sale.plus(amount); } const saleBefore = sale.plus(adjust); const roundedSale = saleBefore.round(precision, Big.roundHalfUp); return { Total: roundedSale.plus(credit).round(exponent).toNumber(), Round: roundedSale.minus(saleBefore).toNumber(), Adjust: new Big(adjust).plus(credit).minus(rawReturn).round(exponent).toNumber() }; }