shareneus 1.5.91 → 1.5.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,38 +73,31 @@ function getHeadersBySequence(headers) {
73
73
  .map((entry) => entry.header);
74
74
  }
75
75
  function getRenderableColumns(headerConfig) {
76
- const hasTaxPercent = headerConfig.some((header) => header.text === 'Tax %');
77
- const hasTaxAmount = headerConfig.some((header) => header.text === 'Tax Amount');
78
- const hasTaxGroup = hasTaxPercent && hasTaxAmount;
79
76
  const columns = [];
80
77
  for (const header of headerConfig) {
81
- if (hasTaxGroup && header.text === 'Tax %') {
78
+ if (isTaxPercentHeader(header)) {
82
79
  columns.push({
83
80
  type: 'tax',
84
81
  taxKind: 'rate',
85
- taxName: '%',
86
82
  dbField: 'CGST',
87
83
  width: getTaxSubWidth(header.width),
88
84
  }, {
89
85
  type: 'tax',
90
- taxKind: 'amount',
91
- dbField: 'CGST',
92
- taxName: 'Amount',
86
+ taxKind: 'rate',
87
+ dbField: 'SGST',
93
88
  width: getTaxSubWidth(header.width),
94
89
  });
95
90
  continue;
96
91
  }
97
- if (hasTaxGroup && header.text === 'Tax Amount') {
92
+ if (isTaxAmountHeader(header)) {
98
93
  columns.push({
99
94
  type: 'tax',
100
- taxKind: 'rate',
101
- taxName: '%',
102
- dbField: 'SGST',
95
+ taxKind: 'amount',
96
+ dbField: 'CGST',
103
97
  width: getTaxSubWidth(header.width),
104
98
  }, {
105
99
  type: 'tax',
106
100
  taxKind: 'amount',
107
- taxName: 'Amount',
108
101
  dbField: 'SGST',
109
102
  width: getTaxSubWidth(header.width),
110
103
  });
@@ -119,9 +112,7 @@ function getRenderableColumns(headerConfig) {
119
112
  return columns;
120
113
  }
121
114
  function buildHeaderRows(headerConfig) {
122
- const hasTaxPercent = headerConfig.some((header) => header.text === 'Tax %');
123
- const hasTaxAmount = headerConfig.some((header) => header.text === 'Tax Amount');
124
- const hasTaxGroup = hasTaxPercent && hasTaxAmount;
115
+ const hasTaxGroup = headerConfig.some((header) => isTaxPercentHeader(header) || isTaxAmountHeader(header));
125
116
  const hasDiscPercent = headerConfig.some((header) => header.text === 'Disc %');
126
117
  const hasDiscAmount = headerConfig.some((header) => isDiscountAmountHeader(header.text));
127
118
  const hasDiscGroup = hasDiscPercent && hasDiscAmount;
@@ -133,14 +124,14 @@ function buildHeaderRows(headerConfig) {
133
124
  const topRow = [];
134
125
  const subRow = [];
135
126
  for (const header of headerConfig) {
136
- if (hasTaxGroup && header.text === 'Tax %') {
137
- topRow.push(Object.assign({ text: 'CGST', colSpan: 2, alignment: 'center' }, getHeaderCellStyle()), {});
138
- subRow.push(Object.assign({ text: '%', alignment: 'center' }, getHeaderCellStyle()), Object.assign({ text: 'Amt', alignment: 'center' }, getHeaderCellStyle()));
127
+ if (isTaxPercentHeader(header)) {
128
+ topRow.push(Object.assign({ text: header.text, colSpan: 2, alignment: 'center' }, getHeaderCellStyle()), {});
129
+ subRow.push(Object.assign({ text: 'CGST', alignment: 'center' }, getHeaderCellStyle()), Object.assign({ text: 'SGST/UGST', alignment: 'center' }, getHeaderCellStyle()));
139
130
  continue;
140
131
  }
141
- if (hasTaxGroup && header.text === 'Tax Amount') {
142
- topRow.push(Object.assign({ text: 'SGST/UGST', colSpan: 2, alignment: 'center' }, getHeaderCellStyle()), {});
143
- subRow.push(Object.assign({ text: '%', alignment: 'center' }, getHeaderCellStyle()), Object.assign({ text: 'Amt', alignment: 'center' }, getHeaderCellStyle()));
132
+ if (isTaxAmountHeader(header)) {
133
+ topRow.push(Object.assign({ text: header.text, colSpan: 2, alignment: 'center' }, getHeaderCellStyle()), {});
134
+ subRow.push(Object.assign({ text: 'CGST', alignment: 'center' }, getHeaderCellStyle()), Object.assign({ text: 'SGST/UGST', alignment: 'center' }, getHeaderCellStyle()));
144
135
  continue;
145
136
  }
146
137
  if (hasDiscGroup && header.text === 'Disc %') {
@@ -165,6 +156,18 @@ function getTaxSubWidth(width) {
165
156
  function isDiscountAmountHeader(text) {
166
157
  return text === 'Disc Amount' || text === 'Adisc Amount';
167
158
  }
159
+ function isTaxPercentHeader(header) {
160
+ return normalizeHeaderText(header === null || header === void 0 ? void 0 : header.text) === 'TAX%';
161
+ }
162
+ function isTaxAmountHeader(header) {
163
+ return normalizeHeaderText(header === null || header === void 0 ? void 0 : header.text) === 'TAXAMOUNT';
164
+ }
165
+ function normalizeHeaderText(value) {
166
+ return String(value !== null && value !== void 0 ? value : '')
167
+ .replace(/\s+/g, '')
168
+ .trim()
169
+ .toUpperCase();
170
+ }
168
171
  function getHeaderCellStyle() {
169
172
  return {
170
173
  bold: true,
@@ -4,6 +4,6 @@
4
4
  * This is the entry point for all tax-related functionality.
5
5
  * Import from "shareneus/tax" in both UI and API code.
6
6
  */
7
- export type { TaxComponentType, CalcMethod, AppliedOn, RoundingMethod, TaxCategory, SupplyType, ResolvedSupplyType, WithholdingType, ResolverType, IRegimeComponent, IRegimeFeatures, IRounding, ITreatment, ITaxRegime, ITaxCodeComponent, ITaxCode, ITaxComponent, ITaxSummaryLine, ITaxSummary, IWithholding, IWithholdingCalcInput, ITaxCalcLineInput, ITaxSummaryInput, IRoundingConfig, ITaxExemption, ITaxAuthority, IExternalProvider, } from "./tax.types";
7
+ export type { TaxComponentType, CalcMethod, AppliedOn, RoundingMethod, TaxCategory, SupplyType, ResolvedSupplyType, WithholdingType, ResolverType, IRegimeComponent, IRegimeFeatures, IRounding, ITreatment, ITaxRegime, ITaxCodeComponent, ITaxCode, ITaxComponent, ITaxSummaryLine, ITaxSummary, IWithholding, IWithholdingCalcInput, IComponentOverride, ITaxCalcLineInput, ITaxSummaryInput, IRoundingConfig, IDocumentTotalsInput, IDocumentTotals, ITaxExemption, ITaxAuthority, IExternalProvider, } from "./tax.types";
8
8
  export type { IInclusiveResult } from "./tax-calculator";
9
- export { calculateLineTax, extractNetFromInclusive, sumTaxComponents, computeTaxSummary, determineSupplyType, findTaxCodeByRateAndSupplyType, calculateWithholding, roundAmount, validateTaxId, validateHSNSACLength, convertFlatToTaxes, } from "./tax-calculator";
9
+ export { calculateLineTax, extractNetFromInclusive, sumTaxComponents, computeTaxSummary, computeDocumentTotals, determineSupplyType, findTaxCodeByRateAndSupplyType, calculateWithholding, roundAmount, validateTaxId, validateHSNSACLength, convertFlatToTaxes, } from "./tax-calculator";
package/dist/tax/index.js CHANGED
@@ -6,12 +6,13 @@
6
6
  * Import from "shareneus/tax" in both UI and API code.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.convertFlatToTaxes = exports.validateHSNSACLength = exports.validateTaxId = exports.roundAmount = exports.calculateWithholding = exports.findTaxCodeByRateAndSupplyType = exports.determineSupplyType = exports.computeTaxSummary = exports.sumTaxComponents = exports.extractNetFromInclusive = exports.calculateLineTax = void 0;
9
+ exports.convertFlatToTaxes = exports.validateHSNSACLength = exports.validateTaxId = exports.roundAmount = exports.calculateWithholding = exports.findTaxCodeByRateAndSupplyType = exports.determineSupplyType = exports.computeDocumentTotals = exports.computeTaxSummary = exports.sumTaxComponents = exports.extractNetFromInclusive = exports.calculateLineTax = void 0;
10
10
  var tax_calculator_1 = require("./tax-calculator");
11
11
  Object.defineProperty(exports, "calculateLineTax", { enumerable: true, get: function () { return tax_calculator_1.calculateLineTax; } });
12
12
  Object.defineProperty(exports, "extractNetFromInclusive", { enumerable: true, get: function () { return tax_calculator_1.extractNetFromInclusive; } });
13
13
  Object.defineProperty(exports, "sumTaxComponents", { enumerable: true, get: function () { return tax_calculator_1.sumTaxComponents; } });
14
14
  Object.defineProperty(exports, "computeTaxSummary", { enumerable: true, get: function () { return tax_calculator_1.computeTaxSummary; } });
15
+ Object.defineProperty(exports, "computeDocumentTotals", { enumerable: true, get: function () { return tax_calculator_1.computeDocumentTotals; } });
15
16
  Object.defineProperty(exports, "determineSupplyType", { enumerable: true, get: function () { return tax_calculator_1.determineSupplyType; } });
16
17
  Object.defineProperty(exports, "findTaxCodeByRateAndSupplyType", { enumerable: true, get: function () { return tax_calculator_1.findTaxCodeByRateAndSupplyType; } });
17
18
  Object.defineProperty(exports, "calculateWithholding", { enumerable: true, get: function () { return tax_calculator_1.calculateWithholding; } });
@@ -14,7 +14,7 @@
14
14
  * IMPORTANT: All monetary calculations use Big.js to avoid floating-point
15
15
  * precision issues. Never use plain JS arithmetic (* / + -) for money.
16
16
  */
17
- import { ITaxCode, ITaxComponent, ITaxSummary, ITaxCalcLineInput, ITaxSummaryInput, IRoundingConfig, ResolvedSupplyType, IWithholding, IWithholdingCalcInput } from "./tax.types";
17
+ import { ITaxCode, ITaxComponent, ITaxSummary, ITaxCalcLineInput, ITaxSummaryInput, IRoundingConfig, IDocumentTotalsInput, IDocumentTotals, ResolvedSupplyType, IWithholding, IWithholdingCalcInput } from "./tax.types";
18
18
  /**
19
19
  * Calculates tax for a single line item based on the provided TaxCode.
20
20
  *
@@ -105,25 +105,30 @@ export declare function sumTaxComponents(taxes: ITaxComponent[] | undefined | nu
105
105
  * This is a PURE FUNCTION — it does not read from database.
106
106
  * Caller must derive NetAmt from line item fields and pass it in.
107
107
  *
108
- * Groups tax amounts by component Code, then provides totals.
109
- * TaxableAmt per code = sum of line NetAmt for lines that have that code.
108
+ * Groups tax amounts by Code+Rate (e.g., CGST@9 and CGST@14 are separate buckets).
109
+ * TaxableAmt per bucket = sum of line NetAmt for lines that have that Code+Rate.
110
110
  * TotalTaxable = sum of unique line NetAmts (not double-counted across components).
111
111
  *
112
- * @param input - All line items with Taxes[] and NetAmt (taxable value), plus regime code
113
- * @returns ITaxSummary the aggregated summary (computed, not stored)
112
+ * Rounding behavior (via input.Rounding):
113
+ * - TaxComponentTotal ON: each bucket's Amt is rounded to Precision
114
+ * - Otherwise: Amts are at currency precision (from calculateLineTax)
115
+ *
116
+ * @param input - All line items with Taxes[] and NetAmt, plus regime code and optional rounding
117
+ * @returns ITaxSummary — the aggregated summary (stored on the document)
114
118
  *
115
119
  * @example
116
120
  * const summary = computeTaxSummary({
117
121
  * Lines: invoice.Items.map(item => ({
118
122
  * Taxes: item.Taxes,
119
- * NetAmt: item.NetAmt, // Caller derives this from Qty × UnitPrice - Discount
123
+ * NetAmt: item.UnAmt - item.Disc - item.RecDisc,
120
124
  * })),
121
- * RegimeCode: "IN_GST"
125
+ * RegimeCode: "IN_GST",
126
+ * Rounding: { Method: "Round", Precision: 0, TaxComponentTotal: true },
122
127
  * });
123
128
  * // Returns: {
124
129
  * // Lines: [
125
- * // { Code: "CGST", TaxableAmt: 50000, Amt: 4500, Rate: 9 },
126
- * // { Code: "SGST", TaxableAmt: 50000, Amt: 4500, Rate: 9 }
130
+ * // { Code: "CGST", Rate: 9, TaxableAmt: 50000, Amt: 4500 },
131
+ * // { Code: "SGST", Rate: 9, TaxableAmt: 50000, Amt: 4500 }
127
132
  * // ],
128
133
  * // TotalTaxable: 50000,
129
134
  * // TotalTax: 9000,
@@ -131,6 +136,50 @@ export declare function sumTaxComponents(taxes: ITaxComponent[] | undefined | nu
131
136
  * // }
132
137
  */
133
138
  export declare function computeTaxSummary(input: ITaxSummaryInput): ITaxSummary;
139
+ /**
140
+ * Computes all document-level totals in one call: SubTotal, TaxTotal,
141
+ * TaxSummary, Round, and Total.
142
+ *
143
+ * This is the primary function UI/API should call at save time to produce
144
+ * the stored financial fields. All rounding is applied per the RoundingConfig.
145
+ *
146
+ * Flow:
147
+ * 1. SubTotal = sum of line NetAmts
148
+ * 2. TaxSummary = grouped by Code+Rate (rounded per TaxComponentTotal)
149
+ * 3. TaxTotal = sum of TaxSummary Amts
150
+ * 4. GrandTotal = SubTotal + TaxTotal
151
+ * 5. If DocTotal ON: round GrandTotal, compute Round adjustment
152
+ * 6. Total = GrandTotal + Round
153
+ *
154
+ * NOTE: Document-level Discount, Adjust, and Withholding are NOT handled here.
155
+ * The caller applies those after this function returns:
156
+ * FinalPayable = Total + Adjust - Withholding.Amt
157
+ *
158
+ * @param input - Line items with NetAmt and Taxes[], plus RoundingConfig
159
+ * @returns IDocumentTotals — all stored financial fields
160
+ *
161
+ * @example
162
+ * // India GST invoice with 2 lines
163
+ * const totals = computeDocumentTotals({
164
+ * Lines: [
165
+ * { NetAmt: 5000, Taxes: [{ Code: "CGST", Rate: 9, Amt: 450, TaxCodeId: 1 },
166
+ * { Code: "SGST", Rate: 9, Amt: 450, TaxCodeId: 1 }] },
167
+ * { NetAmt: 3000, Taxes: [{ Code: "CGST", Rate: 9, Amt: 270, TaxCodeId: 1 },
168
+ * { Code: "SGST", Rate: 9, Amt: 270, TaxCodeId: 1 }] },
169
+ * ],
170
+ * Rounding: { Method: "Round", Precision: 0, TaxComponentTotal: true, DocTotal: true },
171
+ * RegimeCode: "IN_GST",
172
+ * });
173
+ * // Returns: {
174
+ * // SubTotal: 8000, TaxTotal: 1440, Round: 0, Total: 9440,
175
+ * // GrandTotal: 9440,
176
+ * // TaxSummary: [
177
+ * // { Code: "CGST", Rate: 9, TaxableAmt: 8000, Amt: 720 },
178
+ * // { Code: "SGST", Rate: 9, TaxableAmt: 8000, Amt: 720 },
179
+ * // ]
180
+ * // }
181
+ */
182
+ export declare function computeDocumentTotals(input: IDocumentTotalsInput): IDocumentTotals;
134
183
  /**
135
184
  * Determines Intra-State or Inter-State based on seller and buyer state codes.
136
185
  *
@@ -23,6 +23,7 @@ exports.calculateLineTax = calculateLineTax;
23
23
  exports.extractNetFromInclusive = extractNetFromInclusive;
24
24
  exports.sumTaxComponents = sumTaxComponents;
25
25
  exports.computeTaxSummary = computeTaxSummary;
26
+ exports.computeDocumentTotals = computeDocumentTotals;
26
27
  exports.determineSupplyType = determineSupplyType;
27
28
  exports.findTaxCodeByRateAndSupplyType = findTaxCodeByRateAndSupplyType;
28
29
  exports.roundAmount = roundAmount;
@@ -71,13 +72,21 @@ const math_operations_1 = require("../shared/math-operations");
71
72
  * // ]
72
73
  */
73
74
  function calculateLineTax(input, rounding = { Method: "Round", Precision: 2 }) {
74
- const { TaxCode, RCM } = input;
75
+ const { TaxCode, RCM, ComponentOverrides } = input;
75
76
  const netAmt = (0, util_1.GetNumber)(input.NetAmt);
76
77
  const qty = (0, util_1.GetNumber)(input.Qty, 1);
77
78
  // Exempt / Nil / NonTaxable — no components
78
79
  if (!TaxCode.Components || TaxCode.Components.length === 0) {
79
80
  return [];
80
81
  }
82
+ // Determine line-level rounding precision:
83
+ // If LineTax is active, apply the configured rounding (Method + Precision)
84
+ // Otherwise, use standard currency precision (2 decimals, half-up)
85
+ // This distinction matters when Precision is 0 (India/Japan) — we don't want
86
+ // to round each line's tax to whole rupees unless LineTax is explicitly ON.
87
+ const lineRounding = rounding.LineTax
88
+ ? { Method: rounding.Method, Precision: rounding.Precision }
89
+ : { Method: "Round", Precision: 2 };
81
90
  const result = [];
82
91
  // First pass: calculate all "Tax" type components (primary taxes)
83
92
  // These are needed because some components (Cess with AppliedOn: "PostTax")
@@ -85,30 +94,55 @@ function calculateLineTax(input, rounding = { Method: "Round", Precision: 2 }) {
85
94
  let primaryTaxTotal = 0;
86
95
  for (const comp of TaxCode.Components) {
87
96
  if (comp.AppliedOn !== "PostTax") {
88
- const calculated = calculateSingleComponent(comp, netAmt, qty, rounding);
97
+ // Apply ComponentOverrides if provided (e.g., CESS Rate/PerUnitAmt from item master)
98
+ const effectiveComp = applyComponentOverride(comp, ComponentOverrides);
99
+ const calculated = calculateSingleComponent(effectiveComp, netAmt, qty, lineRounding);
89
100
  if (comp.Type === "Tax") {
90
101
  primaryTaxTotal = (0, math_operations_1.Add)(primaryTaxTotal, calculated.Amt);
91
102
  }
92
- result.push(buildTaxComponent(comp, calculated.Amt, TaxCode._id));
103
+ result.push(buildTaxComponent(effectiveComp, calculated.Amt, TaxCode._id));
93
104
  }
94
105
  }
95
106
  // Second pass: calculate components that apply on PostTax (tax-on-tax)
96
107
  for (const comp of TaxCode.Components) {
97
108
  if (comp.AppliedOn === "PostTax") {
109
+ const effectiveComp = applyComponentOverride(comp, ComponentOverrides);
98
110
  const postTaxBase = (0, math_operations_1.Add)(netAmt, primaryTaxTotal);
99
- const calculated = calculateSingleComponent(comp, postTaxBase, qty, rounding);
100
- result.push(buildTaxComponent(comp, calculated.Amt, TaxCode._id));
111
+ const calculated = calculateSingleComponent(effectiveComp, postTaxBase, qty, lineRounding);
112
+ result.push(buildTaxComponent(effectiveComp, calculated.Amt, TaxCode._id));
101
113
  }
102
114
  }
103
115
  return result;
104
116
  }
117
+ /**
118
+ * Merges ComponentOverrides into a TaxCode component.
119
+ *
120
+ * Only Rate and PerUnitAmt can be overridden — structural fields (Code, Name,
121
+ * Type, CalcMethod, AppliedOn) always come from the TaxCode definition.
122
+ *
123
+ * Primary use case: CESS components where the TaxCode has placeholder values
124
+ * (Rate: 0, PerUnitAmt: 0) and the actual values come from the item master.
125
+ */
126
+ function applyComponentOverride(comp, overrides) {
127
+ var _a, _b, _c;
128
+ if (!overrides)
129
+ return comp;
130
+ const override = overrides[comp.Code];
131
+ if (!override)
132
+ return comp;
133
+ return Object.assign(Object.assign({}, comp), { Rate: (_a = override.Rate) !== null && _a !== void 0 ? _a : comp.Rate, PerUnitAmt: (_b = override.PerUnitAmt) !== null && _b !== void 0 ? _b : comp.PerUnitAmt, Unit: (_c = override.Unit) !== null && _c !== void 0 ? _c : comp.Unit });
134
+ }
105
135
  /**
106
136
  * Calculates tax amount for a single component.
107
137
  *
108
- * Handles three calculation methods:
138
+ * Handles four calculation methods:
109
139
  * - Percent: (netAmt * rate) / 100
110
140
  * - PerUnit: perUnitAmt * qty
111
141
  * - PerUnitPlusPercent: (perUnitAmt * qty) + (netAmt * rate / 100)
142
+ * - MaxOfPercentOrPerUnit: max(netAmt * rate / 100, perUnitAmt * qty)
143
+ *
144
+ * MaxOfPercentOrPerUnit is used for India CESS on tobacco categories where
145
+ * the cess is "X% or ₹Y per unit, whichever is higher" (CBIC notification).
112
146
  */
113
147
  function calculateSingleComponent(comp, baseAmt, qty, rounding) {
114
148
  const method = comp.CalcMethod || "Percent";
@@ -139,6 +173,14 @@ function calculateSingleComponent(comp, baseAmt, qty, rounding) {
139
173
  amt = (0, math_operations_1.Add)(fixedPart, percentPart);
140
174
  break;
141
175
  }
176
+ case "MaxOfPercentOrPerUnit": {
177
+ const perUnitAmt = (0, util_1.GetNumber)(comp.PerUnitAmt);
178
+ const rate = (0, util_1.GetNumber)(comp.Rate);
179
+ const perUnitResult = (0, math_operations_1.Multiply)(perUnitAmt, qty);
180
+ const percentResult = (0, math_operations_1.Multiply)(baseAmt, (0, math_operations_1.Divide)(rate, 100));
181
+ amt = Math.max(perUnitResult, percentResult);
182
+ break;
183
+ }
142
184
  }
143
185
  // Apply rounding
144
186
  amt = roundAmount(amt, rounding);
@@ -146,18 +188,39 @@ function calculateSingleComponent(comp, baseAmt, qty, rounding) {
146
188
  }
147
189
  /**
148
190
  * Builds a lean ITaxComponent snapshot from a component definition and calculated amount.
149
- * Only stores Code, Rate, Amt, TaxCodeId (4 fields).
191
+ *
192
+ * Core fields (always stored): Code, Rate, Amt, TaxCodeId
193
+ * Snapshot fields (only for non-percent): CalcMethod, PerUnitAmt
194
+ *
195
+ * For standard percent-based components (CGST, SGST, IGST), CalcMethod and PerUnitAmt
196
+ * are omitted to keep the snapshot lean. They're only included when the calculation
197
+ * method is PerUnit, PerUnitPlusPercent, or MaxOfPercentOrPerUnit — because these
198
+ * values may come from ComponentOverrides (item CessConfig) and must be preserved
199
+ * for invoice reprinting, credit notes, and audit.
150
200
  *
151
201
  * TaxableAmt is NOT stored — it's derivable from line item fields (Qty × UnitPrice - Discount).
152
202
  * TaxAmt (sum of Taxes[].Amt) is NOT stored — it's a simple addition, computed on the fly.
153
203
  */
154
204
  function buildTaxComponent(comp, amt, taxCodeId) {
155
- return {
205
+ const result = {
156
206
  Code: comp.Code,
157
207
  Rate: comp.Rate,
158
208
  Amt: amt,
159
209
  TaxCodeId: taxCodeId,
160
210
  };
211
+ // Snapshot CalcMethod, PerUnitAmt, and Unit for non-percent components
212
+ // so the invoice is self-contained even if item cess config changes later
213
+ const method = comp.CalcMethod || "Percent";
214
+ if (method !== "Percent") {
215
+ result.CalcMethod = method;
216
+ if (comp.PerUnitAmt != null && comp.PerUnitAmt !== 0) {
217
+ result.PerUnitAmt = comp.PerUnitAmt;
218
+ }
219
+ if (comp.Unit) {
220
+ result.Unit = comp.Unit;
221
+ }
222
+ }
223
+ return result;
161
224
  }
162
225
  function extractNetFromInclusive(inclusivePrice, taxCode, qty = 1, rounding = { Method: "Round", Precision: 2 }) {
163
226
  if (!taxCode.Components || taxCode.Components.length === 0) {
@@ -170,6 +233,10 @@ function extractNetFromInclusive(inclusivePrice, taxCode, qty = 1, rounding = {
170
233
  warning = "Inclusive back-calc is approximate: PerUnitPlusPercent component present";
171
234
  break;
172
235
  }
236
+ if (comp.CalcMethod === "MaxOfPercentOrPerUnit") {
237
+ warning = "Inclusive back-calc is approximate: MaxOfPercentOrPerUnit component present";
238
+ break;
239
+ }
173
240
  if (comp.AppliedOn === "PostTax") {
174
241
  warning = "Inclusive back-calc is approximate: PostTax (tax-on-tax) component present";
175
242
  break;
@@ -233,25 +300,30 @@ function sumTaxComponents(taxes) {
233
300
  * This is a PURE FUNCTION — it does not read from database.
234
301
  * Caller must derive NetAmt from line item fields and pass it in.
235
302
  *
236
- * Groups tax amounts by component Code, then provides totals.
237
- * TaxableAmt per code = sum of line NetAmt for lines that have that code.
303
+ * Groups tax amounts by Code+Rate (e.g., CGST@9 and CGST@14 are separate buckets).
304
+ * TaxableAmt per bucket = sum of line NetAmt for lines that have that Code+Rate.
238
305
  * TotalTaxable = sum of unique line NetAmts (not double-counted across components).
239
306
  *
240
- * @param input - All line items with Taxes[] and NetAmt (taxable value), plus regime code
241
- * @returns ITaxSummary the aggregated summary (computed, not stored)
307
+ * Rounding behavior (via input.Rounding):
308
+ * - TaxComponentTotal ON: each bucket's Amt is rounded to Precision
309
+ * - Otherwise: Amts are at currency precision (from calculateLineTax)
310
+ *
311
+ * @param input - All line items with Taxes[] and NetAmt, plus regime code and optional rounding
312
+ * @returns ITaxSummary — the aggregated summary (stored on the document)
242
313
  *
243
314
  * @example
244
315
  * const summary = computeTaxSummary({
245
316
  * Lines: invoice.Items.map(item => ({
246
317
  * Taxes: item.Taxes,
247
- * NetAmt: item.NetAmt, // Caller derives this from Qty × UnitPrice - Discount
318
+ * NetAmt: item.UnAmt - item.Disc - item.RecDisc,
248
319
  * })),
249
- * RegimeCode: "IN_GST"
320
+ * RegimeCode: "IN_GST",
321
+ * Rounding: { Method: "Round", Precision: 0, TaxComponentTotal: true },
250
322
  * });
251
323
  * // Returns: {
252
324
  * // Lines: [
253
- * // { Code: "CGST", TaxableAmt: 50000, Amt: 4500, Rate: 9 },
254
- * // { Code: "SGST", TaxableAmt: 50000, Amt: 4500, Rate: 9 }
325
+ * // { Code: "CGST", Rate: 9, TaxableAmt: 50000, Amt: 4500 },
326
+ * // { Code: "SGST", Rate: 9, TaxableAmt: 50000, Amt: 4500 }
255
327
  * // ],
256
328
  * // TotalTaxable: 50000,
257
329
  * // TotalTax: 9000,
@@ -259,50 +331,54 @@ function sumTaxComponents(taxes) {
259
331
  * // }
260
332
  */
261
333
  function computeTaxSummary(input) {
262
- const { Lines, RegimeCode } = input;
263
- // Group by component Code
334
+ const { Lines, RegimeCode, Rounding } = input;
335
+ // Group by Code+Rate (more granular than Code-only for rate-wise reporting)
264
336
  const groupMap = new Map();
265
337
  let totalTaxable = 0;
266
- let totalTax = 0;
267
338
  for (const line of Lines) {
268
339
  if (!line.Taxes || line.Taxes.length === 0)
269
340
  continue;
270
341
  const lineNetAmt = (0, util_1.GetNumber)(line.NetAmt);
271
342
  // Track this line's taxable amount at document level (once per line, not per component)
272
343
  totalTaxable = (0, math_operations_1.Add)(totalTaxable, lineNetAmt);
273
- // Track which codes we've already added this line's NetAmt to (avoid double-counting)
274
- const codesTrackedForLine = new Set();
344
+ // Track which Code+Rate buckets we've already added this line's NetAmt to
345
+ const bucketsTrackedForLine = new Set();
275
346
  for (const tax of line.Taxes) {
276
- const key = tax.Code;
347
+ const rate = (0, util_1.GetNumber)(tax.Rate);
348
+ const key = `${tax.Code}|${rate}`;
277
349
  if (!groupMap.has(key)) {
278
350
  groupMap.set(key, {
279
351
  Code: tax.Code,
352
+ Rate: rate,
280
353
  TaxableAmt: 0,
281
354
  Amt: 0,
282
- Rates: new Set(),
283
355
  });
284
356
  }
285
357
  const group = groupMap.get(key);
286
- // Add line's NetAmt to this code's TaxableAmt (once per line per code)
287
- if (!codesTrackedForLine.has(key)) {
358
+ // Add line's NetAmt to this bucket's TaxableAmt (once per line per bucket)
359
+ if (!bucketsTrackedForLine.has(key)) {
288
360
  group.TaxableAmt = (0, math_operations_1.Add)(group.TaxableAmt, lineNetAmt);
289
- codesTrackedForLine.add(key);
361
+ bucketsTrackedForLine.add(key);
290
362
  }
291
363
  group.Amt = (0, math_operations_1.Add)(group.Amt, (0, util_1.GetNumber)(tax.Amt));
292
- group.Rates.add((0, util_1.GetNumber)(tax.Rate));
293
- totalTax = (0, math_operations_1.Add)(totalTax, (0, util_1.GetNumber)(tax.Amt));
294
364
  }
295
365
  }
296
- // Build summary lines
366
+ // Build summary lines, applying TaxComponentTotal rounding if configured
297
367
  const summaryLines = [];
368
+ let totalTax = 0;
298
369
  for (const [, group] of groupMap) {
370
+ let amt = group.Amt;
371
+ // If TaxComponentTotal rounding is active, round each bucket's Amt
372
+ if (Rounding === null || Rounding === void 0 ? void 0 : Rounding.TaxComponentTotal) {
373
+ amt = roundAmount(amt, { Method: Rounding.Method, Precision: Rounding.Precision });
374
+ }
299
375
  summaryLines.push({
300
376
  Code: group.Code,
377
+ Rate: group.Rate,
301
378
  TaxableAmt: group.TaxableAmt,
302
- Amt: group.Amt,
303
- // Rate is only set if uniform across all lines, otherwise undefined
304
- Rate: group.Rates.size === 1 ? Array.from(group.Rates)[0] : undefined,
379
+ Amt: amt,
305
380
  });
381
+ totalTax = (0, math_operations_1.Add)(totalTax, amt);
306
382
  }
307
383
  return {
308
384
  Lines: summaryLines,
@@ -312,6 +388,85 @@ function computeTaxSummary(input) {
312
388
  };
313
389
  }
314
390
  // ============================================================
391
+ // Document Totals — Full document computation
392
+ // ============================================================
393
+ /**
394
+ * Computes all document-level totals in one call: SubTotal, TaxTotal,
395
+ * TaxSummary, Round, and Total.
396
+ *
397
+ * This is the primary function UI/API should call at save time to produce
398
+ * the stored financial fields. All rounding is applied per the RoundingConfig.
399
+ *
400
+ * Flow:
401
+ * 1. SubTotal = sum of line NetAmts
402
+ * 2. TaxSummary = grouped by Code+Rate (rounded per TaxComponentTotal)
403
+ * 3. TaxTotal = sum of TaxSummary Amts
404
+ * 4. GrandTotal = SubTotal + TaxTotal
405
+ * 5. If DocTotal ON: round GrandTotal, compute Round adjustment
406
+ * 6. Total = GrandTotal + Round
407
+ *
408
+ * NOTE: Document-level Discount, Adjust, and Withholding are NOT handled here.
409
+ * The caller applies those after this function returns:
410
+ * FinalPayable = Total + Adjust - Withholding.Amt
411
+ *
412
+ * @param input - Line items with NetAmt and Taxes[], plus RoundingConfig
413
+ * @returns IDocumentTotals — all stored financial fields
414
+ *
415
+ * @example
416
+ * // India GST invoice with 2 lines
417
+ * const totals = computeDocumentTotals({
418
+ * Lines: [
419
+ * { NetAmt: 5000, Taxes: [{ Code: "CGST", Rate: 9, Amt: 450, TaxCodeId: 1 },
420
+ * { Code: "SGST", Rate: 9, Amt: 450, TaxCodeId: 1 }] },
421
+ * { NetAmt: 3000, Taxes: [{ Code: "CGST", Rate: 9, Amt: 270, TaxCodeId: 1 },
422
+ * { Code: "SGST", Rate: 9, Amt: 270, TaxCodeId: 1 }] },
423
+ * ],
424
+ * Rounding: { Method: "Round", Precision: 0, TaxComponentTotal: true, DocTotal: true },
425
+ * RegimeCode: "IN_GST",
426
+ * });
427
+ * // Returns: {
428
+ * // SubTotal: 8000, TaxTotal: 1440, Round: 0, Total: 9440,
429
+ * // GrandTotal: 9440,
430
+ * // TaxSummary: [
431
+ * // { Code: "CGST", Rate: 9, TaxableAmt: 8000, Amt: 720 },
432
+ * // { Code: "SGST", Rate: 9, TaxableAmt: 8000, Amt: 720 },
433
+ * // ]
434
+ * // }
435
+ */
436
+ function computeDocumentTotals(input) {
437
+ const { Lines, Rounding, RegimeCode } = input;
438
+ // 1. SubTotal = sum of line NetAmts
439
+ let subTotal = 0;
440
+ for (const line of Lines) {
441
+ subTotal = (0, math_operations_1.Add)(subTotal, (0, util_1.GetNumber)(line.NetAmt));
442
+ }
443
+ // 2. TaxSummary (handles TaxComponentTotal rounding internally)
444
+ const taxSummary = computeTaxSummary({
445
+ Lines,
446
+ RegimeCode,
447
+ Rounding,
448
+ });
449
+ // 3. TaxTotal from the (potentially rounded) summary
450
+ const taxTotal = taxSummary.TotalTax;
451
+ // 4. GrandTotal before doc-level rounding
452
+ const grandTotal = (0, math_operations_1.Add)(subTotal, taxTotal);
453
+ // 5. DocTotal rounding
454
+ let roundedTotal = grandTotal;
455
+ let roundAdj = 0;
456
+ if (Rounding.DocTotal !== false) { // default true
457
+ roundedTotal = roundAmount(grandTotal, { Method: Rounding.Method, Precision: Rounding.Precision });
458
+ roundAdj = (0, math_operations_1.Subtract)(roundedTotal, grandTotal);
459
+ }
460
+ return {
461
+ SubTotal: subTotal,
462
+ TaxTotal: taxTotal,
463
+ TaxSummary: taxSummary.Lines,
464
+ GrandTotal: grandTotal,
465
+ Round: roundAdj,
466
+ Total: roundedTotal,
467
+ };
468
+ }
469
+ // ============================================================
315
470
  // Supply Type Detection
316
471
  // ============================================================
317
472
  /**
@@ -10,7 +10,7 @@
10
10
  /** Tax component types — what kind of tax is this? */
11
11
  export type TaxComponentType = "Tax" | "Cess" | "Levy" | "Excise" | "Surcharge" | "Duty";
12
12
  /** How the tax is calculated */
13
- export type CalcMethod = "Percent" | "PerUnit" | "PerUnitPlusPercent";
13
+ export type CalcMethod = "Percent" | "PerUnit" | "PerUnitPlusPercent" | "MaxOfPercentOrPerUnit";
14
14
  /** What base amount the tax applies to */
15
15
  export type AppliedOn = "NetAmt" | "PostTax" | "Qty";
16
16
  /** Rounding methods */
@@ -43,11 +43,23 @@ export interface IRegimeFeatures {
43
43
  TaxIdLabel?: string;
44
44
  TaxIdFormat?: string;
45
45
  }
46
- /** Rounding rules for a regime */
46
+ /** Rounding rules for a regime.
47
+ * Three rounding points control WHERE in the calculation chain rounding is applied.
48
+ * LineTax and TaxComponentTotal are mutually exclusive in practice (line-level OR document-level tax rounding).
49
+ * DocTotal controls whether the grand total is rounded independently.
50
+ *
51
+ * Country presets:
52
+ * India GST: { Method: "Round", Precision: 0, TaxComponentTotal: true, DocTotal: true }
53
+ * US Sales Tax: { Method: "Round", Precision: 2, LineTax: true, DocTotal: true }
54
+ * Japan CT: { Method: "Floor", Precision: 0, TaxComponentTotal: true }
55
+ * EU/UK/AU: { Method: "Round", Precision: 2, DocTotal: true }
56
+ */
47
57
  export interface IRounding {
48
58
  Method: RoundingMethod;
49
59
  Precision: number;
50
- RoundAtLine: boolean;
60
+ LineTax: boolean;
61
+ TaxComponentTotal: boolean;
62
+ DocTotal: boolean;
51
63
  }
52
64
  /** A registration/treatment type valid for this regime */
53
65
  export interface ITreatment {
@@ -80,6 +92,7 @@ export interface ITaxCodeComponent {
80
92
  Type: TaxComponentType;
81
93
  CalcMethod: CalcMethod;
82
94
  PerUnitAmt?: number;
95
+ Unit?: string;
83
96
  AppliedOn: AppliedOn;
84
97
  }
85
98
  /** The full TaxCode object */
@@ -119,8 +132,10 @@ export interface ITaxCode {
119
132
  * so NOT stored here — line item is the source of truth.
120
133
  * - TaxAmt (sum of Taxes[].Amt) is a simple addition → computed on the fly via sumTaxComponents().
121
134
  *
122
- * Full component details (Name, Type, CalcMethod, PerUnitAmt) can be looked up from
123
- * TaxCode via TaxCodeId when needed.
135
+ * CalcMethod and PerUnitAmt are optional snapshot fields for non-percent cess.
136
+ * They capture the resolved calculation parameters at save time so the invoice
137
+ * remains self-contained and reproducible even if item cess config changes later.
138
+ * For standard percent-based components (CGST, SGST, IGST), these are omitted.
124
139
  *
125
140
  * NOTE: ITC is NOT stored here. ITC is at the line-item and document
126
141
  * level (purchase-side only).
@@ -130,22 +145,29 @@ export interface ITaxComponent {
130
145
  Rate: number;
131
146
  Amt: number;
132
147
  TaxCodeId?: number;
148
+ CalcMethod?: CalcMethod;
149
+ PerUnitAmt?: number;
150
+ Unit?: string;
133
151
  }
134
152
  /**
135
- * TaxSummary is a cached aggregation of line-item Taxes[].
153
+ * TaxSummary is computed by shareneus at save time and stored on the document.
154
+ * Line-item Taxes[] remains the source of truth. TaxSummary is a materialized
155
+ * snapshot for reporting efficiency — avoids complex $unwind/$group in aggregation.
136
156
  *
137
- * NOT required for v1. Line-item Taxes[] is the source of truth.
138
- * Add TaxSummary later as a performance optimization if GST reports
139
- * become slow when aggregating from line items.
157
+ * Grouped by Code+Rate (e.g., CGST@9, CGST@14 are separate entries).
158
+ * Rounding is applied per the RoundingConfig:
159
+ * - If TaxComponentTotal ON: each entry's Amt is rounded to Precision
160
+ * - If LineTax ON: Amts are already rounded at line level, summary just sums them
161
+ * - Otherwise: Amts are at currency precision (2 decimals)
140
162
  */
141
- /** One line in the TaxSummary (computed on the fly, not stored) */
163
+ /** One line in the TaxSummary grouped by Code+Rate */
142
164
  export interface ITaxSummaryLine {
143
165
  Code: string;
166
+ Rate: number;
144
167
  TaxableAmt: number;
145
168
  Amt: number;
146
- Rate?: number;
147
169
  }
148
- /** Document-level tax summary (computed on the fly, not stored) */
170
+ /** Document-level tax summary stored on Invoice, Bill, CreditNote, etc. */
149
171
  export interface ITaxSummary {
150
172
  Lines: ITaxSummaryLine[];
151
173
  TotalTaxable: number;
@@ -193,6 +215,24 @@ export interface IWithholdingCalcInput {
193
215
  BaseAmt: number;
194
216
  PartyLiable?: "Buyer" | "Seller";
195
217
  }
218
+ /**
219
+ * Component-level overrides for tax calculation.
220
+ *
221
+ * Keyed by component Code (e.g., "CESS"). When provided, these values
222
+ * replace the TaxCode component's defaults at calculation time.
223
+ *
224
+ * Primary use case: CESS rates are HSN-specific, so the TaxCode defines
225
+ * the structure (CalcMethod, AppliedOn) while the actual Rate/PerUnitAmt
226
+ * comes from the item master's CessConfig.
227
+ *
228
+ * Only Rate and PerUnitAmt can be overridden — structural fields (Code,
229
+ * Name, Type, CalcMethod, AppliedOn) always come from the TaxCode.
230
+ */
231
+ export interface IComponentOverride {
232
+ Rate?: number;
233
+ PerUnitAmt?: number;
234
+ Unit?: string;
235
+ }
196
236
  /** Input context for calculating tax on a single line item */
197
237
  export interface ITaxCalcLineInput {
198
238
  /** Net amount after discounts (taxable value) */
@@ -203,6 +243,21 @@ export interface ITaxCalcLineInput {
203
243
  TaxCode: ITaxCode;
204
244
  /** Is this a reverse charge line? */
205
245
  RCM?: boolean;
246
+ /**
247
+ * Optional component-level overrides, keyed by component Code.
248
+ *
249
+ * Used when CESS Rate/PerUnitAmt comes from item master (CessConfig)
250
+ * rather than the TaxCode itself (which has placeholder values like Rate: 0).
251
+ *
252
+ * @example
253
+ * // Item has CessRate: 12 (percent cess from HSN config)
254
+ * ComponentOverrides: { CESS: { Rate: 12 } }
255
+ *
256
+ * @example
257
+ * // Item has CessPerUnitAmt: 5 (₹5/liter from HSN config)
258
+ * ComponentOverrides: { CESS: { PerUnitAmt: 5 } }
259
+ */
260
+ ComponentOverrides?: Record<string, IComponentOverride>;
206
261
  }
207
262
  /** Input context for computing document-level TaxSummary.
208
263
  *
@@ -218,11 +273,58 @@ export interface ITaxSummaryInput {
218
273
  }>;
219
274
  /** Tax regime code for the snapshot */
220
275
  RegimeCode?: string;
276
+ /** Rounding config — if provided, applies TaxComponentTotal rounding to summary Amts */
277
+ Rounding?: IRoundingConfig;
221
278
  }
222
- /** Rounding settings passed to the calculator */
279
+ /**
280
+ * Rounding settings passed to calculator functions.
281
+ *
282
+ * Method + Precision define HOW to round.
283
+ * LineTax / TaxComponentTotal / DocTotal define WHERE (which rounding points are active).
284
+ *
285
+ * When passed to calculateLineTax:
286
+ * - LineTax ON → round each Taxes[].Amt to Precision
287
+ * - LineTax OFF → round to standard currency precision (2 decimals)
288
+ *
289
+ * When passed to computeTaxSummary:
290
+ * - TaxComponentTotal ON → round each Code+Rate bucket's Amt to Precision
291
+ *
292
+ * When passed to computeDocumentTotals:
293
+ * - DocTotal ON → round the grand total to Precision
294
+ */
223
295
  export interface IRoundingConfig {
224
296
  Method: RoundingMethod;
225
297
  Precision: number;
298
+ LineTax?: boolean;
299
+ TaxComponentTotal?: boolean;
300
+ DocTotal?: boolean;
301
+ }
302
+ /** Input for computing all document-level totals in one call */
303
+ export interface IDocumentTotalsInput {
304
+ /** All line items — caller derives NetAmt from line item fields (UnAmt - Disc - RecDisc) */
305
+ Lines: Array<{
306
+ NetAmt: number;
307
+ Taxes?: ITaxComponent[];
308
+ }>;
309
+ /** Rounding configuration (from TaxRegime.Rounding or Entity Settings) */
310
+ Rounding: IRoundingConfig;
311
+ /** Tax regime code for the TaxSummary snapshot */
312
+ RegimeCode?: string;
313
+ }
314
+ /** Result of full document totals computation */
315
+ export interface IDocumentTotals {
316
+ /** Sum of all line NetAmts (before tax) */
317
+ SubTotal: number;
318
+ /** Total tax amount — sum of rounded component Amts per RoundingConfig */
319
+ TaxTotal: number;
320
+ /** Rate-wise tax breakdown — grouped by Code+Rate, rounded per config */
321
+ TaxSummary: ITaxSummaryLine[];
322
+ /** SubTotal + TaxTotal (before DocTotal rounding) */
323
+ GrandTotal: number;
324
+ /** Rounding adjustment: RoundedTotal - GrandTotal (0 if DocTotal is off) */
325
+ Round: number;
326
+ /** Final amount: GrandTotal + Round */
327
+ Total: number;
226
328
  }
227
329
  /**
228
330
  * Tax exemption reasons — used when a line item or contact is exempt from tax.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shareneus",
3
- "version": "1.5.91",
3
+ "version": "1.5.93",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",