Shipmind Labs

The net totals matched. The VAT line did not, off by one minor unit, on a handful of documents in a batch.

We had computed it the way arithmetic suggests: sum the line nets, apply the rate once, round once. One rounding step, smallest error available. The reviewer on the other side computed it the way the document actually gets read, VAT on each line, rounded at each line, then summed.

Both are defensible. Only one of them is what the recipient's system reproduces, and we had never decided which one we were. The choice sat scattered across a serializer, a PDF template and a totals helper, each quietly rounding at a different moment.

The cent was not the cost. The cost was a day of three people arguing about which number was correct, with no single place in the code that owned the answer.

So in our open-source invoicing library, euinvoice, the decision has exactly one home.

A line's net is unit price times quantity, rounded. A line's VAT is that net times the rate, rounded again. Rounding happens at the currency's minor unit (zero decimals for JPY-type currencies, three for BHD and TND, two otherwise), half-up, every time, with no per-call override.

Then the part that does the real work: an invoice has no total fields. Net, VAT and gross are properties that sum the lines. The VAT breakdown is a property too, bucketed by category and rate in first-appearance order, each row carrying its own gross. The breakdown equals the totals by construction, because both are the same sum read twice. There is no setter to disagree with.

The same instinct runs through the money type. Floats and bools get rejected at the door, not coerced. Every add, subtract and compare checks the currency first and raises on mismatch. No conversion function exists, so nothing silently crosses currencies. An invoice refuses to exist with mixed currencies, with a due date before its issue date, or with reverse charge sitting next to taxable lines.

The lesson we keep reapplying: when a number appears in two places, it is a question of policy rather than arithmetic, and policy belongs in one function instead of three.

If you have shipped invoicing across currencies, you have probably had this argument already, and we would like to hear whether you rounded per line or on the total, and whether anyone downstream read it differently.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com