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

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

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

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