Mesrai
Back to blog
// essayTechnical Deep Dive

GST Calculation on Negative Refunds: How Mesrai Spots the Sign Flip

Real PR catch: refund-invoice with Math.abs on amount — GST positive on negative invoice. Sign-preserving fix.

Mesrai TeamAugust 21, 20268 min read

A refund-invoice builder. The author wanted GST always positive, used `Math.abs`. Net effect: GSTR-1 filings show tax collected on credit notes — auditor flagged it as inconsistent with the actual book entries.

The vulnerable diff

tsbilling/refund-invoice.ts
// billing/refund-invoice.ts
function buildRefundInvoice(refundRupees: number) {
  // BUG: Math.abs strips the sign — tax positive on a negative invoice
  const gst = Math.abs(refundRupees) * 0.18;
  return {
    amount: refundRupees,    // -1000 (negative for refund)
    gst:    gst,             // +180 (positive — wrong sign for credit note)
    total:  refundRupees + (refundRupees < 0 ? -gst : gst),
  };
}

What is wrong

Indian GST (and most VAT regimes) treats credit notes as negative invoices — the amount, the tax, and the total all carry a negative sign. GSTR-1 reports separate tax credited (refunds) from tax collected (sales); a positive tax on a negative-amount row breaks that separation. The fix is straightforward: tax always inherits the sign of the amount it's computed on. `Math.abs` is the wrong tool. Multiplication preserves sign for free.

The attack

Auditor's flag:

textexploit
GSTR-1 row for the period:
   Sales tax (output):    18000.00
   Credit-note tax:           0.00  (should be -1800 from the refund)
   Net tax payable:       18000.00  (should be 16200)

Cumulative impact: GST overpaid in every period that has refunds.

Refund volume = error volume.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Logic] [Tax-Sign] [medium]

`Math.abs(refundRupees) * 0.18` produces a positive GST on a
negative invoice — incompatible with GSTR-1 reporting and with
the rest of your accounting.

Tax should carry the same sign as the amount it taxes:

  function buildRefundInvoice(refundRupees: number) {
    const gst = refundRupees * 0.18;     // sign follows amount
    return {
      amount: refundRupees,
      gst,
      total: refundRupees + gst,
    };
  }

For a refund of ₹1000:
  amount: -1000, gst: -180, total: -1180

Now GSTR-1 correctly nets the refund's tax against output tax.

The fix

tsbilling/refund-invoice.ts (fixed)
// billing/refund-invoice.ts — fixed (with integer paise for currency safety)
function buildRefundInvoicePaise(refundPaise: number) {
  // GST 18% via basis points: 1800 / 10000
  const gstPaise = Math.round((refundPaise * 1800) / 10_000);
  return {
    amountPaise: refundPaise,
    gstPaise,
    totalPaise: refundPaise + gstPaise,
  };
}

Tax inherits sign from amount. Integer paise math (cluster 8 post 1 applies here too). Refund rows now net correctly in GSTR-1.

Why human review missed it

Tax-sign bugs are hard to find in code review because the code looks correct in isolation — positive tax on positive amount works. The error only shows on credit notes / refunds. Mesrai catches `Math.abs` on currency variables and recommends sign-preserving math.

Related rules + further reading

Mesrai rule pack: logic/tax-sign-matches-amount — flags Math.abs() on currency variables in tax calculation.

Indian GST Act § 34 — credit and debit notes.

GSTR-1 schema — separate fields for sales and credit-note tax.

Takeaway

Tax sign follows amount sign. No Math.abs. Credit notes flow correctly through GSTR-1. Mesrai catches the strip.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.