Tax engine
How Eziseller currently handles tax on orders, drafts, and invoices. Audience: a new dev landing on a tax-related bug or feature.
1. Overview
Eziseller has no centralized tax engine today. Tax is stored as a flat taxAmount (Float) on Order, DraftOrder, and OrderItem, and a single default taxRate (Float) on Store. The value is accepted as input from the client (order form, draft intake, API) and passed through to persistence and the invoice PDF without server-side recalculation. There is no rate table, no inclusive/exclusive toggle, no jurisdiction logic, and no HSN/GSTIN handling on Product or Store. The gstAmount field exists only on ShippingRate (Shiprocket-returned value, not applied to the order total).
Treat this doc as a map of where tax touches the system, not a spec of a tax engine — because one does not yet exist.
2. Architecture
Tax is a pass-through number. Every entry point sets it independently — there is no shared helper.
3. Data model
Relevant fields — see schema.prisma:
Store.taxRate— schema.prisma:L93Order.taxAmount— schema.prisma:L412DraftOrder.taxAmount— schema.prisma:L486OrderItem.taxAmount— schema.prisma:L552ShippingRate.gstAmount— schema.prisma:L795 (shipping quote, not order tax)
There is no hsn, gstin, taxInclusive, cgst/sgst/igst, or jurisdiction field on any model on this branch.
4. Key flows
4.1 Manual order creation
Computation (literal): totalAmount = subtotal + taxAmount + shippingAmount - discountAmount — order-creation-service.ts:L473. No per-item rate application, no rounding step, no inclusive/exclusive branch.
4.2 Public checkout
public-catalog.ts hard-codes taxAmount: 0 on both the order insert and fallback paths — public-catalog.ts:L520, public-catalog.ts:L577. Customers checking out through a catalog pay zero tax regardless of Store.taxRate.
4.3 Draft → order conversion
Drafts store taxAmount verbatim from intake (draft-order-service.ts:L261) and the convert-to-order path carries it forward. Draft creation itself comments // For now, no tax or shipping — draft-order-service.ts:L34, mirrored in order-creation-service.ts:L66.
4.4 Invoice rendering
invoice-template.ts prints a "TAX" column per item but hard-codes each row's value to 0 — invoice-template.ts:L268. The order-level taxAmount appears in the totals block only when > 0 — invoice-template.ts:L309. Header reads "Receipt / Tax Invoice" regardless of whether any tax was charged — invoice-template.ts:L102.
5. Lifecycle / state machine
Tax has no state of its own — it rides on the Order lifecycle. Once an order is persisted, taxAmount is only re-evaluated if an update mutates it, in which case totalAmount is recomputed via the same additive formula — order-creation-service.ts:L785-L788, order-creation-service.ts:L870-L876.
6. Key files
- backend/prisma/schema.prisma:L93,L412,L486,L552 — all tax-related columns
- backend/lib/validation.ts:L143,L162 — Zod
taxAmount: z.number().min(0).default(0)on order + item schemas - backend/lib/order-creation-service.ts:L468-L788 — total recompute on create/update
- backend/lib/draft-order-service.ts:L34,L261 — draft intake + convert
- backend/routes/public-catalog.ts:L520,L577 — checkout hard-codes
0 - backend/routes/store.ts:L53-L229 —
Store.taxRateCRUD (stored, never auto-applied) - backend/routes/orders.ts:L76,L201 —
taxAmountreturned in order responses - backend/routes/documents.ts:L11 — invoice payload includes
taxAmount - backend/templates/invoice-template.ts:L190-L309 — PDF rendering
- backend/lib/pdf.ts — PDFKit wrapper (no tax logic of its own)
7. Env vars & config
None. Tax is data-driven (per-order input) and Store.taxRate, not env-driven.
8. Gotchas & troubleshooting
- No single source of truth. Tax is set independently in at least four places (order creation, order update, draft intake, public checkout). Changing the formula requires touching all of them.
Store.taxRateis dead config. It is stored and returned bystore.tsbut no code path reads it to compute tax. UI forms collecttaxAmountdirectly from the user.- Public checkout always charges zero tax — hard-coded, not a bug in the totals math. See public-catalog.ts:L520.
- Per-item
OrderItem.taxAmountis effectively unused. Schema allows it, validation accepts it (validation.ts:L162), but order creation writestaxAmount: 0on each item (order-creation-service.ts:L542 neighbourhood) and the invoice template prints0per row regardless. - No rounding contract. Values are summed as
Float. Floating-point drift betweensubtotal + taxAmount + …and what the client displayed is possible but unguarded. ShippingRate.gstAmountis not order tax. It is a Shiprocket quote field and never flows intoOrder.taxAmount.- Invoice header says "Tax Invoice" unconditionally — may be misleading for zero-tax orders in jurisdictions with legal definitions of that phrase.
9. Extension points
If you need real tax logic today, the least-invasive path is:
- Add a helper (e.g.
backend/lib/tax.ts) exportingcomputeTax({ items, store, customer }). - Call it from the four entry points listed in §4 — replacing the hard-coded
0inpublic-catalog.tsand the client-supplied value in the order services. - Add HSN / GSTIN / inclusive fields to
ProductandStorevia a migration before the helper can do anything jurisdiction-aware.
Do not add tax math inside invoice-template.ts — it must stay a pure renderer of persisted fields.
10. Planned: centralized tax-service (not on this branch)
The repo's git status at the time of writing lists three untracked paths that sketch a future tax subsystem:
backend/lib/tax-service.ts— intended centralized calculatorbackend/prisma/migrations/20260411171832_add_tax_invoice_system/— schema migration for the tax + invoice-number worksrc/components/dashboard/settings/TaxSettingsTab.tsx— store-level tax configuration UI
None of these files exist on disk on this branch despite appearing in git status — they were drafted and reverted or never committed. Treat this section as a forward pointer, not a description of working code. Related planned work: see invoice-numbering.md (also pending — backend/lib/invoice-number-service.ts is similarly listed-but-absent).
11. Related docs
- invoice-numbering.md — planned invoice-number sequencing
- pdf-generation.md — how invoices are rendered
- ../catalog/checkout.md — public checkout path (where tax is forced to zero)
- ../orders/order-creation-manual.md — manual order entry (where
taxAmountoriginates)