Skip to content

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..50 with 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.

  • PREFIXGST_INVOICE_PREFIX env var, defaults to INV
  • FY — India’s financial year (April 1 – March 31), compacted from "2026-27" to "2627"
  • Sequence — a per-financial-year counter, starting at 000001 each 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 groupContents
HeaderInvoice Number, Invoice Date, Order ID
SellerLegal Name, GSTIN, State
BuyerName, Address, State
Per line itemDescription, HSN code, Quantity, Unit Price, Taxable Value, tax breakdown (CGST/SGST/IGST — code, rate, amount), Line Total
TotalsCurrency, Item Subtotal, CGST Total, SGST Total, IGST Total, Shipping Total, Grand Total
Grand Total in Wordse.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 1507 and the 5% GST rate — taken as given from the brand’s own asset package, not independently verified as legally correct. See docs/gst-assumptions.md in 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/pdf and GET /store/orders/:id/gst-invoice/pdf return 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/