Invoicing
GST-correct invoices, computed server-side, frozen at finalize. No floating-point drift, no per-FY numbering races, no manual intra-vs-inter judgement.
Three states. One direction.
draft is editable. finalized is
immutable — the line items, GST math, customer snapshot, and
invoice number are frozen in a JSONB snapshot_jsonb
column for 6-year retention. cancelled requires
no payments to be allocated (guarded by ErrCannotCancelPaid).
Intra-state vs inter-state — computed automatically.
The same lines, with the supplier in Karnataka (state 29). One invoice goes to a Karnataka buyer, one to a Maharashtra buyer. mb makes the call.
Buyer in Karnataka (state 29)
Buyer state 29 = supplier state 29 → CGST 9% + SGST 9%
| HSN | Description | Qty | Rate | Taxable |
|---|---|---|---|---|
| 7208 | MS plates 6mm | 10.000 | 1,000.00 | 10,000.00 |
| 7308 | Welded structures | 1.000 | 5,000.00 | 5,000.00 |
- Subtotal
- ₹15,000.00
- CGST @ 9%
- ₹1,350.00
- SGST @ 9%
- ₹1,350.00
- IGST
- ₹0.00
- Total
- ₹17,700.00
Buyer in Maharashtra (state 27)
Buyer state 27 ≠ supplier state 29 → IGST 18%
| HSN | Description | Qty | Rate | Taxable |
|---|---|---|---|---|
| 7208 | MS plates 6mm | 10.000 | 1,000.00 | 10,000.00 |
| 7308 | Welded structures | 1.000 | 5,000.00 | 5,000.00 |
- Subtotal
- ₹15,000.00
- CGST
- ₹0.00
- SGST
- ₹0.00
- IGST @ 18%
- ₹2,700.00
- Total
- ₹17,700.00
Both invoices are saved with the same line data — mb infers
intra-vs-inter from the supplier business's state_code
compared to the buyer's place_of_supply. No checkbox
to forget.
Gapless numbering, enforced at the DB.
GST law requires invoice numbers to be sequential within a
financial year. mb stores the last-issued sequence in
invoice_counters(business_id, fy, last_seq) and
increments it inside the finalize transaction with
SELECT FOR UPDATE. Two simultaneous finalize requests
cannot get the same number, ever.
-- Inside the finalize tx (simplified) SELECT last_seq FROM invoice_counters WHERE business_id = $1 AND fy = $2 FOR UPDATE; -- ← row-locked until this tx commits UPDATE invoice_counters SET last_seq = last_seq + 1 WHERE business_id = $1 AND fy = $2 RETURNING last_seq;
6-year retention, built in.
On finalize, mb writes a JSONB snapshot_jsonb column
that captures the full pricing, lines, customer details, business
details, and tax math as they existed at the moment of finalize.
Customer renames, address edits, GSTIN updates — none of them
retroactively change a filed invoice. The audit trail is exactly
what you filed.
Same pattern on credit notes. Same 6-year horizon mandated by the CGST Rules.