An inventory of Posnic's existing GST reporting, measured against what GSTR-1,
GSTR-3B and GSTR-9 actually require. Written from the code on develop.
This answers #45 GST returns: document current report gaps, the gap-analysis slice of #30 India GST return preparation. It changes no behaviour and no calculation. Its companion for the e-invoice side is INDIA_EINVOICING_READINESS.md.
Nothing here is tax advice. It is a description of code.
Posnic has one filing-capable artefact and one correct tax position, and a lot of screens around them that look more finished than they are.
| Genuinely filing-shaped | The GSTR-1 B2B JSON export. Correct grain, correct field names, correct place-of-supply derivation, and it has tests. |
| Genuinely correct | The tax-payable report's per-head netting, which applies the statutory IGST then CGST then SGST order and never lets CGST pay SGST. |
| Screens over weak aggregations | GSTR-1's on-screen table, GSTR-2, GSTR-2B, GSTR-3 and GSTR-9. Their groupings lose rows, and four of them export "JSON" that no filing tool can read. |
| Absent | Ten of the thirteen GSTR-1 tables, every exemption classification, cess, advances, the document series, and any GSTR-2B reconciliation. |
Three structural gaps cut across everything, and none is cosmetic:
- No supply-nature classification. Nothing anywhere records that a line is exempt, nil-rated, non-GST, zero-rated, an export, or under reverse charge. Tables 4B, 6 and 8 of GSTR-1 cannot be derived from stored data at all, because the data does not exist.
- Credit notes are filed in the wrong month, and only for registered buyers. The returns aggregation reuses the invoice's date filter and the invoice's buyer-type filter.
- No cess field anywhere. The GSTR-1 JSON hardcodes
csamt: 0, which is correct only while no shop sells a cess good.
Two pieces are worth protecting when the rest is changed.
gstOneReportPageJson in
api/src/repositories/sale.repository.js
(around line 6599), reached from GET /sales/gstOneReportTableJson. It groups
by { sales_id, ctin, rate }, which is exactly the grain of a GSTR-1 itms
line, with real $sum accumulators. It emits the offline tool's own shape:
{ "ctin": "...", "inv": [ { "inum": "...", "idt": "DD-MM-YYYY", "val": 0,
"pos": "33", "rchrg": "N", "inv_typ": "R",
"itms": [ { "num": 1, "itm_det": { "rt": 5, "txval": 0,
"iamt": 0, "camt": 0, "samt": 0, "csamt": 0 } } ] } ] }Place of supply comes from gstStateCode, which takes the buyer GSTIN's first
two digits and falls back to a state-name table. It is covered by
api/tests/unit/repositories/gstr1-export.test.js.
Three values are hardcoded and each is a real limitation: rchrg is always
N, inv_typ is always R (regular, so SEZ and deemed-export invoice types
cannot be expressed), and csamt is always 0.
taxPayablePage (same file, around line 6106) with
api/src/utils/tax-netting.js. Output tax
comes from sales, input credit from receivings filtered on itc_eligible and
excluding cancelled ones, grouped by calendar month, then netted head by head
in the statutory order. This is the only place in the codebase that treats
IGST, CGST and SGST as separate ledgers rather than one pot, and it is tested.
| Report | Where | What it actually does |
|---|---|---|
| GSTR-1 screen | gstOneReportPage, sale repository |
Four aggregations. The sales one groups by a ten-field tuple with no accumulators, so it is a DISTINCT: two lines on one invoice with the same rate and the same line total collapse into one row and the second is lost. |
| GSTR-1 JSON | gstOneReportPageJson |
Correct. See above. |
| GSTR-2 | inline in receivings.controller.js |
Aggregation written directly in the controller rather than a repository. Products grouped by item name. |
| GSTR-2B | same endpoint as GSTR-2 | A GSTR-2B-labelled skin over GSTR-2 data. Its "ITC available", "ITC not available" and "ITC reversal" sections are static headings that nothing fills. |
| GSTR-3 | gstThreeReportPage |
Six aggregations. The taxed-sales group carries the document-level items_subtotal in its _id, so an invoice with two rates counts its subtotal twice. |
| GSTR-9 | gstNineReportPage, on the receiving model |
Five aggregations, four of which query sales. Takes the branch from the session with no fallback, unlike its sibling reports. |
| Tax summary | taxSummaryReportTable |
Filters on updated_date, not date, so editing an old sale moves it into the current period. Its GST position is single-bucket output minus input, with no per-head separation, and it ignores itc_eligible. |
| Tax payable | taxPayablePage |
Correct, per head, monthly. |
| Tax report | taxSalesReportPage |
Returns raw unwound line documents; all tax arithmetic happens in the browser. |
The GSTR-1 page renders the whole statutory form as static HTML. Four table bodies are ever populated; the rest are literal zeros or empty.
| Table | Status |
|---|---|
| 4A B2B | Partial. Screen table loses duplicate lines; the JSON export is correct. |
| 4B reverse charge | Absent. No reverse-charge field exists; rchrg is hardcoded. |
| 4C e-commerce | Absent. No operator GSTIN on a sale. |
| 5 B2CL, inter-state above the value threshold | Absent. No invoice-value test anywhere. |
| 6A exports, 6B SEZ, 6C deemed export | Absent. No export flag, shipping bill, port code or LUT. |
| 7 B2CS | Half. Only the inter-state part is produced, and not consolidated rate-wise. Intra-state is never filled. |
| 8 nil-rated, exempt, non-GST | Absent. Four rows of literal zero. |
| 9B CDNR, registered credit notes | Partial and mis-filed. See below. |
| 9B CDNUR, unregistered | Absent. Filtered out by the buyer-type condition. |
| 11 advances | Absent. No advance concept in the data model. |
| 12 HSN summary | Present in name. Grouped by item name, and the HSN column is a tax-name string that is blank whenever the line used a tax group. No unit-code column, no rate column. |
| 13 documents issued | Absent. Empty table body; no series or cancelled-count aggregation. |
A return is stored inside the original sale, as items_return[] entries with
return_tax and items_return_total rollups. Only one GST report reads
them at all, and it has two defects that both matter at filing time.
- The credit note is filed in the invoice's month. The returns aggregation
reuses the same
filtersobject as the sales aggregation, and that object filters on the sale'sdate. A credit note raised in October against an August invoice appears in August's GSTR-1 and is invisible in October's. - Unregistered credit notes are dropped. The same shared filter carries
customer_gst_type: { $in: ['regular', 'composite'] }, so a credit note to a walk-in customer never reaches any report. That is table 9B CDNUR, and it is simply missing.
Every other GST report ignores returns entirely. GSTR-3, GSTR-9, tax payable
and the tax summary all report output tax gross of credit notes. The tax
summary additionally includes PartialReturn sales at their full original line
tax with no offset.
The receivings side has a mirror of the same aggregation in which
return_date and return_receiving_date are populated from each other's
source.
taxPayablePage and taxPayableRegisterPage are the good path: itc_eligible
gating, a declared-total mismatch flag, a document-present flag, and per-head
netting. Two cruder duplicates of the same idea exist in the tax summary
controller and in the GSTR-3 aggregation, and the tax summary one ignores
itc_eligible entirely.
What does not exist:
- No HSN on purchases at all. Neither the receiving model nor its controller mentions one.
- No GSTR-2B reconciliation of any kind. No upload of the portal's 2B
file, no matched/unmatched/missing classification, no supplier filing status.
Searches for reconciliation,
gstr2band matching terms return nothing relevant. - The receiving repository contains no tax logic whatsoever; it all sits in the model and inline in the controller.
None of these exist as data. There is no is_exempt on an item, no
nil-rated flag, no non-GST marker, no zero-rated or LUT field, no reverse-charge
flag, and no cess rate or amount anywhere in any model.
The consequence is that a shop cannot distinguish a nil-rated good from a good taxed at zero per cent: the tax summary lumps both into one bucket labelled "0% / untaxed". GSTR-3B's exempt-inward-supplies row is filled by a proxy - purchases from suppliers whose registration type is consumer or composite - which is a statement about the supplier, not about the goods.
Cess appears only as column headings in five report pages, literal 0.00
values written into every cess cell, and the hardcoded csamt: 0 in the JSON
export, whose comment already says cess stays zero until a product records one.
Only the GSTR-1 JSON is built properly, as a Blob with an
application/json type and a real filing structure.
GSTR-2, GSTR-2B, GSTR-3 and GSTR-9 all use the same broken pattern: read the
rendered table's column headings as object keys and the formatted cell text as
values, then emit them as a data:text/csv URI with a .json file extension,
passed through escape(). The keys are English column headings, the values are
formatted currency strings, the MIME type contradicts the extension, and
non-ASCII characters are mangled. No filing tool can consume any of it. The
GSTR-1 export was fixed; these four were not.
There is no server-side export for any GST report.
| Covered | Not covered |
|---|---|
gstOneReportPageJson (7 tests), tax netting (7), tax engine (11), tax profiles, tax regime, GST 2.0 readiness scan, country tax seeding |
gstOneReportPage (the screen table), gstThreeReportPage, gstNineReportPage, gstTwoReportTable, taxPayablePage, taxPayableRegisterPage, taxSalesReportPage |
Every aggregation with a known defect above is in the untested column.
An automated sweep of this repository reported that HSN codes are never
persisted on a sale line, on the grounds that sale.model.js does not declare
the field. That is half true and the conclusion is wrong. The line schema is
declared strict: false, and sale.service.js (around line 592) does write
hsncode and hsndescription onto each line from the item master at sale
time. So new sales do carry an HSN; sales written before that line existed
do not, which is why the read paths back-fill from the item master for display.
The practical effect for GSTR-1 table 12 is unchanged: the HSN summary does not use the field. It groups by item name and reports a tax-name string in the HSN column.
In dependency order. Each is a reviewable PR; sizes follow the issue labels.
| # | Work | Unblocks | Size |
|---|---|---|---|
| 1 | A supply-nature field on the item master and a snapshot on the sale line: taxable, exempt, nil-rated, non-GST, zero-rated | GSTR-1 table 8, GSTR-3B's exempt rows, honest zero-rate reporting | medium |
| 2 | A reverse-charge flag on the sale, and an e-commerce operator GSTIN | table 4B, 4C, rchrg stops being hardcoded |
small |
| 3 | Cess rate and amount on item, sale line and receiving | csamt stops being hardcoded; tobacco and similar shops become servable |
medium |
| 4 | Move the credit-note aggregation onto the return's date and drop the buyer-type filter | table 9B files in the right month; CDNUR appears | small, high value |
| 5 | Rewrite the GSTR-1 screen aggregation to group with accumulators instead of a distinct tuple | duplicate lines stop vanishing | small |
| 6 | HSN summary keyed on the HSN code with quantity, unit code and rate | table 12 becomes real; needs the unit-code work from the e-invoice design | medium |
| 7 | B2CS consolidation and a B2CL value threshold | tables 5 and 7 | medium |
| 8 | A document-issued series aggregation | table 13 | small |
| 9 | Replace the four DOM-scraping exports with server-built JSON in the GSTR-1 export's style | GSTR-2, 3 and 9 exports become usable | medium |
| 10 | Returns accounted for in GSTR-3, GSTR-9, tax payable and the tax summary | output tax stops being overstated | medium |
| 11 | Tests for every aggregation listed as untested above | the defects above stop coming back | medium |
| 12 | GSTR-2B reconciliation: import the portal file, classify matched, unmatched and missing | input credit can be checked rather than assumed | large |
Items 4 and 5 are the best value for their size: both are small, both fix wrong numbers rather than missing ones, and wrong numbers are worse.
Preparing a return and submitting one are different problems with different constraints. Submission is not a matter of writing the client: access to the GST return APIs is granted through a specific programme with its own eligibility, and that pathway is documented separately in INDIA_GOVERNMENT_INTEGRATION.md. Nothing in the list above depends on it. A shop that can produce a correct, complete GSTR-1 JSON and upload it to the portal is filing; the difference is one file transfer.
The work above should be finished first regardless of which submission route is chosen, because every route sends the same data.