Погано реалізована мультивалютність породжує розбіжності в бухгалтерії, баги з округленням і проблеми з ПДВ. Великий інтернет-магазин втратив значну суму через те, що ціни округлювалися вниз на користь покупця. Наш досвід дозволяє уникнути цих помилок і гарантує точність фінансових даних. Понад 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 | USD | Потрібна на стороні магазину | Тільки Росія |
| 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 тижнів.
Отримайте консультацію по вашому проєкту — оцінимо обсяг робіт і запропонуємо оптимальне рішення. Зв'яжіться з нами, щоб обговорити деталі.







