16. Invoicing
[← Refunds & Returns]/15-refunds-and-returns/ · [Index]/00-index/
16. Invoicing
Verified against: services/medusa/src/modules/gst-invoice (models, service, PDF renderer —
source-read directly), packages/gst/src/invoice-number.ts and financial-year.ts,
services/medusa/src/workflows/steps/validate-seller-gst-config.ts,
services/medusa/src/api/{admin,store}/orders/[id]/gst-invoice*, and .ai/features/gst.md.
INFO — unlike most of this wiki, invoicing is a real, custom-built feature, not just default Medusa. This project added its own GST invoice/credit-note module on top of Medusa. It’s also one of the more thoroughly tested parts of the system — its sequence-numbering guarantee has a permanent regression test (50 concurrent invoice creations asserted to produce numbers
1..50with zero duplicates/gaps).
When an invoice is generated
Automatically, the moment an order is placed — an order.placed event triggers it. There
is no manual “create invoice” step or button anywhere; see the correction already noted in
[Section 9]/09-order-processing-sop/#7-invoice.
IMPORTANT — fails loud, not silently: if the seller’s GST config (
GST_SELLER_GSTIN/GST_SELLER_LEGAL_NAME/GST_SELLER_STATE_CODE) is missing or unresolvable, or any line item’s product is missing an HSN code, invoice generation refuses to run rather than fabricate a document — the real error text is “Refusing to generate an invoice with fabricated seller details.” The order itself still completes normally either way; only the invoice is affected. If a customer reports a missing invoice, this is the first thing to check with a developer.
Invoice number
Format: {PREFIX}/{FY}/{6-digit sequence} — e.g. INV/2627/000001.
PREFIX—GST_INVOICE_PREFIXenv var, defaults toINVFY— India’s financial year (April 1 – March 31), compacted from"2026-27"to"2627"- Sequence — a per-financial-year counter, starting at
000001each new FY, allocated via an atomic database upsert (verified safe under concurrency, not just assumed — see the intro note above)
INFO: If an invoice generation attempt fails after a number was already allocated (e.g. the PDF render step fails), that number is not reused — the row is kept with
status: "failed"instead of being deleted. This means invoice numbers can have visible gaps on genuine failures. That’s a deliberate, auditable gap, not a bug — a Sales Tax auditor would reasonably want to see that a number wasn’t secretly reassigned to a different transaction.
What’s on the invoice
The invoice is built from a frozen snapshot saved at issue time — later changes to a product’s HSN code, or to the seller’s GST settings, never retroactively change an already-issued invoice’s PDF. The snapshot contains:
| Field group | Contents |
|---|---|
| Header | Invoice Number, Invoice Date, Order ID |
| Seller | Legal Name, GSTIN, State |
| Buyer | Name, Address, State |
| Per line item | Description, HSN code, Quantity, Unit Price, Taxable Value, tax breakdown (CGST/SGST/IGST — code, rate, amount), Line Total |
| Totals | Currency, Item Subtotal, CGST Total, SGST Total, IGST Total, Shipping Total, Grand Total |
| Grand Total in Words | e.g. “Rupees Five Hundred Seventy Six and Forty Five Paise Only” — Indian lakh/crore numbering, INR only (skipped, not mislabeled, for any other currency) |
TO BE CONFIRMED / gap vs. the original brief: the invoice snapshot has no discount field. Since no Promotion/discount has ever actually been used on this store (see [Section 11 — Coupons & Discounts]/11-coupons-and-discounts/), this hasn’t mattered in practice — but if a discount is ever applied to a real order, how it should appear on the invoice hasn’t been designed yet.
The invoice also does not show payment status — it’s a tax document, not a payment receipt. Payment status lives on the order itself (see [Section 8]/08-order-management/#payment-status-values).
System capability vs. business/legal configuration
Per this project’s own standing rule (.ai/project-context.md) — kept distinct here rather than
blurred together:
System capability (real, working):
- CGST+SGST split (same state as seller) vs. IGST split (different state) — calculated automatically from the buyer’s state at checkout
- Atomic, gapless-except-on-real-failure sequential invoice numbering per financial year
- PDF rendering (via
pdfkit, no headless browser — a deliberate choice given the current 1GB Lightsail deployment target), including the Amount in Words line - Immutable snapshot per invoice, unaffected by later data changes
Business/legal configuration (requires real values / professional review — not verified by this wiki):
GST_SELLER_GSTIN— the value currently in local config is a user-confirmed placeholder, not the real GSTIN. Any invoice generated today is not a legally valid tax document until this is replaced.- HSN code
1507and the5%GST rate — taken as given from the brand’s own asset package, not independently verified as legally correct. Seedocs/gst-assumptions.mdin the codebase. - [REQUIRES BUSINESS/LEGAL REVIEW] — a qualified tax professional has not reviewed this invoice format or the CGST/SGST/IGST logic for compliance.
Where to find/download an invoice
WARNING — correcting a workaround suggested earlier in this wiki. [Section 9]/09-order-processing-sop/#7-invoice noted staff has no in-dashboard button and pointed at the raw API route as a fallback. That fallback is not actually usable by pasting the URL into a browser: both
GET /admin/orders/:id/gst-invoice/pdfandGET /store/orders/:id/gst-invoice/pdfreturn the PDF as base64-encoded JSON, never a raw downloadable file — a deliberate choice, since the local file provider’s “private” access setting doesn’t actually restrict access on its own (.ai/features/gst.md). Retrieving a PDF this way requires a small script or API tool, not just a browser address bar.
The only realistic way today for staff to get an order’s invoice is the same one a customer uses:
- The order confirmation page (right after checkout), or
- The customer’s own Account → Orders → (order) page on the storefront — a “Download
Invoice” button there (
apps/storefront/src/modules/order/components/download-invoice-button) calls the store route and triggers a real file download in the browser.
If a customer needs their invoice resent and can’t access their account, there is currently no admin-side self-service option — this is a real product gap, not a documented workaround.
Credit notes (cancellations)
Same module, mirrored behavior — see [Section 15]/15-refunds-and-returns/#cancellation-before-shipment for when this triggers.
- Number format:
{GST_CREDIT_NOTE_PREFIX (default "CN")}/{FY}/{6-digit sequence}— e.g.CN/2627/000001— its own separate counter from invoice numbers (required per GST Rule 53, per the codebase’s own note) - Built from the original invoice’s frozen snapshot, not the canceled order’s live state — a credit note formally reverses what was actually invoiced. If no invoice was ever issued for the order, credit note generation is skipped (logged), never fabricated
- Same PDF renderer, with the header changed and an “Against Original Invoice No: …” line added
- Re-canceling/re-triggering for an already-credited order returns the existing credit note, not a duplicate (verified, not assumed)
- Same download pattern on the storefront (
download-credit-note-button), same base64-JSON API shape and same “no admin button” gap as invoices above
[← Refunds & Returns]/15-refunds-and-returns/ · [Index]/00-index/ · [Next: Reports →]/17-reports/