Vibe Code an Invoice Generator (Money Maths Done Right)

Build invoicing with AI — PDF generation, sequential numbering, and the floating-point rounding bug that quietly corrupts every total.

C
CodeIllusion Team
#vibe-coding #invoicing #saas #tutorial
Vibe Code an Invoice Generator (Money Maths Done Right)

Invoicing is where a subtle bug costs actual money. Not dramatically — an invoice will be off by a cent, then a few cents, and someone will eventually reconcile against a bank statement and find the discrepancy.

The cause is almost always the same, and it’s in the first line of code AI writes.

The Bug: Floating Point

Ask for an invoice generator and you’ll get this:

const subtotal = items.reduce((s, i) => s + i.quantity * i.unitPrice, 0)
const tax = subtotal * 0.20
const total = subtotal + tax

Looks fine. Isn’t.

> 0.1 + 0.2
0.30000000000000004

> 19.99 * 3
59.970000000000006

Binary floating point can’t represent most decimal fractions exactly. Across a few line items with tax, the error surfaces as a total that’s a cent off — and it varies depending on the order things were added.

The fix:

Store all monetary values as integers in the smallest currency unit — cents. Never use JavaScript numbers for money arithmetic. Use Dinero.js for calculations. Convert to a display string only at render time.

Database columns for money must be BIGINT storing cents, or NUMERIC(19,4). Never DOUBLE PRECISION or REAL.

Rounding Rules

Once you’re in integers, rounding still needs a decision — and it must be made once, consistently.

Calculate tax per line item, rounding each to the nearest cent with half-up rounding, then sum. Do not calculate tax on the subtotal.

Document the rounding rule in a comment and apply it identically everywhere.

Per-line versus on-subtotal produces different totals. Neither is wrong, but mixing them within one system means your invoice total won’t match the sum of its own lines — which is the discrepancy accountants notice.

Invoice Numbers

Most tax authorities require sequential numbering with no gaps. The naive implementation breaks under concurrency:

const last = await db.invoice.findFirst({ orderBy: { number: 'desc' } })
const next = last.number + 1        // ← two requests get the same number

Generate invoice numbers in a database transaction using a dedicated counter row with SELECT FOR UPDATE, or a Postgres sequence per issuing entity. Numbers must be sequential with no gaps, never reused, and unique per entity.

Cancelled invoices keep their number and are marked void — never delete an invoice or reuse its number.

That final rule is a legal requirement in most jurisdictions, and it’s the opposite of what a normal CRUD delete does.

The Build

Next.js App Router, TypeScript, Tailwind, Supabase.

Tables:

  • clients: id, user_id, name, email, address, tax_id
  • invoices: id, user_id, client_id, number, issue_date, due_date, status, currency, subtotal_cents, tax_cents, total_cents, notes
  • invoice_items: id, invoice_id, description, quantity NUMERIC, unit_price_cents BIGINT, tax_rate NUMERIC, position

All money as BIGINT cents. Quantity as NUMERIC to allow fractional hours.

Build a create/edit form with dynamic line items, live totals, and a preview matching the final PDF exactly.

Quantity as NUMERIC matters for anyone billing 2.5 hours.

PDF Generation

Generate PDFs server-side by rendering an HTML template and converting with Playwright. The HTML must be the same template used for the on-screen preview, so they can’t drift.

Include: business details and logo, client details, invoice number, issue and due dates, line items with quantity, unit price, tax rate and line total, subtotal, tax breakdown by rate, grand total, payment terms, and bank details.

A4 with proper print margins. Multi-page invoices must repeat the header and column titles on each page.

Multi-page handling is skipped by default, and a ten-item invoice that spills onto page two with no column headings looks broken.

Delivery and Status

Email invoices as a PDF attachment via Resend, with a link to a public view page at a signed, unguessable URL.

Status flow: draft → sent → paid, plus overdue (derived from due date) and void. Only draft invoices are editable. Editing a sent invoice must create a credit note instead.

Editing a sent invoice is the other thing that breaks tax compliance — once it’s issued, corrections happen through a credit note, not by changing history.

Log every status change with a timestamp for the audit trail.

Multi-Currency, If You Need It

Store the currency code on each invoice. Never convert between currencies for display. If exchange rates are needed, store the rate used and the date it applied on the invoice itself, and never recalculate historical invoices with current rates.

An invoice is a record of what was charged. Recalculating it later changes history.

Test the Money

These catch the bugs that matter:

  • Invoice with 3 items at 19.99 × 3 — total exactly 59.97?
  • Item priced 0.1, quantity 3 — exactly 0.30?
  • Multiple tax rates on one invoice — breakdown sums to total tax?
  • Sum of line totals equals subtotal exactly, no cent drift
  • Two invoices created simultaneously — different numbers?
  • Void an invoice — number retained, not reused?
  • 30-item invoice — PDF paginates with repeated headers?
  • Fractional quantity (2.5 hours) calculates correctly

The Checklist

  • All money as integer cents or NUMERIC, never float
  • Dinero.js or equivalent for arithmetic
  • Rounding rule chosen, documented, applied consistently
  • Invoice numbers generated in a transaction
  • No gaps, no reuse, void instead of delete
  • Sent invoices immutable, credit notes for corrections
  • PDF template shared with preview
  • Multi-page headers repeat
  • Status changes logged
  • Public invoice URLs signed and unguessable
  • Ownership filters on every query

Where to Stop

Invoicing and record keeping are well within what you can build this way. Tax calculation — which rate applies, to whom, in which jurisdiction, with which exemptions — is not a coding problem, and an AI’s confident answer about VAT treatment is worth nothing.

Build the tool. Let an accountant tell you what the numbers should be.

Related reading:

Frequently Asked Questions

How should I store money in a database? +

As integers in the smallest currency unit — cents, not dollars — or as Postgres NUMERIC. Never as a float. Floating point can't represent most decimal fractions exactly, so totals drift by small amounts that compound across line items.

What's the best way to generate PDFs from a web app? +

Render HTML and convert it server-side with Playwright or Puppeteer. You get full CSS control and the PDF matches the on-screen preview. Client-side libraries like jsPDF produce noticeably worse typography and layout.

How do invoice numbers need to work? +

Sequential with no gaps, per issuing entity, and never reused. Most tax jurisdictions require this. Generate the number in a database transaction with a lock — generating it in application code produces duplicates under concurrency.

Can I use AI to build accounting software? +

For invoicing and record keeping, with review of the money maths. For anything involving tax calculation or filing, get professional advice — rules vary by jurisdiction and being wrong has consequences an AI can't be accountable for.

Tagged:

#vibe-coding #invoicing #saas #tutorial

Enjoyed this article?

Get more AI tool picks, coding tutorials, and no-code automation guides every week. No spam, ever.

Found this useful? Share it:

More in AI Coding Tools