Skip to content

GST (India)

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:

const GSTIN_REGEX = /^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$/;

API

  • normalizeGstin(value) — trim + uppercase; empty/nullish → ''.
  • isValidGstin(value) — shape check. Returns false on null, 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):

  1. No GSTIN checksum validation. That's the portal's job on upload.
  2. No HSN lookup. We emit whatever the invoice line carries. Lines without an HSN roll into a blank bucket — UI should warn.
  3. 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:

  1. Production GSTN developer credentials (GSP tier).
  2. The OTP-based auth handshake.
  3. 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)

  1. OTP handshake against /taxpayerapi/v1.3/authenticate.
  2. Session-key derivation (RSA-encrypted ephemeral AES key).
  3. Encrypt payload before POST.
  4. Persist session state in a new GstnSession Supabase table (noted inline at gstnApiClient.ts:23).
  5. 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() from src/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.tshandleFinanceGstSummary).
  • 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.