GST (India)
Written April 2026 — read this first
GSTIN handling, HSN/SAC and the GSTR builders below are still right. The GST summary is now computed in SQL by entryDate in IST and includes reversals, so cancellation GST comes off the output tax. Which GST scheme the company uses is still an open decision (FIN-010), and GST on advances (FIN-011, LAW) is not built.
GSTIN capture, HSN/SAC codes, and the GSTR-1 / GSTR-3B filing-format export. The direct-upload path to the GST portal is stubbed pending production credentials.
1. GSTIN validator
src/lib/gstin.ts — two tiny pure functions. This is the shape check
every frontend form should run before sending to the API; it is
not a checksum validator.
Format
27ABCDE1234F1Z5
└┬┘└───┬────┘└┘└┘
│ │ │ │
│ │ │ └── checksum (1 alphanumeric)
│ │ └──── literal 'Z'
│ └────────── entity number (1 alphanumeric)
│ PAN (10 chars: 5 letters, 4 digits, 1 letter)
└── state code (2 digits)
The regex lives at src/lib/gstin.ts:19:
API
normalizeGstin(value)— trim + uppercase; empty/nullish →''.isValidGstin(value)— shape check. Returnsfalseonnull, wrong length, or regex miss.
Checksum is GST portal's job
We deliberately do not validate the code32 checksum client-side — it requires the GSTN code32 table. The GST portal rejects bad checksums at upload. Our job is the form-level sanity check.
2. Party-level GSTIN
Columns added by supabase/migrations/20260418150000_gst_primitives.sql:
| Table | Column | Notes |
|---|---|---|
Customer |
gstNumber |
Optional 15-char GSTIN. |
Supplier |
gstNumber |
Optional 15-char GSTIN. |
Agent |
gstNumber |
Added earlier (migration 20260409130000). |
All optional metadata. No DB CHECK constraint — validation is
client-side via isValidGstin().
3. HSN / SAC on line items
- HSN — Harmonized System of Nomenclature (goods).
- SAC — Services Accounting Code (services).
Columns added to the following tables (gst_primitives.sql):
| Table | Columns |
|---|---|
QuotationLineItem |
hsnCode, sacCode |
SupplierTransaction |
hsnCode, sacCode |
Invoice |
hsnCode, sacCode |
No DB CHECK on HSN XOR SAC
The migration comment calls out that the business rule (typically one or the other) is enforced by the UI, not by a DB CHECK, because existing tables hold mixed goods + services historically.
GroupInvoice and AgentInvoice store line items as JSONB; HSN/SAC
lives inside the JSON payload.
4. GSTR-1 / GSTR-3B payload generator
src/lib/gstReports.ts builds the JSON payloads that the GSTN portal /
offline tool ingests. Schema field names match the GSTN v2 offline-utility.
Sections implemented
| Section | What it is |
|---|---|
b2b |
Registered B2B (buyer has GSTIN) |
b2cl |
B2C-Large — unregistered inter-state, invoice ≥ ₹2,50,000 |
b2cs |
B2C-Small — everything else B2C, aggregated per rate × state |
cdnr |
Credit / debit notes to registered parties |
hsn |
HSN summary aggregated by HSN × rate |
Sections stubbed / out of scope
exp— Exports. Stub placeholders only.cdnur— Credit / debit notes to unregistered parties. Stub.at,atadj,txp,doc_issue,nil— not implemented (we don't have the data).
What the generator DOESN'T do
Three deliberate omissions (src/lib/gstReports.ts:18-24):
- No GSTIN checksum validation. That's the portal's job on upload.
- No HSN lookup. We emit whatever the invoice line carries. Lines without an HSN roll into a blank bucket — UI should warn.
- No reverse-charge handling beyond the section stubs.
5. GSTN portal API client
src/lib/gstnApiClient.ts is a stub. The real upload path needs:
- Production GSTN developer credentials (GSP tier).
- The OTP-based auth handshake.
- Request / response encryption per the GSTN ASP/GSP spec (RSA + AES-ECB + HMAC-SHA256 on each payload).
None of those are implemented. uploadGstr1() and uploadGstr3b() both
throw with a clear "not yet enabled" message so the UI never silently
pretends to file.
Env vars (placeholder names)
| Variable | Purpose |
|---|---|
VITE_GSTN_API_BASE |
https://api.sandbox.gst.gov.in or https://api.gst.gov.in |
VITE_GSTN_CLIENT_ID |
GSP client ID |
VITE_GSTN_CLIENT_SECRET |
GSP client secret (move to Supabase secrets for prod) |
VITE_GSTN_FILER_GSTIN |
Our GSTIN |
VITE_GSTN_DEVICE_ID |
Registered device identifier |
isGstnConfigured() returns true only when all five are set. The UI
uses it to disable the "File to GSTN" button.
TODO list (follow-ups)
- OTP handshake against
/taxpayerapi/v1.3/authenticate. - Session-key derivation (RSA-encrypted ephemeral AES key).
- Encrypt payload before POST.
- Persist session state in a new
GstnSessionSupabase table (noted inline atgstnApiClient.ts:23). - Status polling via the returned
reference_id.
Until then, operators use "Download JSON" in the UI and upload via the GST portal's offline utility.
6. UI
Finance page → Reports tab → GST Returns button
(Finance.tsx:6492, permission finance.reports.gst_summary.view).
The rendered view exposes:
- GSTR-1 JSON download — runs
generateGstr1Payload()fromsrc/lib/gstReports.ts, previews the section counts, offers download. - GSTR-3B JSON download — same flow with
generateGstr3bPayload(). - GST Summary — totals per rate, per state. Backed by
/finance/gst-summary(src/lib/api.ts→handleFinanceGstSummary). - File to GSTN button — disabled unless
isGstnConfigured()is true AND the user holds the relevant permission. Currently always disabled (stub).
7. Endpoints
| Method | Path | Handler | Permission |
|---|---|---|---|
GET |
/finance/gst-summary |
handleFinanceGstSummary (src/lib/api.ts) |
finance.reports.gst_summary.view |
GET |
/finance/gstr-1 |
handleFinanceGstr1 (src/lib/api.ts:24846) |
finance.reports.gst_summary.view |
GET |
/finance/gstr-3b |
handleFinanceGstr3b (src/lib/api.ts:25049) |
finance.reports.gst_summary.view |
gst-summary / gstr-1 / gstr-3b return the computed payload; the
UI wraps them in a download action.
8. Permissions
| Action | Permission |
|---|---|
| View GST Summary / Returns | finance.reports.gst_summary.view |
| Export / download GST JSON | finance.reports.gst_summary.export |
| File to GSTN (future) | finance.reports.gst_summary.export (+ GSTN creds) |
| Create / edit invoices with GST | finance.create / finance.edit |
9. Tests
src/lib/gstin.test.ts— regex + normalisation coverage.src/lib/gstReports.test.ts— section bucketing (b2b vs b2cl vs b2cs), HSN summary aggregation, credit-note back-links.
Run: npx vitest run src/lib/gstin.test.ts src/lib/gstReports.test.ts.