Amounts, rounding and totals
Amounts in the model are decimal strings, never binary floats. This page covers what you may pass in, what the builder computes (the calculations of BIS Billing 3.0), and Decimal, the arithmetic behind both the builder and the rules.
Numbers or strings
Pass a number or a string. The builder turns numbers into decimal strings with the digits you wrote, and keeps strings as they are.
const [first] = baseInvoice.invoiceLine;
if (first === undefined) {
throw new Error('baseInvoice has a line');
}
const invoice = createInvoice({
...baseInvoice,
invoiceLine: [
{ ...first, id: '1', price: { priceAmount: 19.99 } }, // a number ...
{ ...first, id: '2', price: { priceAmount: '19.990' } }, // ... or a string; precision is kept
],
});
const prices = invoice.invoiceLine.map((line) => line.price.priceAmount.value); // ['19.99', '19.990']
const net = invoice.invoiceLine.map((line) => line.lineExtensionAmount.value); // ['139.93', '139.93']Computed: amounts are rounded to two decimals only where the builder computes them. Each line's net amount is rounded before it is summed, and totals are sums of rounded amounts.
Amounts you state
The builder computes only what is absent. An amount you state is kept, even when it does not add up, and validate() reports it:
const invalidStated = createInvoice({
...baseInvoice,
invoiceLine: [{ ...first, lineExtensionAmount: 2799 }], // wrong on purpose: 7 × 400 is 2800
});
const kept = invalidStated.invoiceLine[0]?.lineExtensionAmount.value; // '2799': never corrected
const reported = validate(invalidStated).fatal.map((issue) => issue.id); // ['PEPPOL-EN16931-R120']The builder never overrides amounts you set
Leave an amount out to have it computed. State it to keep your own figure, for example one that matches your accounting system to the cent, and let the validator check it.
Decimal
Decimal does exact decimal arithmetic with the rounding of XPath: halves round towards positive infinity, so -2.5 becomes -2. The rules compare with it, so a rounding difference you see in a validation issue is the one the Schematron would see.
const sum = Decimal.from('0.1').add(Decimal.from('0.2')).toString(); // '0.3', not 0.30000000000000004
const halfUp = Decimal.from('2.675').round(2).toString(); // '2.68'
const negativeHalf = Decimal.from('-2.5').round().toString(); // '-2': halves round towards +∞, as XPath does
const notDecimal = Decimal.parse('1e3'); // null: not an XSD decimalDecimal.parse() accepts only the XSD decimal syntax and returns null otherwise; Decimal.from() also takes a number or a bigint and throws an InvalidDecimalError for one that is not finite.