@devx-retailos/invoice
v0.1.2
Published
GST-compliant PDF invoice generation for retailOS POS backends.
Keywords
Readme
@devx-retailos/invoice
GST-compliant PDF invoice generation for Medusa v2 POS backends: assembles invoice data from a POS order, computes CGST/SGST/IGST breakdowns, and renders an A4 PDF (or HTML) via a pluggable renderer. Also persists tax invoices and credit notes with gapless, per-store sequential numbering.
Part of retailOS, a Medusa v2 SDK for offline-store POS systems. Each @devx-retailos/* package is an independently installable Medusa plugin; a brand backend composes the ones it needs in medusa-config.ts.
Installation
npm install @devx-retailos/invoicePeer dependencies: @medusajs/framework, @medusajs/medusa, and @medusajs/admin-shared ^2.15.0. puppeteer (^22.0.0) is an optional peer — the default PDF renderer requires it; skip it if you register a custom renderer or only serve HTML.
Requires @devx-retailos/order in the same backend (invoices are assembled from its orders). @devx-retailos/store-details is optional but recommended — it supplies the seller block (address, GSTIN, state code); without it those fields render empty.
Setup
// medusa-config.ts
module.exports = defineConfig({
plugins: [
{ resolve: "@devx-retailos/order", options: {} },
{ resolve: "@devx-retailos/store-details", options: {} },
{ resolve: "@devx-retailos/invoice", options: {} },
],
})The module registers under the key retailos_invoice (exported as INVOICE_MODULE).
Usage
import { INVOICE_MODULE } from "@devx-retailos/invoice"
const invoices = container.resolve(INVOICE_MODULE)
// Render a PDF for an order
const pdf: Buffer = await invoices.renderInvoice("order_01...", {
organization_id: "org_...",
store_id: "store_...",
customer_state_code: "27", // optional — drives intra/interstate GST split
employee_name: "Cashier name", // optional — printed on the invoice
})
// Or get the structured data without rendering
const data = await invoices.assembleInvoiceData("order_01...", { organization_id: "org_..." })Persisted documents & gapless numbering
Beyond on-the-fly rendering, the module persists tax invoices and credit notes with gapless, per-store sequential numbers (backed by an atomic per-(store, document_type) counter — numbers are never skipped or duplicated, even under concurrency).
// Permanent tax invoice
const invoice = await invoices.issueInvoice({
organization_id: "org_...", store_id: "store_...", order_id: "ord_...",
subtotal: 1000, tax_total: 180, grand_total: 1180, currency: "INR",
uc_invoice_reference: "UC-INV-...", // optional — reconcile against Unicommerce
})
// invoice.invoice_number → "INV-000001"
// Credit note for a return — reverses the returned lines (value + tax), points at the invoice
const creditNote = await invoices.createCreditNote({
organization_id: "org_...", store_id: "store_...", return_id: "ret_...",
amount: 1900, tax_amount: 290, currency: "INR",
reversed_lines: [{ sku: "TEE-BLK-M", quantity: 1, amount: 1900 }],
original_invoice_number: "INV-000001",
})
// creditNote.credit_note_number → "CN-000001"@devx-retailos/order calls createCreditNote automatically when a return completes. The original invoice is never edited; an exchange produces two clean documents (a credit note for the item coming back, a new invoice for the item going out), never a single "difference" invoice.
GST notes
- Per-line GST rate and HSN code are read from the order line item's
metadata.gst_rateandmetadata.hsn_code. computeGstBreakdown(taxableAmount, gstRate, isIntrastate)splits tax into CGST + SGST (intrastate) or IGST (interstate). The sale is treated as intrastate when the customer state code is unknown or equals the store's state code.formatInvoiceNumber(orderId, createdAt, override?)prefersorder.metadata.invoice_number, falling back toINV-{YYYYMMDD}-{last 8 of order id}.amountToWords(1250.5)→"One Thousand Two Hundred Fifty Rupees and Fifty Paise Only"(Indian numbering: Lakh/Crore).
These helpers, plus renderInvoiceHtml(data), are exported for direct use.
Extension points
The default PuppeteerInvoiceRenderer launches headless Chromium and prints the HTML template to A4. To use a different PDF engine (or a hosted rendering service), implement InvoiceRenderer and register it at bootstrap:
import type { InvoiceRenderer, InvoiceData } from "@devx-retailos/invoice"
const myRenderer: InvoiceRenderer = {
async render(data: InvoiceData): Promise<Buffer> {
// produce a PDF buffer however you like
return buffer
},
}
invoices.setRenderer(myRenderer)Permissions
Exported as INVOICE_PERMISSIONS from @devx-retailos/invoice/permissions:
| Key | Description |
| --- | --- |
| order.invoice | Generate and download GST-compliant PDF invoices for orders |
| order.credit_note | Issue credit notes against returns |
API routes
| Method | Path | Purpose | Permission |
| --- | --- | --- | --- |
| GET | /admin/retailos/orders/:id/invoice | Download the invoice. Query: organization_id (required), store_id, customer_state_code, employee_name, format=pdf\|html (default pdf). | order.invoice |
Returns 404 if the order doesn't exist and 503 if no PDF renderer is available (puppeteer missing and no custom renderer registered).
Errors
All extend RetailOSError from @devx-retailos/core (also importable from @devx-retailos/invoice/errors); switch on err.code:
RETAILOS_INVOICE_NOT_FOUND— no order found for the given id.RETAILOS_INVOICE_RENDERER_NOT_AVAILABLE— puppeteer not installed and no custom renderer registered.RETAILOS_INVOICE_RENDER_FAILED— the renderer threw; original error oncause.
Related packages
@devx-retailos/core— shared types,RetailOSError,Logger, permission registry.@devx-retailos/order— the POS orders invoices are generated from.@devx-retailos/store-details— store address/GST metadata printed on the invoice.@devx-retailos/rbac— roles and permission checks enforced by the invoice route.
License
MIT
