CRM_Utils_Money::equals should round to monetary values then compare, not do a difference comparison.
When comparing monetary amounts, e.g. via the
Order.create API call, the
CRM_Utils_Money::equals function is using a precision level one higher than the currency precision (currently always 2), i.e. instead of requiring the same cents value, the amounts must be within 1/10 of a cent.
This uses a calculation with an incorrect premise, and should instead round the values and compare them.
So, if the currency has precision of 2 decimal points, a difference of more than 0.001 will cause the values to be considered as different.
Which seems like it's an order of magnitude out at first glance, but actually this is the wrong approach, as for example 0.0146 and 0.0154 are less than 0.001 apart, but round with a decimal precision of 2 places to 0.01 and 0.02 respectively. Yes, these two values are unlikely to come up for real, but similar cases could exist.
A practical example where this goes wrong (using Australian tax and currency rules, which are actually pretty simple):
- We want to create an order with a single taxed line item of $84, including 10% tax.
- The tax exclusive amount is ~ $76.36 (actually $76.3̅6̅),
- and the tax amount is calculated to $7.636.
- The software using the API calculates the total amount as $84.00.
- Each line item is also added to the Order, using the exclusive and tax amounts.
- The Order API diligently checks that the line items add up to the total, by checking whether $84 is equal to $76.36 + $7.636
CRM_Utils_Money::equals(84, 83.996, 'AUD')is called. According to the hard-coded currency precision of 2 decimal places, this should succeed, because $83.996 rounds to a whole dollar value of $84, but
- The equals function compares to two values with a maximum difference of $0.001, which is less than the $0.004 between to two values.
- The Order cannot be created.
Agileware ref CIVICRM-1368