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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, 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 недели.