import Big from "big.js"; import type { IRoundingConfig, RoundingMethod } from "./tax.types"; export interface PayableRounding { readonly Method: RoundingMethod; readonly Precision: number; } export interface DocumentCalculationPolicySnapshot { readonly PolicyId: string; readonly RegimeId: number; readonly PayableRounding: PayableRounding; readonly AppliedAt: Date | string; } export interface DocumentCalculationPolicyDefinition { readonly Id: string; readonly CurrencyCode: string; readonly MinorUnitExponent: number; readonly AllowedPayableRounding: readonly PayableRounding[]; } // Published IDs describe mathematical semantics, never the current application release. export const IndiaGstSalesPolicy: DocumentCalculationPolicyDefinition = Object.freeze({ Id: "IN_GST_SALES_1", CurrencyCode: "INR", MinorUnitExponent: 2, AllowedPayableRounding: Object.freeze([ Object.freeze({ Method: "Round" as const, Precision: 2 }), Object.freeze({ Method: "Round" as const, Precision: 0 }), ]), }); export function GetDocumentCalculationPolicy(id: string): DocumentCalculationPolicyDefinition { if (id === "IN_GST_POS_RETURN_1") return { ...IndiaGstSalesPolicy, Id: id }; if (id !== IndiaGstSalesPolicy.Id) throw new Error(`Unsupported calculation policy: ${id}`); return IndiaGstSalesPolicy; } export function ResolvePolicyPayableRounding(id: string, defaults: Partial, override?: Partial | null): PayableRounding { const policy = GetDocumentCalculationPolicy(id); const method = override?.Method ?? defaults?.Method; const precision = override?.Precision ?? defaults?.Precision; const choice = policy.AllowedPayableRounding.find(pair => pair.Method === method && pair.Precision === precision); // Both configured defaults and effective overrides must be supported. if (!policy.AllowedPayableRounding.some(pair => pair.Method === defaults?.Method && pair.Precision === defaults?.Precision)) { throw new Error(`Unsupported payable rounding default for ${id}`); } if (!choice) throw new Error(`Unsupported payable rounding override for ${id}`); return { ...choice }; } export function PolicyRoundingOf(snapshot: DocumentCalculationPolicySnapshot, currencyCode: string): IRoundingConfig { const policy = GetDocumentCalculationPolicy(snapshot.PolicyId); if (currencyCode !== policy.CurrencyCode) throw new Error(`Policy ${policy.Id} requires ${policy.CurrencyCode}`); if (!Number.isSafeInteger(snapshot.RegimeId) || snapshot.RegimeId <= 0 || !Number.isFinite(new Date(snapshot.AppliedAt).getTime())) { throw new Error("Invalid calculation policy snapshot"); } const choice = policy.AllowedPayableRounding.find(pair => pair.Method === snapshot.PayableRounding?.Method && pair.Precision === snapshot.PayableRounding?.Precision); if (!choice) throw new Error(`Unsupported payable rounding snapshot for ${policy.Id}`); // Line money and every tax component stay in paise, regardless of payable rounding. return { ...choice, LineTax: false, TaxComponentTotal: false, DocTotal: true }; } export function CalculatePolicyPayable(snapshot: DocumentCalculationPolicySnapshot, taxable: number | string, tax: number | string, adjust: number | string = 0): { Total: number; Round: number } { const rounding = PolicyRoundingOf(snapshot, GetDocumentCalculationPolicy(snapshot.PolicyId).CurrencyCode); const before = new Big(taxable).plus(tax).plus(adjust); const total = before.round(rounding.Precision, Big.roundHalfUp); return { Total: Number(total.toString()), Round: Number(total.minus(before).toString()) }; }