E-Factura VAT Calculation in TypeScript and React
Your invoice editor shows a VAT figure, the PDF matches it, and the XML you upload to ANAF still has to follow one rule that line-by-line code misses: an e-Factura VAT calculation groups the lines by VAT rate, adds their net amounts, and rounds the VAT once per group. Every total in the file then has to come from those rounded numbers, to the cent. This tutorial builds that in TypeScript: Romania's VAT rates with their dates, VAT per rate in integer cents, the validation rules that compare totals, and an invoice line editor in React. The UI is Invoice Line Items from Wingo UI, the library we build, so we are not neutral. The per-rate math is plain TypeScript you can paste anywhere.
What are the Romanian VAT rates in 2026?
Two rates, both in force since August 1, 2025: 21% standard and 11% reduced. Law 141/2025 raised the standard rate from 19% and replaced the old 9% and 5% reduced rates with a single 11% rate, apart from a transitional 9% for some housing bought under contracts signed by August 1, 2025, which Law 161/2026 extended from July 31 to September 30, 2026 (Contzilla, August 2026 (opens in a new tab)); ANAF's before-and-after table of the Fiscal Code (opens in a new tab) shows both versions of article 291. As of October 2026, ANAF's consolidated Fiscal Code (opens in a new tab), last updated by Law 170/2026, still reads 21% and 11%.
The code is the VAT category an e-Factura line carries (BT-151): S for standard rated, which covers the reduced rate too, E for exempt, AE for reverse charge and K for intra-community supplies.
Two details matter for code. The 11% list has exceptions inside it: alcoholic drinks, non-alcoholic drinks under NC code 2202, food with added sugar and at least 10 g of sugar per 100 g, and food supplements stay at 21%. So store the rate on each product. And the rate depends on a date: article 291 (4) applies the rate in force when the chargeable event happens, usually the delivery. A correction you issue today for a July 2025 delivery carries 19%, so keep dated rates and let the VAT select offer the old one when a correction needs it:
RO_VAT_RATES from the invoicing lib holds the current five rows of the table above, and the line editor takes any list through its vatRates prop.
Why is e-Factura VAT computed per rate instead of per line?
Because the standard defines it that way. Rule BR-CO-17 of EN 16931, which e-Factura applies, reads: the VAT category tax amount (BT-117) equals the VAT category taxable amount (BT-116) times the rate (BT-119), rounded to two decimals. The Fiscal Code asks for the same on paper: article 319 (20) requires the taxable base for each rate and the VAT amount in lei for each rate. Adding up rounded line VAT gives a different invoice VAT per rate whenever several lines round in the same direction.
Three 21% lines and one 11% line show it:
Each line rounds its half cent up, three times over; the rate rounds once. 7.25 is the VAT for 21% on this invoice, and the line figures can stay on the PDF as information.
How do I compute VAT per rate in TypeScript without float errors?
Keep money in integer cents and divide integers. Math.round(22.5 * 0.21 * 100) / 100 returns 4.72, because 22.5 x 0.21 is stored as 4.7249999999999996. We ran every base from 0.01 to 1,000.00 lei: 1,000 of them have a 21% VAT ending in exactly half a cent, and the float formula rounds 188 of those the wrong way, while toFixed(2) gets 718 wrong. When the numerator is an integer, a half cent is exactly .5:
A reversal (storno) line carries negative amounts. Rounding the absolute value keeps -7.245 at -7.25, the same magnitude the validator computes, since BR-CO-17 compares absolute values.
Which e-Factura validation errors check the totals?
BR-CO-10, BR-CO-11, BR-CO-13, BR-CO-14 and BR-CO-15 compare the document totals to the cent, and only BR-S-08, BR-CO-17 and BR-S-09 allow any slack: less than one unit of the invoice currency. We read the rules in the CIUS-RO validation package the Ministry of Finance publishes for e-Factura, ro16931-ubl 1.0.9 (opens in a new tab), which embeds the EN 16931 rules from the European Commission's validation artefacts (opens in a new tab) (version 1.3.8; we rechecked the tolerances below in that release in October 2026):
BR-CO-17 accepts a VAT that is less than 1 leu (one unit of the invoice currency) away from base times rate. Summing rounded line VAT therefore passes it on most invoices: each line drifts by half a cent at most, so it takes about 200 lines rounding the same way to reach 1 leu. The e-Factura validation errors come from mixing the two paths: BT-117 from the per-rate breakdown while BT-110 is the sum of line VAT, or BT-112 as the sum of line totals with VAT. Those sums have no tolerance, every rule above is flagged fatal, and one cent apart means an error message comes back instead of a validated invoice. Do not build on the slack either. Your PDF, your database and the buyer's books should all show 7.25.
How do I turn the totals into e-Factura fields?
Derive every field from the same integers, in one order: line nets, the document discount per rate, the base and VAT of each rate, then the document totals. A discount on the whole invoice is the step that breaks mixed-rate invoices. BR-S-08 counts only the allowances that carry the same category and rate as the row, so a 100 lei discount on an invoice with 21% and 11% lines becomes two allowances, one per rate.
computeInvoiceTotals from the invoicing lib already shares a document discount across the lines by value, and the last line takes the remainder so the cents add up. The function below keeps each line's own discount inside BT-131, reads the document share back out per rate, and recomputes the VAT of each rate in cents:
Take the sample invoice from the editor demo further down: 25 licenses at 250 lei with 10% off, 6 hours of training at 320 lei and an e-Factura integration at 1,500 lei, all at 21%, plus 12 printed guides at 64.90 lei at 11%. With a 100 lei document discount, the function returns allowances of 92.07 lei at 21% and 7.93 lei at 11%, bases of 8,952.93 and 770.87, VAT of 1,880.12 and 84.80, and 11,688.72 lei in total.
$ npx wingo-ui@latest add invoicingThen check the numbers the XML will carry, whatever produced them: an old system, a stored draft, a hand edit. The check runs on the server right before you build the file and answers with the rule ids ANAF's validator uses:
How do I build the editable invoice lines in React?
Give the editor the lines and read the totals it computes, then recompute on the server. Invoice Line Items edits the lines like a small spreadsheet: a product search that fills unit, price and VAT, a VAT select that defaults to RO_VAT_RATES, a discount per line with a % / lei switch, and drag to reorder. The price cells are a Currency Input, so 1234.5 becomes 1,234.5 as you type and 1,234.50 on blur. Under the lines, the totals show the taxable base, one VAT row per rate and the grand total, all from one computeInvoiceTotals call on every keystroke, and onValueChange(lines, totals) hands you the same numbers. When the editor is narrower than 768px, every line becomes a card: tap it to edit the line in a bottom sheet on phones (a dialog on larger screens), and the totals fold into a Total row that opens the breakdown.
$ npx wingo-ui@latest add invoice-line-itemsIn a form, the editor's name prop writes the lines as JSON into a hidden input, so a server action receives them with the rest of the form:
The whole screen around it, with a client picker and CUI lookup, the series and number, due dates, an A4 preview and the e-Factura switch, is the Invoice Page block. Its onIssue receives the document with lines and documentDiscount, the two inputs toEFacturaTotals needs.
What changes for EUR invoices and 0% lines?
The invoice can be in euros; its VAT must also exist in lei. Article 319 (23) of the Fiscal Code lets the amounts use any currency as long as the VAT is expressed in lei at the exchange rate of article 290. In the XML, rule BR-RO-030 sets the VAT accounting currency (BT-6) to RON on any non-RON invoice, and BR-53 then requires the VAT total in that currency (BT-111). The Invoice Page shows the RON equivalent of a EUR total at the BNR rate; compute BT-111 on the server from the same per-rate VAT.
The 0% rows need more than a zero:
- Exempt (E): an exemption reason code or text on the breakdown row (BR-E-10), and a VAT of 0 (BR-E-09).
- Reverse charge (AE): the reason "Reverse charge" (BR-AE-10), a VAT of 0 (BR-AE-09), the seller's VAT id and the buyer's VAT id or registration number (BR-AE-02).
- Intra-community (K): the reason "Intra-community supply" (BR-IC-10) and the VAT ids of the seller and the buyer (BR-IC-02).
The ids those rules ask for have their own guides: Romanian CUI validation for a company's tax and VAT id, the Romanian CNP validator for B2C buyers (e-Factura accepts 13 zeros when the buyer gives none), and IBAN validation in JavaScript for the account you print under payment terms.
Should I copy this code or install the components?
Copy the math. vat-per-rate.ts has no imports and the preflight needs only its vatCents and one type, so both paste into any TypeScript project; efactura-totals.ts builds on computeInvoiceTotals from the invoicing lib and two types from the line editor. If you want React invoice line items that already show VAT per rate, the editor above installs with npx wingo-ui@latest add invoice-line-items and brings the invoicing lib and Currency Input with it. All three, and the Invoice Page, are part of Wingo UI Pro.
For the rest of an admin app around this form, the Next.js admin dashboard tutorial builds the shell, tables and settings, and more step-by-step React builds live in the Build it category.
FAQ
What are the VAT rates in Romania in 2026?
21% standard and 11% reduced, both in force since August 1, 2025 under Law 141/2025 and article 291 of the Fiscal Code. Before that date the standard rate was 19% and the reduced rates were 9% and 5%; a transitional 9% for some housing, extended by Law 161/2026, ended with deliveries on September 30, 2026. Exempt, reverse charge and intra-community lines go on an e-Factura invoice at 0% with the category codes E, AE and K.
Is e-Factura VAT calculated per line or per rate?
Per rate. Rule BR-CO-17 defines the VAT of each rate as the rate's taxable amount times the rate, rounded to two decimals, and article 319 (20) of the Fiscal Code asks for the VAT amount in lei for each rate. VAT shown on a line is informative and never feeds the totals.
Why does ANAF reject my e-Factura with BR-CO-14 or BR-CO-15?
Both rules compare sums to the cent. BR-CO-14 needs the invoice VAT total to equal the sum of the VAT per rate, and BR-CO-15 needs the total with VAT to equal the total without VAT plus that VAT. They fail when one total comes from rounded line amounts and another from the per-rate breakdown.
Does e-Factura accept a one-cent difference in VAT?
For the VAT of a rate, yes: in the ro16931-ubl 1.0.9 validation package, BR-CO-17 accepts a value less than one currency unit away from base times rate. The rules that add amounts up (BR-CO-10, BR-CO-11, BR-CO-13, BR-CO-14 and BR-CO-15) have no tolerance at all.
Which VAT rate applies when a July 2025 delivery is invoiced in August 2025?
Generally the rate in force when the chargeable event happened, which for a delivery is the delivery date, per article 291 (4) of the Fiscal Code. That makes it 19% for a July 2025 delivery, with exceptions for advances and invoices issued before the delivery.
- e-Factura
- VAT
- Romania
- TypeScript
- React
- Invoicing