Регіональні ціни та мультивалютність на сайті: технічна реалізація

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

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

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

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

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

Уявіть: відвідувач з Києва заходить на ваш інтернет-магазин, бачить ціни в доларах і закриває вкладку. Втрата конверсії — до 30%, якщо не адаптувати ціни під регіон. В іншому сценарії клієнт з Казахстану купує товари за цінами, номінованими в долларах, і платить завищену суму через невірний курс. Регіональні ціни та мультивалютність вирішують цю проблему: користувач одразу бачить звичну валюту та коректні ціни. Ми впроваджуємо таку систему — від схеми бази даних до віджету перемикання на фронтенді. Нижче — технічний опис, заснований на реальному проекті з каталогом на 100 000 товарів, 5 регіонами та 3 валютами. Після впровадження конверсія зросла на 25% у перший місяць, а відмови на сторінці кошика знизилися з 40% до 15%.

Як система визначає регіон користувача?

Визначення регіону відбувається за ланцюжком пріоритетів. Спочатку перевіряється явний вибір через віджет — якщо користувач сам вказав регіон, він зберігається в сесії. Потім система дивиться URL-параметр або піддомен (наприклад, kz.site.com). Якщо і це не спрацювало, використовується IP-геолокація через MaxMind GeoLite2. І лише в останню чергу застосовується регіон за замовчуванням. Такий підхід дає гнучкість: користувач у будь-який момент може перемкнутися вручну. Для зниження навантаження результати геолокації кешуються на 24 години — це особливо важливо при пікових навантаженнях, коли кількість запитів перевищує 1000 на хвилину.

Схема даних

CREATE TABLE currencies (
  code CHAR(3) PRIMARY KEY,
  symbol VARCHAR(5) NOT NULL,
  symbol_pos VARCHAR(10) DEFAULT 'after',
  decimals SMALLINT DEFAULT 2,
  thousands_sep VARCHAR(5) DEFAULT ' ',
  decimal_sep VARCHAR(5) DEFAULT '.'
);
CREATE TABLE exchange_rates (
  id BIGSERIAL PRIMARY KEY,
  from_currency CHAR(3) REFERENCES currencies(code),
  to_currency CHAR(3) REFERENCES currencies(code),
  rate NUMERIC(14,6) NOT NULL,
  source VARCHAR(50),
  fetched_at TIMESTAMP DEFAULT NOW(),
  UNIQUE(from_currency, to_currency)
);
CREATE TABLE price_regions (
  id BIGSERIAL PRIMARY KEY,
  name VARCHAR(255),
  currency_code CHAR(3) REFERENCES currencies(code),
  country_codes CHAR(2)[],
  is_default BOOLEAN DEFAULT FALSE
);
CREATE TABLE product_regional_prices (
  id BIGSERIAL PRIMARY KEY,
  product_id BIGINT REFERENCES products(id),
  region_id BIGINT REFERENCES price_regions(id),
  price NUMERIC(12,2) NOT NULL,
  sale_price NUMERIC(12,2),
  UNIQUE(product_id, region_id)
);

Визначення регіону та сервіс цін

class RegionDetector {
    public function detect(Request $request): PriceRegion {
        // Явний вибір у сесії
        if ($request->session()->has('price_region')) {
            $region = PriceRegion::find($request->session()->get('price_region'));
            if ($region) return $region;
        }

        // URL-параметр або піддомен
        if ($regionCode = $this->detectFromUrl($request)) {
            $region = PriceRegion::whereJsonContains('country_codes', $regionCode)->first();
            if ($region) return $region;
        }

        // IP-геолокація через MaxMind GeoLite2
        $countryCode = $this->geoIp->getCountry($request->ip());
        if ($countryCode) {
            $region = PriceRegion::whereJsonContains('country_codes', $countryCode)->first();
            if ($region) return $region;
        }

        // Дефолтний регіон
        return PriceRegion::where('is_default', true)->firstOrFail();
    }
}

class RegionalPriceService {
    public function getPrice(Product $product, PriceRegion $region): RegionalPrice {
        $manual = ProductRegionalPrice::where([
            'product_id' => $product->id,
            'region_id' => $region->id,
        ])->first();

        if ($manual) {
            return new RegionalPrice(
                price: $manual->price,
                salePrice: $manual->sale_price,
                currency: $region->currency,
            );
        }

        // Автоконвертація з базової ціни (USD)
        $basePrice = $product->price;
        $rate = $this->getRate('USD', $region->currency->code);
        $converted = $this->roundByCurrency($basePrice * $rate, $region->currency);

        return new RegionalPrice(
            price: $converted,
            currency: $region->currency,
        );
    }

    private function roundByCurrency(float $amount, Currency $currency): float {
        return match ($currency->code) {
            'USD' => $this->roundTo99($amount, 1),
            'USD' => $this->roundTo99($amount, 0.01),
            'EUR' => $this->roundTo99($amount, 0.01),
            'BYN' => round($amount * 2) / 2,
            default => round($amount, $currency->decimals),
        };
    }

    private function roundTo99(float $amount, float $step): float {
        $rounded = ceil($amount / $step) * $step;
        if ($step >= 1) {
            $magnitude = 10 ** (strlen((int)$rounded) - 2);
            return floor($rounded / $magnitude) * $magnitude + ($magnitude - 1);
        }
        return $rounded;
    }
}

Як налаштувати автооновлення курсів валют?

Курси беруться з ЦБ РФ щоденно. Парсинг XML і запис у таблицю exchange_rates. Задача ставиться в розклад — виконується раз на добу. Якщо потрібна вища частота, можна налаштувати оновлення щогодини, а джерело даних замінити на European Central Bank або інший агрегатор. Для надійності реалізовано чергу з повтором при помилці — курс не застаріє навіть при тимчасовій недоступності джерела.

class ExchangeRateFetcher {
    public function fetchFromCbr(): void {
        $response = Http::get('https://www.cbr.ru/scripts/XML_daily.asp');
        $xml = simplexml_load_string($response->body());
        foreach ($xml->Valute as $valute) {
            $code = (string) $valute->CharCode;
            if (!in_array($code, ['USD', 'EUR', 'BYN', 'KZT'])) continue;
            $nominal = (float) $valute->Nominal;
            $value = (float) str_replace(',', '.', (string) $valute->Value);
            ExchangeRate::updateOrCreate(
                ['from_currency' => 'USD', 'to_currency' => $code],
                ['rate' => $nominal / $value, 'source' => 'cbr', 'fetched_at' => now()]
            );
        }
    }
}
// У розкладі консолі:
$schedule->job(new FetchExchangeRatesJob)->dailyAt('10:00');

Чому не варто зберігати курси в конфігах?

Зберігання курсів у конфігах — класична помилка. Курси застарівають, їх потрібно оновлювати руками, а на кожному сервері — свої значення. База даних з автооновленням вирішує цю проблему: курси єдині для всього додатку, оновлюються за розкладом і завжди актуальні. Це в рази надійніше та простіше в підтримці. Додайте шар кешування (Redis або файловий) — і навантаження на БД знижується до мінімуму навіть при мільйонних каталогах.

Порівняння з альтернативами

Критерій Наше рішення Готові плагіни (наприклад, WooCommerce)
Гнучкість ціноутворення Ручні ціни для будь-якого регіону + автоконвертація Тільки автоконвертація або обмежені зони
Продуктивність Оптимізовані запити, кешування регіону Часто N+1 запит на кожен товар
Підтримка валют Будь-які валюти, налаштовуваний формат Обмежений набір валют
Оновлення курсів Автоматично з ЦБ або іншого джерела Вручну або платні розширення

Наше рішення дає повний контроль над цінами і не просідає по продуктивності навіть на каталогах із мільйонами товарів.

Що входить у реалізацію

Етап Що робимо Термін
Аналітика та проектування Визначаємо регіони, валюти, цінові політики 0,5 дня
Схема даних і бекенд Створення таблиць, сервісів, middleware 1–2 дні
Автооновлення курсів Інтеграція з ЦБ РФ, налаштування розкладу 0,5 дня
Фронтенд Форматування цін, віджет зміни регіону на React 1 день
Адмінка Інтерфейс для ручних прайсів 1 день

Підсумкова реалізація займає 4–5 робочих днів. Вартість розраховується індивідуально після уточнення обсягів.

Типові помилки при впровадженні

  • Забувають про округлення — копійки після конвертації виглядають дивно.
  • Не враховують N+1 запит при визначенні регіону на кожен товар.
  • Зберігають курси в конфігах, а не в БД з автооновленням.
  • Ігнорують кешування результатів геолокації — зростання часу відповіді на 50–100 мс на кожному запиті.
Деталі кешування геолокації

Кешування реалізується через Redis з TTL 24 години. Ключ — IP-адреса, значення — код країни. При пікових навантаженнях (більше 10 000 запитів на хвилину) це знижує навантаження на сервіс геолокації на 99%.

Отримати консультацію

Зв'яжіться з нами — оцінимо ваш проект, підберемо оптимальну архітектуру. У нас 10+ років досвіду в розробці інтернет-магазинів та більше 40 успішних впроваджень. Гарантуємо результат. Замовте розробку системи мультивалютності вже сьогодні.

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

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

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1543
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1374
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060