Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,58 @@ getStats();

---

## GST calculation utilities

Pure, zero-dependency helpers for GST tax math. These are **rate-agnostic** — you pass the GST rate in (automatic rate lookup from HSN codes is tracked in a separate enhancement).

### `calculateTax(taxableAmount, rate)`
```js
calculateTax(10000, 18);
// { taxableAmount: 10000, rate: 18, taxAmount: 1800, total: 11800 }
```

### `calculateGSTBreakdown(taxableAmount, gstRate, options?)`
Splits GST into CGST/SGST (intra-state) or IGST (inter-state), with optional cess.
```js
calculateGSTBreakdown(10000, 18);
// { taxableAmount: 10000, cgst: 900, sgst: 900, igst: 0, cess: 0, totalTax: 1800, grandTotal: 11800 }

calculateGSTBreakdown(10000, 28, { isInterState: true, cessRate: 25 });
// { ..., igst: 2800, cess: 2500, totalTax: 5300, grandTotal: 15300 }
```

### `reverseCalculateTax(grandTotal, rate)`
Extracts base + tax from a tax-inclusive amount.
```js
reverseCalculateTax(11800, 18);
// { grandTotal: 11800, rate: 18, taxableAmount: 10000, taxAmount: 1800 }
```

### `getApplicableTaxType(supplierStateCode, placeOfSupplyStateCode)`
```js
getApplicableTaxType('33', '33'); // 'CGST_SGST'
getApplicableTaxType('33', '29'); // 'IGST'
```

### `calculateInvoiceTotals(items, isInterState)`
Sums line items into invoice totals with GST round-off.
```js
calculateInvoiceTotals([
{ taxableValue: 10000, gstRate: 18 },
{ taxableValue: 5000, gstRate: 12 }
], false);
// { totalTaxableValue: 15000, totalCGST: 1200, totalSGST: 1200, totalIGST: 0,
// totalCess: 0, totalTax: 2400, grandTotal: 17400, roundOff: 0 }
```

### `groupItemsByTaxRate(items)`
Groups line items by GST rate (useful for GSTR-1 summaries).

### `applyRoundOffRules(amount)`
Rounds a monetary amount to 2 decimals (half-up).

---

## TypeScript

Type definitions are bundled. No `@types/` package needed.
Expand Down
196 changes: 196 additions & 0 deletions gst.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
'use strict';

/**
* GST calculation utilities — pure, rate-agnostic helpers.
*
* These functions do NOT look up tax rates from HSN codes (that depends on
* rate data being added to the dataset — see issue #2). You pass the rate in.
*/

function _assertNumber(value, name) {
if (typeof value !== 'number' || Number.isNaN(value)) {
throw new TypeError(`${name} must be a number, got ${value}`);
}
if (value < 0) {
throw new RangeError(`${name} must not be negative, got ${value}`);
}
}

/**
* Rounds a monetary amount to 2 decimal places using standard half-up rounding.
* GST invoices are expressed to 2 decimals.
* @param {number} amount
* @returns {number}
*/
function applyRoundOffRules(amount) {
_assertNumber(amount, 'amount');
return Math.round((amount + Number.EPSILON) * 100) / 100;
}

/**
* Calculates tax on a taxable amount at a given rate.
* @param {number} taxableAmount
* @param {number} rate - percentage, e.g. 18 for 18%
* @returns {{ taxableAmount: number, rate: number, taxAmount: number, total: number }}
*/
function calculateTax(taxableAmount, rate) {
_assertNumber(taxableAmount, 'taxableAmount');
_assertNumber(rate, 'rate');
const taxAmount = applyRoundOffRules((taxableAmount * rate) / 100);
return {
taxableAmount: applyRoundOffRules(taxableAmount),
rate,
taxAmount,
total: applyRoundOffRules(taxableAmount + taxAmount)
};
}

/**
* Splits GST into CGST/SGST (intra-state) or IGST (inter-state) plus optional cess.
* @param {number} taxableAmount
* @param {number} gstRate - total GST percentage (e.g. 18)
* @param {{ isInterState?: boolean, cessRate?: number }} [options]
* @returns {{ taxableAmount: number, cgst: number, sgst: number, igst: number, cess: number, totalTax: number, grandTotal: number }}
*/
function calculateGSTBreakdown(taxableAmount, gstRate, options) {
_assertNumber(taxableAmount, 'taxableAmount');
_assertNumber(gstRate, 'gstRate');

const { isInterState = false, cessRate = 0 } = options || {};
_assertNumber(cessRate, 'cessRate');

let cgst = 0;
let sgst = 0;
let igst = 0;

if (isInterState) {
igst = applyRoundOffRules((taxableAmount * gstRate) / 100);
} else {
cgst = applyRoundOffRules((taxableAmount * (gstRate / 2)) / 100);
sgst = applyRoundOffRules((taxableAmount * (gstRate / 2)) / 100);
}

const cess = applyRoundOffRules((taxableAmount * cessRate) / 100);
const totalTax = applyRoundOffRules(cgst + sgst + igst + cess);
const grandTotal = applyRoundOffRules(taxableAmount + totalTax);

return {
taxableAmount: applyRoundOffRules(taxableAmount),
cgst,
sgst,
igst,
cess,
totalTax,
grandTotal
};
}

/**
* Extracts the base amount and tax from a tax-inclusive total.
* @param {number} grandTotal - amount that already includes tax
* @param {number} rate - percentage, e.g. 18
* @returns {{ grandTotal: number, rate: number, taxableAmount: number, taxAmount: number }}
*/
function reverseCalculateTax(grandTotal, rate) {
_assertNumber(grandTotal, 'grandTotal');
_assertNumber(rate, 'rate');
const taxableAmount = applyRoundOffRules((grandTotal * 100) / (100 + rate));
const taxAmount = applyRoundOffRules(grandTotal - taxableAmount);
return {
grandTotal: applyRoundOffRules(grandTotal),
rate,
taxableAmount,
taxAmount
};
}

/**
* Determines whether a supply attracts IGST or CGST+SGST.
* @param {string} supplierStateCode - 2-digit GST state code, e.g. '33'
* @param {string} placeOfSupplyStateCode - 2-digit GST state code, e.g. '29'
* @returns {'IGST' | 'CGST_SGST'}
*/
function getApplicableTaxType(supplierStateCode, placeOfSupplyStateCode) {
if (!supplierStateCode || !placeOfSupplyStateCode) {
throw new TypeError('Both supplierStateCode and placeOfSupplyStateCode are required');
}
return String(supplierStateCode).trim() === String(placeOfSupplyStateCode).trim()
? 'CGST_SGST'
: 'IGST';
}

/**
* Calculates totals for an invoice of line items.
* Each item: { taxableValue, gstRate, cessRate? }
* @param {Array<{ taxableValue: number, gstRate: number, cessRate?: number }>} items
* @param {boolean} isInterState
* @returns {{ totalTaxableValue: number, totalCGST: number, totalSGST: number, totalIGST: number, totalCess: number, totalTax: number, grandTotal: number, roundOff: number }}
*/
function calculateInvoiceTotals(items, isInterState) {
if (!Array.isArray(items)) {
throw new TypeError(`items must be an array, got ${typeof items}`);
}

const acc = {
totalTaxableValue: 0,
totalCGST: 0,
totalSGST: 0,
totalIGST: 0,
totalCess: 0,
totalTax: 0
};

for (const item of items) {
const breakdown = calculateGSTBreakdown(item.taxableValue, item.gstRate, {
isInterState: !!isInterState,
cessRate: item.cessRate || 0
});
acc.totalTaxableValue += breakdown.taxableAmount;
acc.totalCGST += breakdown.cgst;
acc.totalSGST += breakdown.sgst;
acc.totalIGST += breakdown.igst;
acc.totalCess += breakdown.cess;
acc.totalTax += breakdown.totalTax;
}

const rawGrandTotal = acc.totalTaxableValue + acc.totalTax;
const grandTotal = applyRoundOffRules(Math.round(rawGrandTotal));
const roundOff = applyRoundOffRules(grandTotal - rawGrandTotal);

return {
totalTaxableValue: applyRoundOffRules(acc.totalTaxableValue),
totalCGST: applyRoundOffRules(acc.totalCGST),
totalSGST: applyRoundOffRules(acc.totalSGST),
totalIGST: applyRoundOffRules(acc.totalIGST),
totalCess: applyRoundOffRules(acc.totalCess),
totalTax: applyRoundOffRules(acc.totalTax),
grandTotal,
roundOff
};
}

/**
* Groups invoice line items by their GST rate (useful for GSTR-1 summaries).
* @param {Array<{ gstRate: number }>} items
* @returns {Object<string, Array>} keyed by rate
*/
function groupItemsByTaxRate(items) {
if (!Array.isArray(items)) {
throw new TypeError(`items must be an array, got ${typeof items}`);
}
return items.reduce((groups, item) => {
const key = String(item.gstRate);
(groups[key] = groups[key] || []).push(item);
return groups;
}, {});
}

module.exports = {
applyRoundOffRules,
calculateTax,
calculateGSTBreakdown,
reverseCalculateTax,
getApplicableTaxType,
calculateInvoiceTotals,
groupItemsByTaxRate
};
81 changes: 81 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,84 @@ export function searchHsn(query: string, options?: SearchOptions): HsnCode[];

/** Returns metadata about the bundled dataset (version, date, totals). */
export function getStats(): HsnStats;

// ── GST calculation utilities (issue #3) ────────────────────────────────────

export interface TaxResult {
taxableAmount: number;
rate: number;
taxAmount: number;
total: number;
}

export interface GSTBreakdown {
taxableAmount: number;
cgst: number;
sgst: number;
igst: number;
cess: number;
totalTax: number;
grandTotal: number;
}

export interface ReverseTaxResult {
grandTotal: number;
rate: number;
taxableAmount: number;
taxAmount: number;
}

export interface InvoiceLineItem {
taxableValue: number;
gstRate: number;
cessRate?: number;
}

export interface InvoiceTotals {
totalTaxableValue: number;
totalCGST: number;
totalSGST: number;
totalIGST: number;
totalCess: number;
totalTax: number;
grandTotal: number;
roundOff: number;
}

export interface GSTBreakdownOptions {
isInterState?: boolean;
cessRate?: number;
}

/** Rounds a monetary amount to 2 decimal places (half-up). */
export function applyRoundOffRules(amount: number): number;

/** Calculates tax on a taxable amount at a given percentage rate. */
export function calculateTax(taxableAmount: number, rate: number): TaxResult;

/** Splits GST into CGST/SGST (intra-state) or IGST (inter-state) plus optional cess. */
export function calculateGSTBreakdown(
taxableAmount: number,
gstRate: number,
options?: GSTBreakdownOptions
): GSTBreakdown;

/** Extracts the base amount and tax from a tax-inclusive total. */
export function reverseCalculateTax(grandTotal: number, rate: number): ReverseTaxResult;

/** Determines whether a supply attracts IGST or CGST+SGST based on state codes. */
export function getApplicableTaxType(
supplierStateCode: string,
placeOfSupplyStateCode: string
): 'IGST' | 'CGST_SGST';

/** Calculates totals for an invoice of line items. */
export function calculateInvoiceTotals(
items: InvoiceLineItem[],
isInterState: boolean
): InvoiceTotals;

/** Groups invoice line items by their GST rate (useful for GSTR-1 summaries). */
export function groupItemsByTaxRate<T extends { gstRate: number }>(
items: T[]
): Record<string, T[]>;
12 changes: 11 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ function getStats() {
return require(path.join(__dirname, 'data', 'metadata.json'));
}

const gst = require('./gst');

module.exports = {
getAllHsn,
getCodeByTxt,
Expand All @@ -128,5 +130,13 @@ module.exports = {
isValidHsnCode,
getHsnChapter,
searchHsn,
getStats
getStats,
// GST calculation utilities (issue #3)
applyRoundOffRules: gst.applyRoundOffRules,
calculateTax: gst.calculateTax,
calculateGSTBreakdown: gst.calculateGSTBreakdown,
reverseCalculateTax: gst.reverseCalculateTax,
getApplicableTaxType: gst.getApplicableTaxType,
calculateInvoiceTotals: gst.calculateInvoiceTotals,
groupItemsByTaxRate: gst.groupItemsByTaxRate
};
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "hsn-code-package",
"version": "2.0.0",
"description": "Fast, zero-runtime-dependency HSN code lookup. Search 12,000+ codes by description or code number, validate HSN codes, and browse by chapter.",
"version": "2.1.0",
"description": "Fast, zero-runtime-dependency HSN code lookup with GST calculation utilities. Search 12,000+ codes, validate HSN codes, browse by chapter, and compute CGST/SGST/IGST.",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"gst.js",
"index.d.ts",
"data/"
],
Expand Down
Loading
Loading