Core
Tax And Invoices
Invoice Numbering

Invoice numbering

How Eziseller assigns human-readable identifiers to tax invoices. Audience: new dev exploring how invoices are labelled on PDFs and in audit logs.

1. Overview

Eziseller does not have a dedicated invoice-numbering subsystem today. There is no Invoice model, no sequence counter, and no invoiceNumber column anywhere in the schema. Every PDF invoice is labelled by reusing the parent Order.orderNumber and prefixing it with the string INV- at render time inside the PDF template. The invoice-number-service.ts file referenced in scratch notes does not exist on disk — it is planned work, not shipped code.

This means the "invoice number" is really just a presentation-layer decoration of the order number. Any uniqueness, monotonicity, or fiscal-year reset behaviour has to be inherited from how order numbers themselves are generated.

2. Architecture

The PDF template is the only place the INV- prefix is introduced. The database stores only the raw order number and a Cloudinary URL pointing at the last-uploaded PDF.

3. Data model

There is no Invoice, InvoiceSequence, InvoiceCounter, or FiscalYear model. See schema.prisma:L402-L422 for Order and schema.prisma:L725-L727 for DraftOrder.invoiceUrl.

4. Key flows

4.1 Order number assignment (the upstream source of truth)

WhatsApp and Instagram flows use their own generators (WA- / IG- prefixes) in order-creation-service.ts:L179 and draft-order-service.ts:L359, which retry on unique-constraint collision.

4.2 Invoice rendering

The download endpoint streams the PDF directly and does not persist invoiceUrl. Persistence only happens through document-service.ts:L12 (generateAndUploadInvoice), which is called from other flows (e.g. messaging) but not from the on-demand download route.

5. Lifecycle / state machine

Invoice "identity" has no independent state — the invoice is recomputed from the order every time.

6. Key files

7. Env vars & config

VarRequiredPurposeWhat breaks
CLOUDINARY_*yes (for upload path)Stores generated PDFs under documents/invoicesgenerateAndUploadInvoice throws; download route still works
No invoice-numbering env vars exist (no prefix, no fiscal-year, no starting-number config)

8. Gotchas & troubleshooting

  • No real invoice number: The INV- string is hard-coded in the template. You cannot configure a per-store prefix, a fiscal-year segment (e.g. INV/2025-26/0001), or a separate series for credit notes. Indian GST requires a monotonic per-FY series — Eziseller does not satisfy this today.
  • Count-based order numbers are racy: prisma.order.count({ where: { userId } }) + 1 in routes/orders.ts:L390 is not atomic. Two concurrent POST /api/orders can both read the same count and both try to insert ORD-<ts>-0042. The unique constraint on Order.orderNumber will reject one of them and the user gets a 500 — there is no retry loop here (unlike the messaging-side generators).
  • No uniqueness across stores for the visible number: orderNumber is unique globally, but the human-facing INV-ORD-<ts>-0042 leaks timestamp and per-user count. Two different stores will both see counts starting at 1, so their INV- strings look similar but are not actually collision-free across the table (the timestamp disambiguates in practice).
  • Re-generating the PDF does not change the number: Because the "number" is derived, regenerating after fixing a typo produces the same INV- label — good for idempotency, bad if you need to void-and-reissue with a new legal number.
  • invoiceUrl is stale after re-upload: generateAndUploadInvoice uses public_id: invoice-<orderNumber>-<Date.now()>, so each upload creates a new Cloudinary asset. The column is updated by the caller, not by the service; if the caller forgets, Order.invoiceUrl points at an older render.
  • Missing files from git status are not shipped: backend/lib/invoice-number-service.ts and the 20260411171832_add_tax_invoice_system migration appear in git status as untracked but are not present on disk in the working tree. Do not assume they exist when reading this doc.

9. Extension points

A planned centralised InvoiceNumberService should own:

  1. A per-store InvoiceSequence table keyed by (storeId, fiscalYear, series) with a nextNumber column incremented inside a transaction using SELECT ... FOR UPDATE (or Prisma's $transaction + update with version check).
  2. A configurable format string per store (e.g. {{prefix}}/{{fy}}/{{seq:6}}).
  3. Separate series for invoices, credit notes, and proforma.
  4. Writing the resolved number to a new Order.invoiceNumber column (distinct from orderNumber) so the label is stable even if orderNumber changes.

Until that lands, treat the INV- label as cosmetic and never rely on it for statutory reporting.

10. Related docs