Dinero.js

Decision

We use the dinero.js library to work with prices.

Problems

At present, numerous rounding errors accumulate during the computation of the final order price, resulting in variations from the reference prices. Furthermore, our product engine lacks the capability to define and handle multiple currencies.

Also, JavaScript doesn't have decimal numbers (yet), and e.g. 0.1 + 0.2 != 0.3, which means that we always need to round manually to avoid rounding mistakes.

Context

Our current approach involves using the expression Math.round(x * 1000) / 1000 to truncate decimals. However, combining multiple results obtained this way can lead to inaccuracies in the total, which can be mitigated by retaining decimal values. Additionally, our pricing system lacks explicit currency definition, as it relies on the supplier's country for this purpose.

Options

The following packages were considered:

  1. Dinero.js
  2. Wallet.js
  3. Currency.js
  4. numeral

Reasoning

We chose this library due to its straightforward API and robust currency support.

Consequences

How do we implement this change?

At present, a significant portion of our prices are handled as floating-point number operations. We intend to transition to using dinero.js functions. Rather than executing a complete conversion all at once, we'll employ utilities to facilitate the conversion of amounts to dinero and vice versa:

  function toDinero(amount) {
    if (Array.isArray(amount)) {
      return amount.map(toDinero);
    }

    if (!amount) {
      return dinero({ amount: 0, currency: EUR });
    }

    if (amount.calculator && amount.create) {
      return amount;
    }

    return dinero({ amount: parseInt(amount * 100), currency: EUR });
  }

  const cpm = add(toDinero(batch.cpm), preparationCosts);


Furthermore, for the purpose of price traceability, we can employ trace operations, which will record all the operations that contributed to the current value:

  const d1 = dinero({ amount: 500, currency: USD });
  const d2 = dinero({ amount: 100, currency: USD });

  const result = add(d1, d2);

  expect(toDecimal(result)).toBe("6.00");
  expect(result.trace).toBe("5.00 + 1.00");

Until we add the currency field to the prices in the PE spreadsheet, we will assume that all prices are using the EUR currency.

Who will implement the change?

The Price team is committed to implementing dinero.js to substitute all floating-point price operations with dinero objects.

How do we teach this change?

Through pair programming and studying the documentation and examples.

What could go wrong?

We're well-prepared to handle any potential issues, thanks to our extensive suite of snapshot tests for price calculations. Additionally, we closely monitor price changes by analyzing the differences with the updated prices. While the new algorithms may yield slightly lower prices, they will be more aligned with the reference prices.

What do we do if something goes wrong?

Here are various approaches to revert the code:

  1. Expand the suite of unit tests and address the issues.
  2. Swap out the add/multiply/allocate functions with our custom implementation that returns numbers, rather than dinero objects.
  3. Simply revert the code to its previous state.

What is still unclear?

The price adjustments remain uncertain, which is why our strategy involves introducing releases with this new library alongside other planned price modifications.

Related ADRs

none.