User Tools

Site Tools

Action disabled: register

Dealing With Prices

Dealing with prices is a rather tricky business, because they need to be accurate and predictable across all platforms. However, floating point precision varies between PHP installations and between database engines.

The General Solution

The general solution is to round all prices to a precision lower than the most inprecise PHP platform and database engine. This precision is defined by the constant _TB_PRICE_DATABASE_PRECISION_, which is currently set to 6 (behind the decimal).

Entirely distinct from this, some few places round to display precision, typically 2 digits. See Rounding to Display Precision below.

Best Practices

All these hints apply when rounding to database precision. Rounding for di collection of best practices. Please enhance as wisdom flows in :-)

Rounding Method

round() is more adequate than Tools::ps_round() for rounding to _TB_PRICE_DATABASE_PRECISION_. Not so when rounding for display.

Parameters

Values coming in as parameters need rounding. Because these can be user data or being provided by a sloppily written module.

Calculations

All calculations including a division or multiplication with floats need rounding. As opposed to additions, subtractions and multiplications with integers, which keep or even lower precision behind the decimal.

Some methods, like TaxCalculator::addTaxes() guarantee a rounded result already. This is noted in the method comment, then.

Database

Values coming from the database can be assumed to be rounded properly already. This assumption is probably not always true for the time being, because there can be database records from before introduction of proper rounding and data written by modules into the database directly.

Long term plan is to enforce proper database recording by making Validate::isPrice() more strict. Until then, existing database records will need an upgrade.

TODO: this more strict method exists already, describe here how to enable it as soon as it's public.

Comparing Prices

As floats can't get compared reliably, it's better to compare them as stings:

// $price might be a string, so cast to float, first.
if ((string) (float) $price1 === (string) (float) $price2) {
    [...]
}

Or, if rounding is unclear or might mismatch:

if ((string) round($price1, _TB_PRICE_DATABASE_PRECISION_)
    === (string) round($price2, _TB_PRICE_DATABASE_PRECISION_)) {
    [...]
}

Sample Code

Lots of rounding in fairly easily readable code happens in classes OrderDetail and OrderSilp.

Rounding to Display Precision

Prices shown in front office and back office (the latter with exceptions) are shown not with 6, but with usually only 2 digits behind the decimal. This precision is merchant-configurable in back office -> Preferences -> General -> Number of decimals and can get read in PHP code with Configuration::get('PS_PRICE_DISPLAY_PRECISION').

Rounding Immediately Before Display

With the exception of price calculations in the Cart or when creating an Order, rounding of prices should happen for display, only.

Notably, price calculations (or any calculations) should not happen in templates. Currently templates do this a lot, which is a huge code maintenance burden.

Price Display in Flow Text

Display of prices is quite configurable. Number of decimals to display, currency at the front or at the tail, such stuff. To easily take all this into account, use Smarty function displayPrice:

Price of this product is {displayPrice price=$price currency=$currency}.

This also takes care of proper rounding and adds a currency sign as appropriate. Assembling such price text directly in the template is frowned upon, as such occurrences are hard to maintain.

Prices in Back Office Input Fields

Back office input fields should allow full database precision, e.g. € 1.0001. As a nice compromise to allow this, but also not distract with lots of trailing zeros, there's the Smarty function displayPriceValue. Usage like:

<input type="text"
       value="{displayPriceValue price=$field['value']}"
       onchange="this.value = this.value.replace(/,/g, '.');"
       [...]
/>

For options panels, this is achieved automatically by using type 'price'. Like:

'MY_OPTION' => [
    'type'       => 'price',
    'cast'       => 'priceval',
    'validation' => 'isPrice',
    [...]
],
Rounding in Cart and Orders

The other place to round to display precision is when adding up the cart. There are multiple strategies to do this. Strategy is merchant-configurable in back office -> Preferences -> General -> Round type. In code it can be read by (int) Configuration::get('PS_ROUND_TYPE') when dealing with prices not yet part of an order. Comparison constants are Order::ROUND_ITEM, Order::ROUND_LINE and Order::ROUND_TOTAL.

As the rounding type setting can change over time, orders store this setting in Order->round_type. Accordingly, when dealing with already ordered products, use the order's setting. However, rounding to display precision of already ordered items should be a rare need. A cart contains properly rounded prices, as do orders created from carts.

Another caveat is, only either the price with tax or the price without tax can get rounded to display precision. The variant visible to the shop customer gets rounded, the counterpart hat to follow.

thirty bees' code for adding up the cart takes all this into account already.

Rounding of Taxes

Taxes never get rounded to display precision during price calculations. Doing so would either falsify the base price, falsify the tax rate or match only by accident.

dealing_with_prices.txt · Last modified: 2019/03/05 22:11 by Traumflug