Реалізація мультивалютності на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація мультивалютності на сайті
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація мультивалютності на сайті

Мультивалютність — це не просто умноження ціни на курс. Це система, яка визначає, в якій валюті показувати ціни, як їх зберігати в базі, як обробляти платежі в кількох валютах та як сводити фінансову звітність воєдино. Погано реалізована мультивалютність породжує розбіжності в бухгалтерії, баги з округленням та проблеми з НДС.

Стратегії зберігання цін

Є два принципово різних підходи:

Стратегія 1: Зберігання в базовій валюті, конвертація на льоту

Всі ціни зберігаються в одній валюті (USD, EUR або локальна), при відображенні умножаються на актуальний курс. Просто в реалізації, але курс змінюється — покупець бачить різні ціни при кожному візиті. Підходить для B2B та інформаційних сайтів.

Стратегія 2: Явні ціни в кожній валюті

В базі зберігається ціна окремо для кожної валюти. Менеджер керує цінами вручну або з допомогою автообновлення за курсом. Покупець бачить фіксовану «красиву» ціну (999 руб. а не 997.34 руб.). Підходить для роздрібної торгівлі.

Для e-commerce майже завжди вибирається другий підхід з напівавтоматичним обновленням:

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  -- до базової валюти
);

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),  -- зачеркнута ціна
    updated_at  TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE (variant_id, currency)
);

Автообновлення курсів

Курси обновляються по розписанню з публічних джерел:

class ExchangeRateUpdater
{
    private array $providers = [
        CbrExchangeRateProvider::class,  // ЦБ РФ
        NbrbExchangeRateProvider::class, // НБРБ (Беларусь)
        EcbExchangeRateProvider::class,  // ЕЦБ
    ];

    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();
    }
}

ЦБ РФ публікує XML за адресою https://www.cbr.ru/scripts/XML_daily.asp. НБРБ — JSON API: https://api.nbrb.by/exrates/rates?periodicity=0.

Автообновлення курсів не означає автопересчет цін у product_prices. Це окремий крок — либо ручний (менеджер натискає «Пересчитати за курсом»), либо автоматичний з порогом (пересчитувати тільки якщо курс змінився більше чим на 2%).

Вибір валюти користувачем

Користувач вибирає валюту в шапці сайту. Вибір зберігається:

  • Для гостей — у cookie preferred_currency (термін 90 днів)
  • Для авторизованих — у users.preferred_currency

Middleware визначає поточну валюту при кожному запиті:

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. Явний параметр в запиті (перемикач в шапці)
        if ($request->has('currency') && $this->isValid($request->currency)) {
            $this->persistChoice($request, $request->currency);
            return $request->currency;
        }

        // 2. Зберіжений вибір користувача
        if ($request->user()?->preferred_currency) {
            return $request->user()->preferred_currency;
        }

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

        // 4. GeoIP (якщо включено)
        return $this->geoipCurrency->detect($request->ip())
            ?? config('shop.default_currency', 'RUB');
    }
}

Форматування цін

Форматування — не тривіальна задача: різні валюти мають різні розділювачі та позиції символу:

class PriceFormatter
{
    public function format(float $amount, Currency $currency): string
    {
        $formatted = number_format(
            $amount,
            $currency->decimals,
            ',',  // десятичний розділювач
            ' '   // розділювач тисяч
        );

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

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

Платежи в кількох валютах

Платіжний шлюз має підтримувати мультивалютність. Stripe — ідеально: приймає платіж у будь-якій валюті, конвертує на стороні процесора. ЮKassa — тільки в рублях, конвертація на стороні магазину. CloudPayments — BYN, RUB, USD, EUR.

При оплаті фіксується валюта замовлення та курс на момент оплати:

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); -- для звітності

Це дозволяє потім звести звітність в єдиній валюті незалежно від того, в чому платив покупець.

Округлення та anti-patterns

Ніколи не зберігати гроші в FLOAT — втрата точності при математиці. Завжди NUMERIC(12,2) або DECIMAL.

Округлення при конвертації:

// НЕПРАВИЛЬНО: 9.994999... → 9.99 але 9.995000 → 10.00 (PHP_ROUND_HALF_UP)
round($price * $rate, 2);

// ПРАВИЛЬНО: банківське округлення, помилка не накапливується
round($price * $rate, 2, PHP_ROUND_HALF_EVEN);

При сумуванні позицій замовлення — спочатку сумуємо, потім округлюємо, не навпаки.

Відображення в каталозі

При індексації каталогу через Elasticsearch або Meilisearch — індексувати ціни у всіх активних валютах як окремі поля для фільтрації:

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

Це дозволяє фільтрувати за ціною в поточній валюті користувача без пересчету при кожному запиті.

Терміни реалізації

  • Базова система: зберігання цін + перемикач валюти + форматування: 3–4 дні
  • Автообновлення курсів (ЦБ РФ / НБРБ): 1 день
  • Автопересчет цін з порогом відклонення: 1–2 дні
  • Мультивалютні платежі (залежить від шлюзу): 2–4 дні
  • Фінансова звітність в базовій валюті: 1–2 дні

Повна реалізація для магазину з 3–5 валютами: 1–2 тижні.