Direct answer
Most GST billing bugs are data bugs that surface as tax errors. Store money as integer paise, attach tax rates to the product and service master, decide CGST/SGST versus IGST in one place, back-calculate GST from MRP, apply discounts before tax, compute dates in IST, and make finalized bills immutable.
The biggest lesson from building the GST billing engine in Clinizy Care is that most invoicing bugs aren’t tax-law bugs — they’re data bugs that show up as tax errors. Store money as integer paise, keep tax rates on the product and service master, decide CGST/SGST versus IGST in one place, back-calculate GST from MRP, apply discounts before tax, compute dates in IST, and make finalized bills impossible to edit. Get those right and the tax maths mostly takes care of itself.
This is an engineering write-up, not tax or legal advice. GST treatment depends on your facts and the rules change; have a chartered accountant review your tax logic.
Key takeaways
- Money is integers. Store and compute every amount as integer paise; format to rupees only at the display or PDF edge.
- Rates are data, not code. With the rate on the product or service master, a rate change becomes a master-data edit, not a deploy — and September 2025 proved rates do change.
- Tax identity drives everything. The clinic’s GSTIN decides whether GST applies and how it splits — read it from the source of truth, never a cached session.
- Taxable value comes first. Back-calculate it from MRP, and allocate bill-level discounts before tax. An internal audit once caught us getting the second part wrong.
- India runs on IST. UTC puts post-midnight bills on the wrong day and, on 31 March, in the wrong financial year.
What does a clinic GST billing engine have to get right?
One clinic bill can mix an exempt consultation, medicines at different rates whose printed price already includes tax, and a procedure. The clinic may or may not be GST-registered, the patient is almost always an unregistered individual, and the front desk is busy. Building billing for Clinizy Care, our hospital management software for Indian clinics, nursing homes and small hospitals, we encoded the rules below. Each prevents a specific, boring, expensive bug.
| Billing rule | How we implemented it | Bug it prevents |
|---|---|---|
| Money is exact to the paisa | Integer paise everywhere | Floating-point drift |
| Intra-state: CGST + SGST; inter-state: IGST | GSTIN state code vs patient state; SGST = tax − CGST | Halves that don’t add up to the tax |
| Clinic healthcare services are exempt, with exclusions | Consultations at 0%; rates on the master (HSN/SAC) | Staff typing the wrong rate |
| Unregistered clinics can’t collect GST | No GSTIN: every line 0%, GST entry locked | Collecting tax illegally |
| MRP includes all taxes | GST = MRP − round(MRP / (1 + rate)) | A sale a paisa above MRP |
| Line and slab totals must agree | Per-line tax, reconciled per slab one paisa at a time | An invoice that contradicts itself |
| Discounts reduce taxable value | Pro-rata allocation before tax | Tax on the pre-discount value |
| Invoice numbers unique per financial year | Atomic counter in the bill’s transaction | Duplicate numbers under concurrency |
| Indian dates are IST dates | Days, months and financial year computed in IST | Bills on the wrong day or financial year |
| Issued bills are records | Finalized bills immutable; day-close lock | Edits after cash is reconciled |
For the product story rather than the engineering one, see the Clinizy Care case study.
How should you store money and round tax on an invoice?
As integers in paise, never as floating-point rupees. Binary floating point can’t represent most decimal fractions exactly: in JavaScript, 0.1 + 0.2 evaluates to 0.30000000000000004. Invisible on one bill; a paisa somebody has to chase across a month of exports.
In Clinizy Care, every amount is stored and computed as integer paise. Conversion happens once on the way in (toPaise = Math.round(rupees × 100)), and formatting back to rupees happens only at the display and PDF edge.
Integers don’t remove rounding; they make it explicit. The subtle case is the invoice summary. Take three lines at 5% with a taxable value of ₹10.10 each: each line’s tax is 50.5 paise, rounding to 51, so the lines total 153 paise — but 5% of the slab’s combined ₹30.30 is 151.5, rounding to 152. So we compute tax per line, then reconcile each GST slab one paisa at a time until line totals and slab totals agree. Nobody notices a paisa; everybody notices an invoice that doesn’t add up.
How do you decide between CGST + SGST and IGST?
An intra-state supply is taxed as central plus state tax (section 9 of the CGST Act); an inter-state supply attracts integrated tax (section 5 of the IGST Act). Sections 7 and 8 of the IGST Act classify a supply by comparing the supplier’s location with the place of supply. And according to the GST Council’s guide to registration, a GSTIN’s first two digits are the state code.
Our rule compares the clinic’s GSTIN state code with the patient’s address state. Different states means IGST. Same state means a split, which is where naive code goes wrong: rounding tax / 2 for each half turns a 2,701-paisa tax into 1,351 + 1,351. We compute CGST = round(tax / 2) and SGST = tax − CGST, giving 1,351 + 1,350, so the halves always add back to the exact tax.
If the patient’s state is missing, we default to intra-state — a deliberate, documented choice that matches the statutory fallback for goods. Under section 10(1)(ca) of the IGST Act, in force since 1 October 2023 according to CBIC Circular No. 209/3/2024-GST, goods supplied to an unregistered person are placed at the address recorded on the invoice, or the supplier’s location if none is recorded. Services follow section 12 of the IGST Act, where some, including health services, turn on where the service is performed. If you build this, keep place of supply in one tested function with an explicit branch per supply type, and have your CA sign off on each.
// Simplified for illustration; not production code
function splitTax(taxPaise: number, clinicGstin: string, patientState?: string) {
const clinicState = clinicGstin.slice(0, 2);
if (patientState && patientState !== clinicState) {
return { igst: taxPaise, cgst: 0, sgst: 0 };
}
const cgst = Math.round(taxPaise / 2); // missing state: intra-state
return { igst: 0, cgst, sgst: taxPaise - cgst };
}
Want to build this — the right way?
Brynex Labs designs and ships production-grade AI agents, automation, and software for teams in India and worldwide. Book a free scoping call and we'll tell you honestly what's worth building — and what isn't yet.
Why should GST rates be data, not code?
Because rates change, and one bill can carry several. Under entry 74 of Notification No. 12/2017-Central Tax (Rate), “health care services by a clinical establishment, an authorised medical practitioner or para-medics” are taxed at nil — but the notification’s definition of health care services excludes hair transplant and cosmetic or plastic surgery, unless done to restore or reconstruct anatomy or functions affected by congenital defects, developmental abnormalities, injury or trauma. “Services are exempt” isn’t a rule you can hard-code; it’s a property of each service.
In Clinizy Care, consultations are billed at 0%, and the tax rate is attached to the product or service master: inventory items carry HSN codes, services carry SAC codes, and front-desk staff never type a rate by hand. That principle sits at the centre of Clinizy Care’s GST billing software for clinics, because manual GST entry on a busy counter is where errors creep in.
Then the rates move. At its 56th meeting, the GST Council recommended replacing the four-tier structure with a standard rate of 18% and a merit rate of 5%, plus a 40% de-merit rate for a select few goods and services, effective 22 September 2025 for services and most goods, according to PIB’s release on the Council’s recommendations. PIB’s backgrounder on the reforms describes this as removing the earlier 12% and 28% rates. For healthcare, the release lists 33 lifesaving drugs moving from 12% to nil, three more from 5% to nil, all other drugs and medicines from 12% to 5%, and supplies such as bandages, diagnostic kits and glucometers from 12% to 5%.
Now picture a codebase with a hard-coded list of rates, a validator that rejects other rates, or a slab summary with fixed columns: each needs a code change and a deploy. With the rate on the master, a rate change is a master-data edit, and the slab summary derives from whatever rates are on the lines. The corollary: a bill should keep the rate it was billed at, so editing the master never rewrites history.
Tax identity is data too. Section 32 of the CGST Act says a person who isn’t registered shall not collect any amount by way of tax, so when a clinic has no GSTIN, Clinizy Care forces every line to 0% and locks GST entry. That rule once bit us from the other side: the clinic’s GSTIN wasn’t carried into the signed-in session context, so GST-registered clinics briefly couldn’t bill with GST. The lesson: read tax identity from the source of truth, not from a cached session.
How do you get the taxable value right for MRP pricing and discounts?
MRP-inclusive medicines
Pharmacy lines run backwards. Under the Legal Metrology (Packaged Commodities) Rules, 2011, pre-packaged goods declare a Maximum Retail Price inclusive of all taxes, as PIB has summarised. The price is fixed; the tax is carved out of it.
We treat MRP as GST-inclusive: taxable = round(MRP / (1 + rate)), then GST = MRP − taxable. The subtraction matters. Computing tax forward can overshoot: for a ₹40 MRP at 5%, taxable rounds to 3,810 paise, and 5% of that is 190.5, rounding to 191 — ₹40.01 in total, a paisa above MRP. Subtraction gives 190 paise, so taxable plus GST equals MRP by construction and a sale never exceeds MRP.
Bill-level discounts
Section 15(3)(a) of the CGST Act excludes from the value of supply a discount given before or at the time of supply if it’s recorded on the invoice, and rule 46 of the CGST Rules requires the invoice to show taxable value “taking into account discount or abatement, if any”.
A bill-level discount is harder than a line discount because lines carry different rates. We allocate it pro-rata across lines before tax, and the last line absorbs any remainder: ₹100 over three equal lines becomes 3,333, 3,333 and 3,334 paise. Where the discount lands changes the tax. On a ₹1,000 bill with a ₹500 exempt consultation and a ₹500 line at 18%, a pro-rata ₹100 discount cuts the taxable line to ₹450 and its tax to ₹81.
To be straight about it: an earlier version of our engine calculated tax on the pre-discount value — ₹90 instead of ₹81 in that example. An internal audit caught it, and we fixed it.
How should invoice numbers, dates and financial years work?
Rule 46(b) requires an invoice serial number that is unique for a financial year. The engineering corollary: never count existing bills and add one, because two people billing at the same moment read the same count.
In Clinizy Care, bill numbers are per clinic and per IST calendar month (YYYYMM-NNNN), generated by an atomic counter inside the same MongoDB transaction that creates the bill, so the counter and the bill commit or roll back together. The GST invoice number — GSTIN, financial year and bill number — is assigned only when the bill is finalized. Per-clinic counters are one place tenancy shows up in billing; we covered the wider design in how we built a multi-tenant HMS for Indian clinics.
Dates are the other half. Midnight in India is 18:30 UTC, so for five and a half hours the server’s UTC clock still reads the previous day. An earlier version of our engine used UTC, so bills raised in that window could land on the wrong day — and on the night of 31 March, in the wrong financial year, the very unit rule 46 scopes invoice numbers to. We now compute the financial year and every day and month bucket explicitly in IST.
Want to build this — the right way?
Brynex Labs designs and ships production-grade AI agents, automation, and software for teams in India and worldwide. Book a free scoping call and we'll tell you honestly what's worth building — and what isn't yet.
What should happen after a bill is finalized?
Nothing that changes it. Finalized bills in Clinizy Care can’t be edited. Cancelling is an atomic, race-safe void that computes the refund due from the live amount paid, not from a figure that may have gone stale on someone’s screen. Refunds are recorded on the bill and feed the collections and refund reports.
Payments arrive as cash, UPI, card, netbanking and partial payments. The time-based guardrail is a day-close lock, which stops edits to a day once its cash is reconciled, and Clinizy Care’s day-end auto-reconciliation automation closes the day with a collection summary.
PDF invoices are rendered by a background worker with a UPI payment QR code, and a content fingerprint re-renders a stored PDF if the bill changed. Clinic-branded PDFs are a Pro-plan feature in Clinizy Care’s plans, and accountants get a Tally XML export. Bills also carry patient names and addresses; our DPDP Act guide for health-tech builders covers that side.
The honest takeaway
None of this needed clever maths. It needed firm decisions made early — integers for money, rates on the master, one function for place of supply, taxable value before tax, IST for dates, immutability after finalization — and the discipline to hold them. The bugs in this post, from pre-discount tax to UTC day buckets to a GSTIN missing from the session, all came from computing with the wrong input, not from misreading the law.
If you’re building invoicing for Indian healthcare, or any Indian B2C billing, that’s the work of our AI-native software engineering practice, and our healthcare software engineering page covers what we build for clinics and hospitals. To see the billing engine in context, read how we built Clinizy Care.
Technologies Covered
Written by
Abhi PandeyFounder & CTO
Abhi Pandey is the Founder & CTO of Brynex Labs. He leads product and engineering for Clinizy Care, the hospital management software Brynex Labs builds and runs for India's clinics and nursing homes, and for the AI agents and SaaS platforms the studio builds for clients. He writes about multi-tenant SaaS architecture, applied AI engineering, and shipping reliable systems to production.
Read Next
How We Built a Multi-Tenant HMS for Indian Clinics
We run Clinizy Care on one shared MongoDB database with shared collections: every tenant-owned document carries a tenantId, and a Mongoose plugin injects that filter into every read, write, count and aggregation, failing closed when no tenant is in context. At ₹1,999 a month for a solo clinic, a database per tenant would cost and operate out of proportion; a strict, tested guard with tenant-first indexes fits the economics.
WhatsApp Business API at Clinic Scale: What We Learned
The API call is the easy part; the system around it is the work. Sending clinic messages over the WhatsApp Cloud API for many clinics from one SaaS comes down to a sender model with a safe fallback, utility-only templates in each language you support, a queue with dead-letter handling, platform-enforced guardrails and atomic quotas, and webhooks verified against the raw request.
The DPDP Act for Health-Tech Builders: What We Built, What's Next
For health-tech builders, the DPDP Act, 2023 and the DPDP Rules, 2025 turn privacy into a product spec: itemised notices, consent as easy to withdraw as to give, minimum security safeguards with logs kept for a year, a detailed breach report to the Board within 72 hours, and working access, correction and erasure requests. The core obligations commence in May 2027. In Clinizy Care we have built India hosting, tenant isolation, role-based access, two-factor sign-in, an append-only audit trail and clinic-level export and deletion; patient-level consent, individual rights requests, field-level encryption and read auditing are next.