Multi-currency support implementation for website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Multi-currency support implementation for website
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementation of Multi-Currency Support on Website

Multi-currency is not just price multiplied by rate. It's a system that determines which currency to display prices in, how to store them in database, how to process payments in multiple currencies, and how to reconcile financial reports. Poorly implemented multi-currency causes accounting discrepancies, rounding bugs, and VAT issues.

Price Storage Strategies

Two fundamentally different approaches:

Strategy 1: Store in Base Currency, Convert on Display

All prices stored in one currency (USD, EUR or local), displayed price multiplied by current rate. Simple to implement, but rate changes — customer sees different prices on each visit. Good for B2B and info sites.

Strategy 2: Explicit Prices in Each Currency

Database stores price separately for each currency. Manager controls prices manually or with auto-update by rate. Customer sees fixed "nice" price (999 rubles not 997.34). Good for retail.

For e-commerce, almost always second approach with semi-automatic update:

CREATE TABLE currencies (
    code        CHAR(3) PRIMARY KEY,      -- ISO 4217: RUB, USD, EUR, BYN
    name        VARCHAR(100) NOT NULL,
    symbol      VARCHAR(10) NOT NULL,
    symbol_pos  VARCHAR(10) NOT NULL DEFAULT 'after', -- before | after
    decimals    SMALLINT NOT NULL DEFAULT 2,
    is_active   BOOLEAN NOT NULL DEFAULT true,
    is_default  BOOLEAN NOT NULL DEFAULT false,
    rate_to_base NUMERIC(15,6) NOT NULL DEFAULT 1.0  -- to base currency
);

CREATE TABLE product_prices (
    id          BIGSERIAL PRIMARY KEY,
    variant_id  BIGINT NOT NULL REFERENCES product_variants(id),
    currency    CHAR(3) NOT NULL REFERENCES currencies(code),
    price       NUMERIC(12,2) NOT NULL,
    compare_at  NUMERIC(12,2),  -- strikethrough price
    updated_at  TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE (variant_id, currency)
);

Auto-Update Exchange Rates

Rates updated on schedule from public sources:

class ExchangeRateUpdater
{
    private array $providers = [
        CbrExchangeRateProvider::class,  // CBR RF
        NbrbExchangeRateProvider::class, // NBRB (Belarus)
        EcbExchangeRateProvider::class,  // ECB
    ];

    public function update(): void
    {
        foreach ($this->providers as $providerClass) {
            $provider = app($providerClass);
            $rates = $provider->fetchRates();

            foreach ($rates as $code => $rate) {
                Currency::where('code', $code)->update([
                    'rate_to_base' => $rate,
                ]);
            }
        }

        Cache::tags(['currencies'])->flush();
    }
}

CBR RF publishes XML at https://www.cbr.ru/scripts/XML_daily.asp. NBRB — JSON API: https://api.nbrb.by/exrates/rates?periodicity=0.

Auto-update rates doesn't mean auto-recalculate prices in product_prices. Separate step — either manual (manager clicks "Recalculate by rate") or automatic with threshold (only recalculate if rate changed 2%+).

User Currency Selection

User selects currency in site header. Choice saved as:

  • For guests — preferred_currency cookie (90 days)
  • For logged in — users.preferred_currency

Middleware determines current currency on each request:

class ResolveCurrency
{
    public function handle(Request $request, Closure $next): Response
    {
        $currency = $this->detectCurrency($request);

        app()->instance('current_currency', Currency::find($currency));
        $request->merge(['currency' => $currency]);

        return $next($request);
    }

    private function detectCurrency(Request $request): string
    {
        // 1. Explicit parameter in request (switcher in header)
        if ($request->has('currency') && $this->isValid($request->currency)) {
            $this->persistChoice($request, $request->currency);
            return $request->currency;
        }

        // 2. Saved user choice
        if ($request->user()?->preferred_currency) {
            return $request->user()->preferred_currency;
        }

        // 3. Cookie
        if ($cookie = $request->cookie('preferred_currency')) {
            return $cookie;
        }

        // 4. GeoIP (if enabled)
        return $this->geoipCurrency->detect($request->ip())
            ?? config('shop.default_currency', 'RUB');
    }
}

Price Formatting

Formatting is non-trivial: different currencies have different separators and symbol positions:

class PriceFormatter
{
    public function format(float $amount, Currency $currency): string
    {
        $formatted = number_format(
            $amount,
            $currency->decimals,
            ',',  // decimal separator
            ' '   // thousands separator
        );

        return match($currency->symbol_pos) {
            'before' => $currency->symbol . $formatted,
            'after'  => $formatted . ' ' . $currency->symbol,
        };
    }
}

// 1 499,00 ₽
// $1,499.00
// 1.499,00 €

Payments in Multiple Currencies

Payment gateway must support multi-currency. Stripe — ideal: accepts payment in any currency, converts on processor side. Yandex.Kassa — RUB only, conversion on store side. CloudPayments — BYN, RUB, USD, EUR.

On payment, order's currency and exchange rate at payment time are fixed:

ALTER TABLE orders ADD COLUMN currency CHAR(3) NOT NULL DEFAULT 'RUB';
ALTER TABLE orders ADD COLUMN exchange_rate NUMERIC(15,6) NOT NULL DEFAULT 1.0;
ALTER TABLE orders ADD COLUMN base_currency_total NUMERIC(12,2); -- for reporting

Allows later reconciling reports in single currency regardless of customer's payment currency.

Rounding and Anti-Patterns

Never store money in FLOAT — precision loss in math. Always NUMERIC(12,2) or DECIMAL.

Rounding on conversion:

// WRONG: 9.994999... → 9.99 but 9.995000 → 10.00 (PHP_ROUND_HALF_UP)
round($price * $rate, 2);

// RIGHT: banker's rounding, error doesn't accumulate
round($price * $rate, 2, PHP_ROUND_HALF_EVEN);

On summing order items — sum first, then round, not vice versa.

Catalog Display

When indexing catalog via Elasticsearch or Meilisearch — index prices in all active currencies as separate fields for filtering:

{
  "id": 123,
  "price_rub": 1499.00,
  "price_usd": 16.50,
  "price_eur": 15.20,
  "price_byn": 52.10
}

Allows filtering by price in user's current currency without recalculation on each request.

Timeline

  • Basic system: price storage + currency switcher + formatting: 3–4 days
  • Auto-update rates (CBR RF / NBRB): 1 day
  • Auto-recalculate prices with threshold: 1–2 days
  • Multi-currency payments (depends on gateway): 2–4 days
  • Financial reporting in base currency: 1–2 days

Full implementation for store with 3–5 currencies: 1–2 weeks.