Skip to content
Devix Open Source

Reference

Options

vat(amount, options)

vat(100, {
  country: 'SA',        // or…
  rate: 0.15,           // …a rate of your own, which wins
  on: '2019-06-01',     // the date of supply, whose rate applies
  inclusive: false,     // the amount already includes the tax
  treatment: 'standard',
  rounding: 'half-up',
  decimals: 2,          // from the currency when not given
  currency: 'SAR',
});

add() and extract() are the same function with inclusive fixed, because those are the two things anyone actually wants:

add(100, { country: 'SA' });       // net → gross
extract(115, { country: 'SA' });   // gross → net

What comes back

{
  net: '100.00', vat: '15.00', gross: '115.00',
  rate: 0.15,
  treatment: 'standard',
  currency: 'SAR',
  minor: { net: 10000, vat: 1500, gross: 11500 },
}

Treatments

Rate Means
standard the country's The usual case.
zero-rated 0% Taxable at zero — exports, and some healthcare and education. Input tax is recoverable.
exempt 0% Outside the tax — some financial services, residential property. Input tax is not recoverable.
out-of-scope 0% Not a taxable supply at all.
reverse-charge 0% The buyer accounts for the tax, not you.

All four of the last are zero, and they are not interchangeable: a return reports them separately, and two of them affect what you can reclaim. The treatment is carried through to the breakdown for that reason.

invoice(lines, options)

invoice([
  { amount: '10.00', quantity: 2, description: 'Consulting' },
  { amount: '50.00', rate: 0, treatment: 'zero-rated' },
  { amount: '10.00', discount: '2.00' },
], {
  country: 'AE',
  rounding_level: 'line',   // or 'total'
});

A line may carry its own rate and treatment, so a basket mixing standard, zero-rated and exempt items produces the breakdown a return wants rather than one number.

Rates

import { rateFor, historyFor, countryFor, COUNTRIES } from '@devix-labs/vat-calculator';

rateFor('SA');                  // 0.15
rateFor('SA', '2019-06-01');    // 0.05
rateFor('QA');                  // null — announced, not in force
rateFor('ZZ');                  // throws RateError

historyFor('SA');
// [{ rate: 0.05, since: '2018-01-01' }, { rate: 0.15, since: '2020-07-01' }]

countryFor('BH');
// { name: 'Bahrain', currency: 'BHD', label: 'VAT', labelAr: '…', periods: [ … ] }

COUNTRIES is plain data. If a rate changes before this package does, copy the object, change the number, and pass rate — nothing here has to be waited on:

const rate = myRates[country] ?? rateFor(country);
vat(amount, { rate });

label and labelAr are what the tax is called locally, for printing on an invoice — "VAT" in the Gulf, "Sales tax" in Pakistan and Jordan.

Feeding a ZATCA QR code

The Saudi e-invoice QR wants the gross total and the tax, as strings. That is exactly what this produces:

import { invoice } from '@devix-labs/vat-calculator';
import { encodeInvoice } from '@devix-labs/e-invoice-qr';

const totals = invoice(lines, { country: 'SA' });

encodeInvoice({
  seller: 'متجر ديفكس',
  vatNumber: '300000000000003',
  timestamp: new Date(),
  total: totals.gross,
  vatTotal: totals.vat,
});

Both packages are independent — neither depends on the other — but the strings line up, and check() on the QR side will confirm the VAT is plausible for the rate.

Updated 15 Sep 2026