On a PDF, VAT fits in two columns: a rate and an amount. When the rate is zero, you write the reason at the bottom of the page, in plain language, and whoever receives the invoice understands. In a structured electronic invoice, that line of text does not exist. You have to declare a regime code, and if that regime charges no VAT, a coded reason as well. Without it, the flow is rejected at the door.
I maintain einvoicing, an open source Ruby gem that generates Factur-X, UBL and CII invoices compliant with the EN 16931 standard. Recently I ran a batch of fifteen deliberately hostile invoices through it: multi-rate, reverse charge, five hundred lines, overseas rates, foreign currency. The batch went through the XSD schema, the PDF/A-3 container checks and a hand-recomputed EN 16931 rule set.
The batch turned up four defects, all of them in my gem.
Seven regimes, not one rate
The standard does not know about “a 0 % invoice”. It knows seven VAT categories, and zero can come from five of them for entirely different legal reasons:
| Code | Regime | Typical case |
|---|---|---|
| S | Standard or reduced rate | The ordinary invoice |
| Z | Zero rated | Certain supplies taxed at 0 % |
| E | Exempt | Small-business franchise, medical acts, training |
| AE | Reverse charge | Construction subcontracting |
| K | Intra-Community supply | EU customer with a valid VAT number |
| G | Export outside the EU | Sale to a Swiss or American customer |
| O | Outside the scope of VAT | Certain indemnities and subsidies |
My gem stopped at three: standard, zero rated, reverse charge. Everything else came out as “standard”, quietly and without any visible error.
The field paper never asked for
When a category charges no VAT, the standard requires a reason: readable text and, in most cases, a European code. Five compliance rules check for it, one per category.
My gem wrote none of them. As a result, every reverse-charge invoice it emitted was non-compliant. They produced a correct PDF and schema-valid XML while being rejectable by the business rules.
For reverse charge, intra-Community supply, export and out-of-scope, the reason is mechanical: there is a dedicated European code. For plain exemption, there is not. The reason depends on the article invoked, and a small business under the French franchise regime is not exempt for the same reason as a medical practice.
Show the Ruby code
Einvoicing::LineItem.new(
description: "Consulting work",
quantity: 1,
unit_price: BigDecimal("2500.00"),
vat_rate: 0,
category: :exempt,
exemption_reason: "TVA non applicable, art. 293 B du CGI",
exemption_reason_code: Einvoicing::Tax::VATEX_FR_FRANCHISE
)
That is three lines of configuration. It still requires the software issuing the invoice to have somewhere to store them, which assumes it models the regime and not just the rate.
Three cents apart, and the invoice is refused
The second defect is sneakier. A category’s VAT total can be computed two ways: by summing the rounded VAT of each line, or by applying the rate to the category’s taxable base.
On a five-line invoice, both methods give the same figure. On five hundred lines, they diverge: three cents apart on one category, fifteen on another. The check is verified to the cent, and a “nearly right” invoice is refused like any other.
The standard settles on the second method: a category’s VAT amount is derived from its aggregated taxable base, not from the sum of the lines. One multiplication, one rounding, at the end.
Show the Ruby code
# EN 16931, BR-CO-17: category VAT = category taxable base x rate, rounded
# to the cent. The bases therefore add up in BigDecimal, with no intermediate
# rounding, and the rounding only happens after the multiplication.
base = lines.sum(&:net_amount) # BigDecimal, no rounding here
vat = (base * rate).round(2) # rate is 0.20 for 20%, as in the gem
Any rounding placed higher in the chain reopens the gap, which is why the defect survives line-by-line debugging, where every line taken on its own is correct.
And it never shows up in acceptance testing either, because nobody tests a five-hundred-line invoice.
The rates that exist anyway
20, 10, 5.5 and 0. My list of French VAT rates stopped there, like nearly every tutorial out there. So it rejected 2.1 %, the rate for the press and reimbursable medicine. It rejected 8.5 %, the standard rate in Guadeloupe, Martinique and Réunion. It rejected the Corsican rates. All of them perfectly legal.
Same family of error on VAT numbers. My validator held everyone to the French format, buyers included. An intra-Community invoice to Germany therefore failed on the customer’s VAT number, which was perfectly valid.
What both bugs have in common is a reference list written from the common case. It holds until the day a user sells in Corsica or buys from Munich.
Why this becomes a business problem
None of these cases is exotic. The French small-business franchise covers hundreds of thousands of companies. Reverse charge is the rule in construction subcontracting. The overseas rates cover several regions. Foreign currency shows up with the first customer outside the euro area.
Every one of these cases is somebody’s daily routine.
Today these invoices go out and nobody complains, because a human reads them on arrival and mentally fills in what is missing. The moment a machine receives them, there is no mental correction left. There is a rejection, a late payment, and a customer asking you to send it again.
In France, receiving electronic invoices becomes mandatory in September 2026, and issuing them in September 2027 for small and mid-sized companies. The first rejection, though, will land before the date on the calendar.
The question to ask your software vendor
Not “will you be ready for 2027?”. Everyone answers yes.
Rather: “what does your software write in the exemption reason field when I issue a reverse-charge invoice?”. If there is no answer, there is no field.
And to settle the doubt on a specific invoice, there is something faster than theory: run it through a validator, local analysis, no upload. If you want to review your whole chain, the 2026 e-invoicing page describes the assessment I offer.
What I take away from it
My conclusion is a little uncomfortable. I have been writing this gem for months, I know the standard, and I still emitted non-compliant invoices the whole time without a single test flinching.
The trap is not in the standard itself: schema-valid XML can be business-invalid, and nothing will tell you until somebody writes the invoices that hurt. The batch of fifteen hostile invoices cost half a day. It would have cost a great deal more in September 2027.
All of it is fixed in version 0.9.0 of the gem, released right after.