The Patch Bump That Fixed a Bug and Broke Our Invoices
A Renovate auto-merge bumped a transitive dependency from 6.2.1 to 6.2.2. The patch fixed rounding behavior. Our invoice calculations disagreed.
The support ticket said "invoice total is wrong." That was it. No screenshot, no invoice number, no details about what "wrong" meant. The kind of ticket that sits in the backlog for a day because nobody knows what to do with it.
Then three more came in over the next hour. Different customers, same complaint. One of them included a spreadsheet showing their own calculations next to ours. The difference was $0.03 on a $4,200 invoice.
Three cents. In a better world, we'd have caught it before any customer did.
Finding the discrepancy
The client was a B2B SaaS company that handled procurement for mid-size manufacturers. Their platform generated thousands of invoices a month — line items with quantities, unit prices, tax rates, and discount tiers. The kind of math where you're multiplying $47.83 × 0.0725 and need the result to be exactly what the customer's accounting software expects.
I started by pulling the four disputed invoices and running the calculations by hand. The line items were correct. The tax rates were correct. But the per-line rounding was off on about 40% of the rows. Sometimes our total was a penny higher, sometimes a penny lower. It averaged out on small invoices, but on a 50-line purchase order, the pennies added up.
The code hadn't changed in weeks. The last deploy touching the invoice module was 19 days ago, and it was a copy change in the email template. So I did what I always do when behavior changes without code changes: I checked the dependencies.
The Renovate PR nobody read
The team used Renovate with auto-merge enabled for patch versions. Their config looked like this:
{
"extends": ["config:base"],
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true
}
]
}The reasoning was solid on paper. Patch versions are bug fixes. Semver says they're backwards-compatible. Auto-merging them keeps the noise down so developers can focus on minor and major bumps that actually need review. In practice, this meant about 15-20 PRs a week merged with no human eyes on them.
I pulled up the Renovate PR log and started correlating timestamps. Sixteen days ago — three days before the first customer complaint — a PR had bumped big.js from 6.2.1 to 6.2.2 as a transitive dependency of their currency formatting library. The PR had a green CI check, zero comments, and was merged by the bot at 3:12 AM.
What actually changed
The big.js 6.2.2 changelog listed one item: "Fix: toFixed() rounding for midpoint values." In version 6.2.1, toFixed(2) on the value 2.455 returned "2.45". In 6.2.2, it returned "2.46".
The old behavior was arguably a bug — it was truncating rather than rounding at the midpoint. The new behavior was mathematically correct round-half-up. The problem was that every invoice the platform had ever generated used the old behavior, and so did the reconciliation logic, and so did the tax calculation that fed into downstream accounting systems.
The "fix" was correct. The invoices were now, technically, more accurate. But they didn't match what customers expected, what the tax calculations produced, or what the audit trail showed for historical invoices.
Here's what the difference looked like in practice:
// big.js 6.2.1
new Big('2.455').toFixed(2) // "2.45"
new Big('1.235').toFixed(2) // "1.23"
new Big('0.845').toFixed(2) // "0.84"
// big.js 6.2.2
new Big('2.455').toFixed(2) // "2.46"
new Big('1.235').toFixed(2) // "1.24"
new Big('0.845').toFixed(2) // "0.85"Roughly half of midpoint values rounded differently. On any given line item, it was a penny. Across a 50-line invoice, it could be 10-20 cents. Not enough to trigger any of their existing alerts, but enough for a customer's accounting team to flag.
The fix and the fallout
The immediate fix was pinning big.js to 6.2.1. That took five minutes. The fallout took two weeks.
They had to identify every invoice generated in the 16-day window — roughly 3,400 invoices — and determine which ones had rounding differences. About 1,400 did. Of those, 200 had already been paid against the "wrong" totals. The accounting team had to issue corrections for 47 invoices where the difference exceeded their materiality threshold, and send explanatory emails to every affected customer.
Total financial impact was around $14,000 in staff time and customer credits. For a three-cent rounding change in a patch version.
What we changed
Pinning the dependency was a band-aid. Here's what actually mattered:
We killed auto-merge for anything touching the invoice path. Not all auto-merge — that would defeat the purpose. But any dependency used in financial calculations got pulled out of auto-merge and into a weekly manual review. It's six packages. Someone spends 20 minutes a week looking at changelogs. Cheap insurance.
We added snapshot tests for invoice calculations. Not unit tests with a handful of hardcoded values — those existed and passed through the entire incident. We generated 500 invoices from production-like data and stored the expected outputs. Any dependency change that shifts a penny in any of those 500 invoices breaks the build. It's slow and ugly and I don't care.
We stopped treating semver as a contract. Semver is a communication tool, not a guarantee. The big.js maintainer did nothing wrong — they fixed a genuine bug and bumped the patch version. But "bug fix" and "breaking change" aren't mutually exclusive when your system depends on the buggy behavior.
Warning
The uncomfortable truth
The team had 87% test coverage. They had CI running on every PR. They had Renovate configured exactly the way the docs recommended. They did everything "right," and a three-cent rounding change still slipped through and ran undetected for over two weeks.
The gap wasn't in their tooling. It was in their mental model. They treated dependencies like stable ground, when really they're someone else's code that changes on someone else's schedule for someone else's reasons. Patch versions don't break things — until they do, and by then you've generated 3,400 invoices with the wrong math.
I've started asking every new client the same question: which of your dependencies would hurt the most if its behavior changed by one penny? Most of them don't have an answer. That's the problem.