FOX - WooCommerce Currency Switcher Professional

Round Converted Prices in WooCommerce: .99, Whole Numbers and Steps

An exchange rate turns a clean 49.00 into 45.37 or 4,083.12, and few shops want to show that. FOX has no rounding setting, because every shop wants a different pattern, but it gives you two filters to round converted prices in WooCommerce to whatever you like: whole numbers, .99 or .95 endings, steps of 5, 10, 100 or 1000, per currency. Most snippets that "do not work" fail for one of a few reasons: only one of the two filters is used, the base currency gets rounded too, taxes are added after the rounding, or the code does not run in the request that redraws prices. This guide covers each of them.

Two filters, and why you need both

FOX converts prices in two places, and each has its own filter:

  • woocs_raw_woocommerce_price receives every single converted price: simple products, each variation, and with Is multiple allowed = No every amount displayed in the selected currency.
  • woocs_woocommerce_variation_prices receives the prices WooCommerce collects for a variable product to build its price range, the "45.37 – 61.20" under the title. It is used with Is multiple allowed = Yes; with No the range is displayed through the first filter.

With only the first filter, a variable product shows an unrounded range and then a rounded price once a variation is selected. With only the second, the range is rounded and the selected variation is not. Attach the same function to both.

For a converted currency both filters receive a numeric string with a dot as decimal separator, already rounded to the currency's Decimal setting from the Currencies tab. Both also run for the base currency, without conversion, so the function has to decide which currencies to touch.

A rounding function to start from

/*
 * Round converted prices per currency.
 * Child theme functions.php, or a snippets plugin set to run everywhere.
 */
function myshop_woocs_round_price($price) {
    global $WOOCS;
    if (!isset($WOOCS) || !is_numeric($price) || (float) $price <= 0) {
        return $price; // keep empty and free prices as they are
    }
    $p = (float) $price;

    switch ($WOOCS->current_currency) {
        case 'EUR':
        case 'GBP':
            $p = ceil($p) - 0.01;      // 45.37 -> 45.99
            break;
        case 'JPY':
            $p = ceil($p / 100) * 100; // 4,083 -> 4,100
            break;
        default:
            return $price;             // the base currency and all others stay exact
    }
    return $p > 0 ? $p : $price;
}
add_filter('woocs_raw_woocommerce_price', 'myshop_woocs_round_price');
add_filter('woocs_woocommerce_variation_prices', 'myshop_woocs_round_price');

Replace the currency codes and the formulas with your own. Leaving the base currency out of the switch matters: without that the prices you typed into your products are rounded too, and the amount you entered is no longer the amount you charge.

Rounding recipes

Put any of these formulas in place of the lines in the switch; $p is the converted price.

ResultFormula45.37 becomes
Nearest whole numberround($p)45
Whole number, always up / downceil($p) / floor($p)46 / 45
Ending .99, upceil($p) - 0.0145.99
Ending .99, nearestround($p + 0.01) - 0.0144.99
Ending .95, upceil($p) - 0.0545.95
Nearest 0.50round($p * 2) / 245.50
Up to the next 5ceil($p / 5) * 550
Nearest 10 / 100 / 1000round($p, -1) / -2 / -350 / 0 / 0
Up, ending in 9 (49, 149, 1,999)ceil($p / 10) * 10 - 149

Steps of 100 or 1000 only make sense for currencies where prices are that large; the zero in the table is the warning. For endings such as .99 the currency must show two decimals: with Decimal set to 0 the result is shown as a whole number. The short FAQs for 99 cents, 50 cents, whole numbers and steps of 500 show single-currency versions.

Two things that look like rounding are not. Hide cents in the Currencies tab removes the cents from the displayed price and leaves them in the cart total, so a product shown as 45 is charged 45.37. Prices without cents in the Options tab recalculates the listed currencies with zero decimals, which is real rounding to whole numbers; its own description asks you to test the checkout after enabling it, and that includes an order with a percentage coupon.

Fixed prices you enter per currency also pass through woocs_raw_woocommerce_price. A formula leaves a price that already fits its pattern unchanged, so a fixed 19.99 stays 19.99 under the .99 rule, but a fixed 20 becomes 19.99. Exclude such currencies from the switch if their fixed prices must stay as entered.

Rounding and taxes

woocs_raw_woocommerce_price runs on the product price before WooCommerce adds or removes tax. woocs_woocommerce_variation_prices is different: when the shop shows prices including tax, the variable-product range it receives already contains the tax. What that means depends on Prices entered with tax and Display prices in the shop in WooCommerce → Settings → Tax.

Prices entered without tax, shown with tax

WooCommerce adds the tax to your rounded net price, and 45.99 plus 20% is shown as 55.19, while the variable-product range, rounded after tax, ends in .99. To make every price the customer sees end in .99, round the gross amount in the first filter and convert it back with your rate, and keep the plain formula in the second. This works when one tax rate applies to your customers:

/*
 * .99 endings on prices shown with 20% tax. Child theme functions.php.
 */
add_filter('woocs_raw_woocommerce_price', function ($price) {
    global $WOOCS;
    if (!isset($WOOCS) || !is_numeric($price) || (float) $price <= 0
        || $WOOCS->current_currency === $WOOCS->default_currency) {
        return $price;
    }
    $tax = 1.20; // your tax rate
    return (ceil((float) $price * $tax) - 0.01) / $tax; // net price, shown with tax as .99
});

add_filter('woocs_woocommerce_variation_prices', function ($price) {
    global $WOOCS;
    if (!isset($WOOCS) || !is_numeric($price) || (float) $price <= 0
        || $WOOCS->current_currency === $WOOCS->default_currency) {
        return $price;
    }
    return ceil((float) $price) - 0.01; // the range already includes tax
});

Prices entered with tax

Both filters then receive gross prices, and customers taxed at your base rate see the rounded price unchanged. For a customer in a country with another rate, WooCommerce removes your base tax and adds theirs, so 45.99 with 20% at home becomes 44.84 at 17%. If you want one gross price everywhere, WooCommerce has a filter for exactly that; the margin then varies by country instead:

add_filter('woocommerce_adjust_non_base_location_prices', '__return_false');

When the currency itself does not change, this is standard WooCommerce tax behaviour and happens the same way with FOX deactivated.

Cart totals, shipping and approximate prices

With Is multiple allowed = Yes, the cart is calculated from the rounded product prices, so line totals and the cart total agree with what the customer saw. With No, converted amounts exist only on screen: woocs_raw_woocommerce_price receives every displayed amount, cart subtotals, shipping and tax lines included, and rounds each one separately, so the rows can stop adding up to the total. The checkout then shows the base currency, which the function above leaves alone. If the rounded cart rows bother you, return $price unchanged when is_cart() is true.

Shipping costs are not product prices and need their own filter. With Is multiple allowed = Yes, FOX converts shipping rates on woocommerce_package_rates at priority 9999, so the rounding has to run later:

/*
 * Round converted shipping costs up to whole units, after FOX has converted them.
 * Child theme functions.php.
 */
add_filter('woocommerce_package_rates', function ($rates, $package) {
    global $WOOCS;
    if (!isset($WOOCS) || $WOOCS->current_currency === $WOOCS->default_currency) {
        return $rates;
    }
    foreach ($rates as $rate) {
        $cost = (float) $rate->get_cost();
        if ($cost <= 0) {
            continue; // free shipping stays free
        }
        $cost = ceil($cost);
        $rate->set_cost($cost);
        if (wc_tax_enabled() && !empty($rate->get_taxes()) && !WC()->customer->is_vat_exempt()) {
            $rate->set_taxes(WC_Tax::calc_shipping_tax($cost, WC_Tax::get_shipping_tax_rates()));
        }
    }
    return $rates;
}, 10000, 2);

WooCommerce stores calculated rates in the customer session, so change the cart contents or the address to see the new costs while testing.

The approximate amounts from Show approx. price and Show approximate amount are calculated directly from the rates and do not pass through these filters, so they stay unrounded.

When the rounding does not show

  • Old prices on variable products. WooCommerce caches the price ranges of variable products. After adding or changing the code, clear them in WooCommerce → Status → Tools → WooCommerce transients, or save the product, and purge the page cache.
  • Rounded on first load, unrounded a moment later. With I am using cache plugin on my site enabled, FOX redraws prices through admin-ajax.php. A snippets plugin set to run only on the front end does not run in that request. Set the snippet to run everywhere, or put the code in the child theme's functions.php.
  • Nothing changes at all. Make the function return a constant such as 777 for a moment. If prices do not show 777, the code is not loaded or something else sets the prices: a third-party price template, a dynamic pricing plugin or fixed prices.
  • Fatal error after pasting. Code copied from web pages often carries a typographic dash instead of a minus, curly quotes, or &gt; instead of >. Type those characters again by hand. Cannot redeclare means the same function name was pasted twice; keep one copy.
  • Prices rounded that you did not round. Another snippet or plugin uses the same filter. Search the theme and snippets for woocs_raw_woocommerce_price.

Checklist

  • One function attached to both woocs_raw_woocommerce_price and woocs_woocommerce_variation_prices.
  • The base currency, and any currency with fixed prices that must stay exact, left out.
  • Tax setup checked: gross rounding or woocommerce_adjust_non_base_location_prices where needed.
  • Shipping rounded separately at a priority above 9999.
  • WooCommerce transients and page caches cleared, code running in AJAX requests.

If the prices are still not rounded with a default theme and only WooCommerce and FOX active, post your snippet and a product link on the FOX support forum, and we will look at it.