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

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

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

Інформаційні сайти або веб-програми
Сайти візитки, 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

Реалізація регіональних цін та валют на веб-сайті

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

Сценарії використання

  • Багатовалютний магазин — ціни в RUB, USD, EUR, з автоматичною конвертацією або ручними прайс-листами
  • Регіональні ціни в межах країни — Москва та інші регіони можуть мати різні ціни
  • Контрактні ціни — дилерські та B2B ціни для окремих клієнтів або груп

Архітектура

Запит → RegionDetector (IP/Cookie/URL) → PriceResolver → Форматування

Схема даних

CREATE TABLE currencies (
    code        CHAR(3) PRIMARY KEY,   -- 'RUB', 'USD', 'EUR', 'BYN'
    symbol      VARCHAR(5) NOT NULL,   -- '₽', '$', '€', 'Br'
    symbol_pos  VARCHAR(10) DEFAULT 'after', -- 'before'|'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),    -- 'cbr', 'manual', 'ecb'
    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)[],      -- ['RU'], ['BY'], ['KZ']
    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
    {
        // 1. Явний вибір користувача (сесія / cookie)
        if ($request->session()->has('price_region')) {
            $region = PriceRegion::find($request->session()->get('price_region'));
            if ($region) return $region;
        }

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

        // 3. IP-геолокація
        $countryCode = $this->geoip->getCountry($request->ip());
        if ($countryCode) {
            $region = PriceRegion::whereJsonContains('country_codes', $countryCode)->first();
            if ($region) return $region;
        }

        // 4. Типовий регіон
        return PriceRegion::where('is_default', true)->firstOrFail();
    }
}

IP-геолокація через MaxMind GeoLite2:

use MaxMind\Db\Reader;

class MaxMindGeoIp
{
    public function getCountry(string $ip): ?string
    {
        $reader = new Reader(storage_path('app/GeoLite2-Country.mmdb'));
        try {
            $record = $reader->country($ip);
            return $record->country->isoCode;
        } catch (\Exception $e) {
            return null;
        } finally {
            $reader->close();
        }
    }
}

Сервіс ціноутворення

class RegionalPriceService
{
    public function getPrice(Product $product, PriceRegion $region): RegionalPrice
    {
        // 1. Перевірити, чи існує ручна ціна для регіону
        $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,
            );
        }

        // 2. Автоматична конвертація з базової ціни
        $basePrice = $product->price;  // у базовій валюті (RUB)
        $rate      = $this->getRate('RUB', $region->currency->code);

        $converted = $this->roundByCurrency(
            amount:   $basePrice * $rate,
            currency: $region->currency,
        );

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

    private function roundByCurrency(float $amount, Currency $currency): float
    {
        // Психологічне округлення для кожної валюти
        return match ($currency->code) {
            'RUB' => $this->roundTo99($amount, 1),      // 1299, 4999, 29990
            'USD' => $this->roundTo99($amount, 0.01),   // 29.99, 149.95
            'EUR' => $this->roundTo99($amount, 0.01),
            'BYN' => round($amount * 2) / 2,            // кратно 0.50
            default => round($amount, $currency->decimals),
        };
    }

    private function roundTo99(float $amount, float $step): float
    {
        $rounded = ceil($amount / $step) * $step;
        // Замінити останні цифри на 9: 1301 → 1299
        if ($step >= 1) {
            $magnitude = 10 ** (strlen((int)$rounded) - 2);
            return floor($rounded / $magnitude) * $magnitude + ($magnitude - 1);
        }
        return $rounded;
    }
}

Автоматичне оновлення курсів обміну

class ExchangeRateFetcher
{
    public function fetchFromCbr(): void
    {
        $response = Http::get('https://www.cbr.ru/scripts/XML_daily.asp');
        $xml      = simplexml_load_string($response->body());

        $rates = [];
        foreach ($xml->Valute as $valute) {
            $code    = (string) $valute->CharCode;
            $nominal = (float) $valute->Nominal;
            $value   = (float) str_replace(',', '.', (string) $valute->Value);

            if (in_array($code, ['USD', 'EUR', 'BYN', 'KZT'])) {
                ExchangeRate::updateOrCreate(
                    ['from_currency' => 'RUB', 'to_currency' => $code],
                    [
                        'rate'       => $nominal / $value,
                        'source'     => 'cbr',
                        'fetched_at' => now(),
                    ]
                );
            }
        }
    }
}

// Розклад оновлення курсів
$schedule->job(new FetchExchangeRatesJob)->dailyAt('10:00');

Middleware та контекст

class SetPriceRegionMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $region = $this->detector->detect($request);

        // Передати до контексту додатку
        app()->instance('price_region', $region);

        // Для Inertia.js — передати на фронтенд
        Inertia::share('priceRegion', [
            'id'       => $region->id,
            'currency' => [
                'code'   => $region->currency->code,
                'symbol' => $region->currency->symbol,
                'pos'    => $region->currency->symbol_pos,
            ],
        ]);

        return $next($request);
    }
}

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

interface Currency {
  code: string;
  symbol: string;
  pos: 'before' | 'after';
  decimals: number;
}

function formatPrice(amount: number, currency: Currency): string {
  const formatted = new Intl.NumberFormat('ru-RU', {
    minimumFractionDigits: currency.decimals,
    maximumFractionDigits: currency.decimals,
  }).format(amount);

  return currency.pos === 'before'
    ? `${currency.symbol}${formatted}`
    : `${formatted} ${currency.symbol}`;
}

Віджет перемикання регіону

const RegionSwitcher: React.FC = () => {
  const { priceRegion } = usePage<{ priceRegion: PriceRegion }>().props;
  const { data: regions } = useQuery(['regions'], fetchRegions);

  return (
    <select
      value={priceRegion.id}
      onChange={e => {
        router.post('/region/switch', { region_id: e.target.value });
      }}
      className="text-sm border rounded px-2 py-1"
    >
      {regions?.map(r => (
        <option key={r.id} value={r.id}>{r.currency.code} — {r.name}</option>
      ))}
    </select>
  );
};

Графік реалізації

  • Схема даних + RegionDetector + PriceService: 1–2 дні
  • Автоматичне оновлення курсів (Центральний банк): 0.5 дня
  • Middleware + Inertia-шеринг: 0.5 дня
  • Форматування фронтенду + перемикання регіону: 1 день
  • Ручні ціни в інтерфейсі адміністратора: 1 день

Всього: 4–5 робочих днів.