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.