| name | indian-gst-einvoice |
| description | Indian GST calculation engine, GSTIN validation, and e-Invoice IRP integration for DropFlow. Use when working with GST logic in packages/gst/, validating GSTINs, calculating tax breakdowns, or submitting e-Invoices to IRP. |
Indian GST + e-Invoice — DropFlow
Location: packages/gst/
GST Calculation (src/calculate.ts)
import { GSTParams, GSTBreakdown } from "./types";
import { getHSNRate } from "./hsn-map";
export function calculateGST(params: GSTParams): GSTBreakdown {
const { subtotalPaise, hsnCode, sellerStateCode, buyerStateCode, isExport } = params;
const hsnEntry = getHSNRate(hsnCode);
const rate = hsnEntry.ratePercent;
if (isExport) {
return {
gstType: "EXPORT_LUT",
gstRatePercent: 0,
cgstPaise: 0,
sgstPaise: 0,
igstPaise: 0,
totalTaxPaise: 0,
totalWithTaxPaise: subtotalPaise,
hsnCode,
hsnDescription: hsnEntry.description,
};
}
if (sellerStateCode === buyerStateCode) {
const halfRate = rate / 2;
const cgstPaise = Math.round((subtotalPaise * halfRate) / 100);
const sgstPaise = Math.round((subtotalPaise * halfRate) / 100);
const totalTaxPaise = cgstPaise + sgstPaise;
return {
gstType: "CGST_SGST",
gstRatePercent: rate,
cgstPaise,
sgstPaise,
igstPaise: 0,
totalTaxPaise,
totalWithTaxPaise: subtotalPaise + totalTaxPaise,
hsnCode,
hsnDescription: hsnEntry.description,
};
}
const igstPaise = Math.round((subtotalPaise * rate) / 100);
return {
gstType: "IGST",
gstRatePercent: rate,
cgstPaise: 0,
sgstPaise: 0,
igstPaise,
totalTaxPaise: igstPaise,
totalWithTaxPaise: subtotalPaise + igstPaise,
hsnCode,
hsnDescription: hsnEntry.description,
};
}
HSN Code Map (src/hsn-map.ts)
Seed with common e-commerce HSN codes:
interface HSNEntry {
code: string;
description: string;
ratePercent: number;
}
const HSN_MAP: Record<string, HSNEntry> = {
"6109": { code: "6109", description: "T-shirts, singlets and other vests, knitted", ratePercent: 5 },
"6110": { code: "6110", description: "Jerseys, pullovers, cardigans, knitted", ratePercent: 12 },
"6204": { code: "6204", description: "Women's suits, dresses, skirts, woven", ratePercent: 12 },
"6403": { code: "6403", description: "Footwear with outer soles of rubber/plastic", ratePercent: 18 },
"8471": { code: "8471", description: "Automatic data processing machines (laptops, computers)", ratePercent: 18 },
"8517": { code: "8517", description: , : },
: { : , : , : },
: { : , : , : },
: { : , : , : },
: { : , : , : },
: { : , : , : },
: { : , : , : },
: { : , : , : },
};
(): {
entry = [code.(, )];
(!entry) ();
entry;
}
GSTIN Validation (src/validate-gstin.ts)
const GSTIN_REGEX = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/;
const CHECKSUM_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
export function validateGSTIN(gstin: string): { valid: boolean; stateCode: string; error?: string } {
if (!GSTIN_REGEX.test(gstin)) {
return { valid: false, stateCode: "", error: "Invalid GSTIN format" };
}
const stateCode = gstin.substring(0, 2);
const stateNum = parseInt(stateCode, 10);
if (stateNum < 1 || stateNum > 37) {
return { valid: false, stateCode, error: "Invalid state code" };
}
let sum = 0;
for (let i = 0; i < 14; i++) {
const charIndex = CHECKSUM_CHARS.(gstin[i]);
factor = i % === ? : ;
product = charIndex * factor;
sum += .(product / ) + (product % );
}
expectedCheck = [( - (sum % )) % ];
(gstin[] !== expectedCheck) {
{ : , stateCode, : };
}
{ : , stateCode };
}
Indian State Codes
export const STATE_CODES: Record<string, string> = {
"01": "Jammu & Kashmir", "02": "Himachal Pradesh", "03": "Punjab",
"04": "Chandigarh", "05": "Uttarakhand", "06": "Haryana",
"07": "Delhi", "08": "Rajasthan", "09": "Uttar Pradesh",
"10": "Bihar", "11": "Sikkim", "12": "Arunachal Pradesh",
"13": "Nagaland", "14": "Manipur", "15": "Mizoram",
"16": "Tripura", "17": "Meghalaya", "18": "Assam",
"19": "West Bengal", "20": "Jharkhand", "21": "Odisha",
"22": "Chhattisgarh", "23": "Madhya Pradesh", "24": "Gujarat",
"26": ,
: , : , : ,
: , : , : ,
: , : , : ,
: ,
};
e-Invoice IRP Integration (Custom — No OSS Library)
Build a typed wrapper around the NIC/IRP REST API:
const IRP_BASE_URL = "https://einv-apisandbox.nic.in";
interface IRPAuthResponse {
AuthToken: string;
TokenExpiry: string;
}
export async function authenticateIRP(gstin: string): Promise<string> {
const res = await fetch(`${IRP_BASE_URL}/eivital/v1.04/auth`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"gstin": gstin,
"user_name": env.IRP_USERNAME,
"password": env.IRP_PASSWORD,
},
});
const data: IRPAuthResponse = await res.json();
return data.AuthToken;
}
export async function generateIRN(token: string, invoicePayload: ) {
res = (, {
: ,
: {
: ,
: token,
},
: .(invoicePayload),
});
res.();
}
Conventions
- All arithmetic in integer paise — never use floating-point for money
Math.round() for CGST/SGST 50/50 split on odd amounts
- CGST+SGST total must exactly equal the full-rate amount (rounding goes to SGST)
- HSN map lookup uses first 4 digits of HSN code
- GSTIN checksum uses mod-36 algorithm — validate before storing
- e-Invoice IRP: use sandbox URL during development, switch to production in env
- 100% unit test coverage required for
calculateGST() and validateGSTIN()