Invoices look like elementary school arithmetic until you support 150+ currencies, discounts, shipping, and stacked tax lines. Then floating point shows up and your PDF disagrees with the editor by a cent — which is enough for a client’s AP department to bounce the document.
The classic bug
JavaScript (and many naive PHP casts) compute 0.1 + 0.2 as something that is not quite 0.3. Multiply that by quantities, tax rates, and currency decimal places (JPY is not USD), and you get totals that “look fine” until you format them twice — once for the UI and once for DomPDF.
Rules we follow
- Store major units carefully — decide early whether you persist decimals as strings/integers and stick to it.
- Round at display boundaries — round line extensions and tax amounts according to currency rules before summing for the invoice total users see.
- One total function — editor preview and PDF must call the same calculation path. Duplicating formulas in React and Blade is how silent drift starts.
- Currency metadata — decimal digits, symbol position, and separators come from data, not hard-coded assumptions about “always two decimals.”
Taxes make it worse (on purpose)
Inclusive vs exclusive tax changes the order of operations. If you discount before tax in one place and after tax in another, you have two products. We keep a deterministic order documented in code comments and mirrored in the UI so users can explain the invoice to their accountant.
Practical test cases worth keeping
- Quantity 3 × unit 33.33 with 10% tax.
- Zero-decimal currency with a percentage discount.
- Shipping + multiple tax rates on different lines.
- Partial amount paid that should not invent negative balances from rounding.
If those four survive a refactor, you are in better shape than most early invoicing tools.
Related reading: our tax schema notes and the PDF generation pipeline.