Check WPGlobalCart for WordPress MultiSite

Search
TOP

Currency Conversion Using 3rd Exchange Rates

WooCommerce Global Cart - Single Site / Currency Conversion Using 3rd Exchange Rates
Share on FacebookTweet about this on TwitterPin on Pinterest

Currency Conversion Using 3rd Exchange Rates

When using WooCommerce shops with different currencies, product prices often need to be converted from the currency of the original product to the currency used by the destination shop. The WOOGC_Currency_Converter class included with WP Global Cart provides a simple way to perform these conversions without requiring an additional currency conversion plugin.

The class uses the European Central Bank (ECB) reference exchange rates as its source. ECB rates are published against the Euro, allowing the class to convert between any two supported currencies indirectly through EUR. For example, a product priced in RON can be converted to USD using the EUR/RON and EUR/USD rates. The rates are stored in WordPress options and reused between updates, so a temporary failure of the ECB service does not prevent conversions from working.

Conversion is performed with a simple function call:

$value = WOOGC_Currency_Converter::convert(
    $amount,
    $from_currency,
    $to_currency
);

For example:

$value = WOOGC_Currency_Converter::convert(
    100,
    'EUR',
    'USD'
);

This approach can eliminate the need for dedicated WooCommerce multi-currency plugins such as Aelia Currency Switcher, FOX Currency Switcher, or WooCommerce’s own Multicurrency extension when your requirement is simply to convert product prices programmatically rather than provide a customer-facing currency switcher. Aelia Currency Switcher is a premium product, while FOX follows a free/premium model with additional features available in its paid version.

The class is particularly useful with WP Global Cart, where products can originate from one WooCommerce shop while being synchronized to another shop using a different currency. The original product currency and price can be retained, and the destination shop can calculate its local price automatically using the latest stored ECB rates.

This keeps the currency conversion logic lightweight, avoids adding another plugin dependency, and allows WP Global Cart to control the conversion process directly as part of product synchronization.

The code should be placed inside a custom file in the /wp-content/mu-plugins/ folder.

    /**
     * WP Global Cart - ECB Currency Converter
     *
     * Retrieves Euro foreign exchange reference rates from the ECB
     * and converts amounts between currencies using EUR as the base.
     */
    class WOOGC_Currency_Converter {

        /**
         * ECB daily XML feed.
         */
        const ECB_FEED_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';

        /**
         * WordPress option used to store the rates.
         */
        const OPTION_RATES = 'woogc_ecb_currency_rates';

        /**
         * WordPress option used to store the date of the last
         * successfully downloaded ECB rates.
         */
        const OPTION_DATE = 'woogc_ecb_currency_rates_date';

        /**
         * WordPress option used to store the last update timestamp.
         */
        const OPTION_UPDATED = 'woogc_ecb_currency_rates_updated';

        /**
         * Convert an amount from one currency to another.
         *
         * Example:
         * 100 EUR -> USD
         * 100 RON -> USD
         *
         * @param float  $amount
         * @param string $from_currency
         * @param string $to_currency
         *
         * @return float|false
         */
        public static function convert( $amount, $from_currency, $to_currency ) {

            $amount = (float) $amount;

            $from_currency = strtoupper( trim( $from_currency ) );
            $to_currency   = strtoupper( trim( $to_currency ) );

            // Nothing to convert.
            if ( $from_currency === $to_currency ) {
                return $amount;
            }

            // Make sure we have current rates, or the latest available rates.
            $rates = self::get_rates();

            if ( ! is_array( $rates ) || empty( $rates ) ) {
                return false;
            }

            /*
             * EUR is the base currency.
             *
             * Example:
             * EUR = 1
             * USD = 1.1551
             * RON = 5.2568
             */
            $from_rate = 1.0;
            $to_rate   = 1.0;

            /*
             * Currency other than EUR must exist in the ECB rates.
             */
            if ( 'EUR' !== $from_currency ) {

                if ( ! isset( $rates[ $from_currency ] ) ) {
                    return false;
                }

                $from_rate = (float) $rates[ $from_currency ];
            }

            if ( 'EUR' !== $to_currency ) {

                if ( ! isset( $rates[ $to_currency ] ) ) {
                    return false;
                }

                $to_rate = (float) $rates[ $to_currency ];
            }

            /*
             * Convert:
             *
             * source currency -> EUR -> target currency
             *
             * Example RON -> USD:
             *
             * 100 / 5.2568 * 1.1551
             */
            return ( $amount / $from_rate ) * $to_rate;
        }

        /**
         * Get the currently stored ECB rates.
         *
         * This will attempt to update the rates once per day.
         * If today's update is unavailable, the previously stored
         * successful rates are returned.
         *
         * @return array|false
         */
        public static function get_rates() {

            $stored_rates = get_option( self::OPTION_RATES, array() );
            $stored_date  = get_option( self::OPTION_DATE, '' );

            /*
             * If we already have rates downloaded today, use them.
             */
            $today = current_time( 'Y-m-d' );

            if (
                ! empty( $stored_rates ) &&
                $stored_date === $today
            ) {
                return $stored_rates;
            }

            /*
             * Try to download fresh ECB rates.
             */
            $new_rates = self::update_rates();

            /*
             * Successful update.
             */
            if ( is_array( $new_rates ) && ! empty( $new_rates ) ) {
                return $new_rates;
            }

            /*
             * ECB unavailable / no new rates today.
             *
             * Fall back to the last successful rates.
             */
            if ( ! empty( $stored_rates ) ) {
                return $stored_rates;
            }

            return false;
        }

        /**
         * Download and store the latest ECB rates.
         *
         * @return array|false
         */
        public static function update_rates() {

            $response = wp_remote_get(
                self::ECB_FEED_URL,
                array(
                    'timeout'     => 15,
                    'redirection' => 3,
                    'sslverify'   => true,
                    'headers'     => array(
                        'Accept' => 'application/xml,text/xml',
                    ),
                )
            );

            if ( is_wp_error( $response ) ) {
                return false;
            }

            $status_code = wp_remote_retrieve_response_code( $response );

            if ( 200 !== $status_code ) {
                return false;
            }

            $body = wp_remote_retrieve_body( $response );

            if ( empty( $body ) ) {
                return false;
            }

            /*
             * Prevent XML external entity processing.
             */
            libxml_use_internal_errors( true );

            $xml = simplexml_load_string(
                $body,
                'SimpleXMLElement',
                LIBXML_NONET | LIBXML_NOCDATA
            );

            if ( false === $xml ) {
                libxml_clear_errors();
                return false;
            }

            libxml_clear_errors();

            $rates = array(
                'EUR' => 1.0,
            );

            /*
             * ECB XML structure:
             *
             * <Cube currency="USD" rate="1.1551"/>
             */
            $currency_nodes = $xml->xpath(
                '//*[local-name()="Cube"][@currency and @rate]'
            );

            if ( empty( $currency_nodes ) ) {
                return false;
            }

            foreach ( $currency_nodes as $node ) {

                $currency = strtoupper( (string) $node['currency'] );
                $rate     = (float) $node['rate'];

                if ( empty( $currency ) || $rate <= 0 ) {
                    continue;
                }

                $rates[ $currency ] = $rate;
            }

            /*
             * Make sure we got a reasonable set of rates.
             */
            if ( count( $rates ) < 2 ) {
                return false;
            }

            /*
             * Save only after successfully parsing the complete feed.
             *
             * This is important: a failed/invalid request must never
             * overwrite the last known-good rates.
             */
            $today = current_time( 'Y-m-d' );

            update_option(
                self::OPTION_RATES,
                $rates,
                false
            );

            update_option(
                self::OPTION_DATE,
                $today,
                false
            );

            update_option(
                self::OPTION_UPDATED,
                time(),
                false
            );

            return $rates;
        }

        /**
         * Get information about the currently stored rates.
         *
         * Useful for debugging/admin UI.
         *
         * @return array
         */
        public static function get_status() {

            return array(
                'date'    => get_option( self::OPTION_DATE, '' ),
                'updated' => get_option( self::OPTION_UPDATED, 0 ),
                'rates'   => get_option( self::OPTION_RATES, array() ),
            );
        }
    }