Skip to content

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.

ts
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:

ts
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.

ts
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 decimal

Decimal.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.

MIT licensed. Specification artefacts belong to OpenPEPPOL, CEN and OASIS.