Погано реалізована мультивалютність породжує розбіжності в бухгалтерії, баги з округленням і проблеми з ПДВ. Великий інтернет-магазин втратив значну суму через те, що ціни округлювалися вниз на користь покупця. Наш досвід дозволяє уникнути цих помилок і гарантує точність фінансових даних. Понад 5 років ми впроваджуємо мультивалютність на сайтах різного масштабу — від стартапів до enterprise-рішень з 30+ валютами. У цій статті розберемо ключові технічні рішення: схеми зберігання цін, автооновлення курсів, форматування та мультивалютні платежі.
Чому мультивалютність — це не просто перемикач валют?
Мультивалютність — комплексна задача, що зачіпає базу даних, бізнес-логіку та платіжні шлюзи. Помилки на будь-якому етапі призводять до фінансових втрат. Розглянемо два основні підходи до зберігання цін.
Як ми зберігаємо ціни?
Є два принципово різних підходи. Перший — базова валюта з конвертацією на льоту: всі ціни зберігаються в одній валюті, при відображенні множаться на актуальний курс. Простий у реалізації, але курс змінюється — покупець бачить різні ціни при кожному візиті. Підходить для B2B та інформаційних сайтів.
Другий — явні ціни в кожній валюті: в базі зберігається ціна окремо для кожної валюти. Менеджер керує цінами вручну або за допомогою автооновлення за курсом. Покупець бачить фіксовану «красиву» ціну (999 грн, а не 997,34). Це оптимально для роздробу.
Явні ціни кращі за конвертацію на льоту в 3 рази за стабільністю для покупця — ціна не змінюється від візиту до візиту.
| Характеристика | Базова валюта + конвертація | Явні ціни в кожній валюті |
|---|---|---|
| Складність реалізації | Низька | Середня |
| Стабільність цін для покупця | Низька (коливається з курсом) | Висока (фіксовані) |
| Підходить для | B2B, вітрини | e-commerce, роздріб |
| Управління цінами | Автоматичне | Ручне / напівавтомат |
CREATE TABLE currencies ( code CHAR(3) PRIMARY KEY, -- ISO 4217: UAH, USD, EUR, BYN name VARCHAR(100) NOT NULL, symbol VARCHAR(10) NOT NULL, symbol_pos VARCHAR(10) NOT NULL DEFAULT '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) ); Коди валют стандартизовані за ISO 4217.
Як оновлюються курси?
Курси оновлюються за розкладом з публічних джерел. ЦБ РФ публікує XML за адресою https://www.cbr.ru/scripts/XML_daily.asp, НБРБ — JSON API https://api.nbrb.by/exrates/rates?periodicity=0. Підтримуються й інші провайдери.
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(); } } Автооновлення курсів не означає автоперерахунок цін у 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', 'UAH'); } } Як форматувати ціни?
Форматування — нетривіальна задача: валюти мають різні роздільники та позицію символу. Ми використовуємо гнучкий клас PriceFormatter:
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, }; } } Як реалізовані мультивалютні платежі?
Платіжний шлюз має підтримувати мультивалютність. Stripe — оптимальний вибір: приймає платіж у будь-якій валюті, конвертує на стороні процесора. ЮKassa працює лише в рублях, потребує конвертації на стороні магазину. CloudPayments підтримує BYN, UAH, USD, EUR.
| Шлюз | Підтримувані валюти | Конвертація на стороні | Рекомендація |
|---|---|---|---|
| Stripe | Будь-які | Ні (автоматична) | Для міжнародної торгівлі |
| ЮKassa | RUB | Потрібна на стороні магазину | Тільки Росія |
| CloudPayments | BYN, UAH, USD, EUR | Ні | Для України та Білорусі |
При оплаті фіксується валюта замовлення та курс на момент оплати:
ALTER TABLE orders ADD COLUMN currency CHAR(3) NOT NULL DEFAULT 'UAH'; 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); Це дозволяє звести звітність в єдиній валюті незалежно від того, в чому платив покупець.
Округлення та анти-патерни
Ніколи не зберігайте гроші в FLOAT — втрата точності при математиці. Завжди використовуйте NUMERIC(12,2) або DECIMAL.
Округлення при конвертації: round($price * $rate, 2, PHP_ROUND_HALF_EVEN) — банківське округлення, помилка не накопичується. При підсумовуванні позицій замовлення спочатку підсумовуємо, потім округлюємо.
Які типові помилки допускають при реалізації мультивалютності?
- Використання
FLOATдля зберігання грошей — втрата точності. - Округлення кожної позиції окремо, а не підсумкової суми.
- Відсутність фіксації курсу на момент замовлення — звіти в базовій валюті будуть розходитися.
- Неврахування податків для різних валют — ПДВ може відрізнятися.
- Змішування стратегій зберігання цін в одному проєкті.
Наші клієнти відзначають зниження помилок у звітності на 95% та економію до 30% часу на звірянні даних після впровадження правильної архітектури.
Що входить у роботу?
- Аудит поточної архітектури та вибір стратегії зберігання цін.
- Проєктування схеми бази даних і міграцій.
- Реалізація модуля валют, автооновлення курсів і форматування.
- Інтеграція вибору валюти в інтерфейс (cookie, профіль, GeoIP).
- Налаштування мультивалютних платежів (Stripe, CloudPayments та ін.).
- Розробка звітів у базовій валюті.
- Документація та навчання команди замовника.
- Підтримка після впровадження.
Ми реалізували мультивалютність у 30+ проєктах, включаючи інтернет-магазини з оборотом понад $10M. Якщо вам потрібна надійна мультивалютність, зв'яжіться з нами для оцінки вашого проєкту.
Строки реалізації
- Базова система (зберігання + перемикач + форматування): від 3 до 4 днів.
- Автооновлення курсів: від 1 дня.
- Автоперерахунок з порогом: від 1–2 днів.
- Мультивалютні платежі (залежить від шлюзу): від 2 до 4 днів.
- Фінансова звітність: від 1–2 днів.
Повна реалізація для магазину з 3–5 валютами займає від 1 до 2 тижнів.
Отримайте консультацію по вашому проєкту — оцінимо обсяг робіт і запропонуємо оптимальне рішення. Зв'яжіться з нами, щоб обговорити деталі.







