Руководство

Коды валют и международные платежи

Освойте коды валют ISO 4217 для финтех-приложений. Изучите валютные операции, API курсов валют, локализацию форматирования и интеграцию с платежными системами как Stripe и PayPal.

Pavel Volkov
29 августа 2025 г.
8 мин чтения

Введение в коды валют и международные платежи

Коды валют являются основой международных финансов, электронной коммерции и обработки платежей. Стандарт ISO 4217 определяет трёхбуквенные коды валют, которые обеспечивают беспрепятственные глобальные транзакции, точную финансовую отчётность и согласованное ценообразование на различных рынках.

Понимание кодов валют ISO 4217

Структура и формат

Коды валют ISO 4217 следуют стандартизированному 3-буквенному формату:

  • Формат: XXX (3 заглавные буквы)
  • Первые две буквы: Обычно соответствуют коду страны ISO 3166-1
  • Третья буква: Обычно представляет начальную букву названия валюты
  • Примеры: USD (Доллар США), EUR (Евро), RUB (Российский рубль)

Основные коды валют

Код Валюта Страна/Регион Цифровой код Минимальные единицы
RUB Российский рубль Россия 643 2
USD Доллар США США 840 2
EUR Евро Европейский союз 978 2
JPY Японская йена Япония 392 0
CNY Китайский юань Китай 156 2

Реализация в платёжных системах

Мультивалютная платформа электронной коммерции

Создание надёжной системы обработки валют:

// PHP: Сервис управления валютами
class CurrencyService
{
    private array $supportedCurrencies = [
        'RUB' => ['name' => 'Российский рубль', 'symbol' => '₽', 'decimal_places' => 2],
        'USD' => ['name' => 'Доллар США', 'symbol' => '$', 'decimal_places' => 2],
        'EUR' => ['name' => 'Евро', 'symbol' => '€', 'decimal_places' => 2],
        'GBP' => ['name' => 'Фунт стерлингов', 'symbol' => '£', 'decimal_places' => 2],
        'JPY' => ['name' => 'Японская йена', 'symbol' => '¥', 'decimal_places' => 0],
        'CNY' => ['name' => 'Китайский юань', 'symbol' => '¥', 'decimal_places' => 2]
    ];

    public function formatAmount(float $amount, string $currency): string
    {
        if (!$this->isValidCurrency($currency)) {
            throw new InvalidArgumentException("Неподдерживаемая валюта: {$currency}");
        }

        $config = $this->supportedCurrencies[$currency];
        $formattedAmount = number_format($amount, $config['decimal_places'], '.', ' ');
        
        return $formattedAmount . ' ' . $config['symbol'];
    }

    public function convertAmount(
        float $amount, 
        string $fromCurrency, 
        string $toCurrency,
        array $exchangeRates = null
    ): float {
        if ($fromCurrency === $toCurrency) {
            return $amount;
        }

        $rates = $exchangeRates ?? $this->getExchangeRates();
        
        // Конвертируем в USD сначала, если не USD
        if ($fromCurrency !== 'USD') {
            $amount = $amount / $rates[$fromCurrency];
        }
        
        // Конвертируем из USD в целевую валюту, если не USD
        if ($toCurrency !== 'USD') {
            $amount = $amount * $rates[$toCurrency];
        }

        return round($amount, $this->supportedCurrencies[$toCurrency]['decimal_places']);
    }

    public function isValidCurrency(string $currency): bool
    {
        return isset($this->supportedCurrencies[$currency]);
    }

    public function getExchangeRates(): array
    {
        // Кеширование курсов валют на 1 час
        return Cache::remember('exchange_rates', 3600, function () {
            return $this->fetchExchangeRates();
        });
    }

    private function fetchExchangeRates(): array
    {
        // Интеграция с API курсов валют
        $response = Http::get('https://api.exchangerate-api.com/v4/latest/USD');
        return $response->json()['rates'] ?? [];
    }
}

Интеграция платежей Stripe

Обработка нескольких валют с помощью Stripe:

// JavaScript: Обработка мультивалютных платежей
class PaymentProcessor {
    constructor(stripePublicKey) {
        this.stripe = Stripe(stripePublicKey);
        this.supportedCurrencies = new Set([
            'RUB', 'USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY', 'SEK'
        ]);
    }

    async createPaymentIntent(amount, currency, customerCountry = null) {
        if (!this.supportedCurrencies.has(currency.toUpperCase())) {
            throw new Error(`Валюта ${currency} не поддерживается`);
        }

        const paymentData = {
            amount: this.convertToMinorUnits(amount, currency),
            currency: currency.toLowerCase(),
            automatic_payment_methods: {
                enabled: true
            },
            metadata: {
                customer_country: customerCountry,
                original_amount: amount,
                display_currency: currency
            }
        };

        // Добавление способов оплаты для конкретной страны
        if (customerCountry) {
            paymentData.payment_method_types = this.getPaymentMethodsForCountry(
                customerCountry, 
                currency
            );
        }

        try {
            const response = await fetch('/api/create-payment-intent', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(paymentData)
            });

            const { clientSecret, paymentIntentId } = await response.json();
            
            return {
                clientSecret,
                paymentIntentId,
                displayAmount: this.formatCurrency(amount, currency)
            };
        } catch (error) {
            console.error('Создание платёжного намерения не удалось:', error);
            throw error;
        }
    }

    convertToMinorUnits(amount, currency) {
        const zeroCurrencies = ['JPY', 'KRW', 'VND', 'CLP'];
        const threeDecimalCurrencies = ['BHD', 'JOD', 'KWD', 'OMR'];
        
        if (zeroCurrencies.includes(currency.toUpperCase())) {
            return Math.round(amount);
        } else if (threeDecimalCurrencies.includes(currency.toUpperCase())) {
            return Math.round(amount * 1000);
        } else {
            return Math.round(amount * 100); // Большинство валют используют 2 знака после запятой
        }
    }

    formatCurrency(amount, currency, locale = 'ru-RU') {
        return new Intl.NumberFormat(locale, {
            style: 'currency',
            currency: currency.toUpperCase(),
            currencyDisplay: 'symbol'
        }).format(amount);
    }

    getPaymentMethodsForCountry(country, currency) {
        const countryMethods = {
            'RU': ['card'],
            'US': ['card', 'apple_pay', 'google_pay'],
            'GB': ['card', 'apple_pay', 'google_pay'],
            'DE': ['card', 'sepa_debit', 'sofort', 'giropay'],
            'NL': ['card', 'ideal', 'sepa_debit'],
            'FR': ['card', 'sepa_debit'],
            'JP': ['card'],
            'CN': ['card', 'alipay', 'wechat_pay']
        };

        return countryMethods[country] || ['card'];
    }
}

Интеграция с API курсов валют

Создание комплексного сервиса курсов валют:

// Python: Сервис курсов валют с несколькими провайдерами
import requests
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import redis

class ExchangeRateService:
    def __init__(self):
        self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
        self.api_providers = {
            'primary': {
                'url': 'https://api.exchangerate-api.com/v4/latest/{base}',
                'key_required': False
            },
            'fallback': {
                'url': 'https://api.fixer.io/latest?access_key={key}&base={base}',
                'key_required': True
            }
        }
        
    def get_exchange_rate(
        self, 
        base_currency: str, 
        target_currency: str, 
        use_cache: bool = True
    ) -> float:
        """Получение курса валют от базовой валюты к целевой."""
        
        if base_currency == target_currency:
            return 1.0
            
        cache_key = f"exchange_rate:{base_currency}:{target_currency}"
        
        if use_cache:
            cached_rate = self.redis_client.get(cache_key)
            if cached_rate:
                return float(cached_rate.decode())
        
        rates = self.fetch_exchange_rates(base_currency)
        target_rate = rates.get(target_currency)
        
        if target_rate is None:
            raise ValueError(f"Курс валют не найден для {base_currency} к {target_currency}")
        
        # Кеширование на 1 час
        self.redis_client.setex(cache_key, 3600, str(target_rate))
        return float(target_rate)
    
    def convert_amount(
        self, 
        amount: float, 
        from_currency: str, 
        to_currency: str
    ) -> Dict[str, any]:
        """Конвертация суммы из одной валюты в другую."""
        
        rate = self.get_exchange_rate(from_currency, to_currency)
        converted_amount = amount * rate
        
        # Округление до соответствующего количества знаков после запятой
        decimal_places = self.get_currency_decimal_places(to_currency)
        converted_amount = round(converted_amount, decimal_places)
        
        return {
            'original_amount': amount,
            'original_currency': from_currency,
            'converted_amount': converted_amount,
            'target_currency': to_currency,
            'exchange_rate': rate,
            'conversion_date': datetime.now().isoformat(),
            'formatted_amount': self.format_currency(converted_amount, to_currency)
        }
    
    def get_multiple_rates(
        self, 
        base_currency: str, 
        target_currencies: List[str]
    ) -> Dict[str, float]:
        """Получение курсов валют для нескольких валют."""
        
        rates = {}
        base_rates = self.fetch_exchange_rates(base_currency)
        
        for currency in target_currencies:
            if currency in base_rates:
                rates[currency] = base_rates[currency]
            else:
                # Попытка обратного поиска
                try:
                    reverse_rate = self.get_exchange_rate(currency, base_currency)
                    rates[currency] = 1 / reverse_rate if reverse_rate != 0 else 0
                except ValueError:
                    rates[currency] = None
                    
        return rates
    
    def get_currency_decimal_places(self, currency: str) -> int:
        """Получение количества знаков после запятой для валюты."""
        zero_decimal_currencies = {'JPY', 'KRW', 'VND', 'CLP', 'GNF', 'ISK'}
        three_decimal_currencies = {'BHD', 'JOD', 'KWD', 'OMR', 'TND'}
        
        if currency in zero_decimal_currencies:
            return 0
        elif currency in three_decimal_currencies:
            return 3
        else:
            return 2
    
    def format_currency(self, amount: float, currency: str) -> str:
        """Форматирование суммы валюты для отображения."""
        currency_symbols = {
            'RUB': '₽',
            'USD': '$',
            'EUR': '€',
            'GBP': '£',
            'JPY': '¥',
            'CNY': '¥'
        }
        
        symbol = currency_symbols.get(currency, currency)
        decimal_places = self.get_currency_decimal_places(currency)
        
        formatted = f"{amount:,.{decimal_places}f}"
        return f"{formatted} {symbol}"

Финансовые вычисления и округление

Точные финансовые расчёты

Правильная обработка точности валют:

// PHP: Сервис финансовых вычислений
use Brick\Money\Money;
use Brick\Money\Currency;

class FinancialCalculator
{
    public function calculateOrderTotal(array $items, string $currency, float $taxRate = 0): Money
    {
        $total = Money::zero(Currency::of($currency));
        
        foreach ($items as $item) {
            $itemPrice = Money::of($item['price'], $currency);
            $itemTotal = $itemPrice->multipliedBy($item['quantity']);
            $total = $total->plus($itemTotal);
        }
        
        if ($taxRate > 0) {
            $tax = $total->multipliedBy($taxRate);
            $total = $total->plus($tax);
        }
        
        return $total;
    }
    
    public function applyDiscount(Money $amount, float $discountPercent): array
    {
        $discountAmount = $amount->multipliedBy($discountPercent / 100);
        $finalAmount = $amount->minus($discountAmount);
        
        return [
            'original_amount' => $amount,
            'discount_amount' => $discountAmount,
            'discount_percent' => $discountPercent,
            'final_amount' => $finalAmount,
            'savings' => $discountAmount->getAmount()->toFloat()
        ];
    }
    
    public function calculateInstallments(
        Money $principal, 
        float $annualInterestRate, 
        int $termMonths
    ): array {
        $monthlyRate = $annualInterestRate / 12 / 100;
        $amount = $principal->getAmount()->toFloat();
        
        if ($monthlyRate == 0) {
            $monthlyPayment = $amount / $termMonths;
        } else {
            $monthlyPayment = $amount * 
                ($monthlyRate * pow(1 + $monthlyRate, $termMonths)) / 
                (pow(1 + $monthlyRate, $termMonths) - 1);
        }
        
        $monthlyPaymentMoney = Money::of($monthlyPayment, $principal->getCurrency());
        $totalAmount = $monthlyPaymentMoney->multipliedBy($termMonths);
        $totalInterest = $totalAmount->minus($principal);
        
        return [
            'principal' => $principal,
            'monthly_payment' => $monthlyPaymentMoney,
            'total_amount' => $totalAmount,
            'total_interest' => $totalInterest,
            'term_months' => $termMonths,
            'annual_rate' => $annualInterestRate
        ];
    }
}

Локализация и форматирование

Отображение валют по локализации

// JavaScript: Международное форматирование валют
class CurrencyFormatter {
    constructor() {
        this.localeMapping = {
            'RUB': ['ru-RU'],
            'USD': ['en-US', 'es-US'],
            'EUR': ['de-DE', 'fr-FR', 'it-IT', 'es-ES'],
            'GBP': ['en-GB'],
            'JPY': ['ja-JP'],
            'CNY': ['zh-CN']
        };
    }

    formatForLocale(amount, currency, userLocale = 'ru-RU') {
        // Валидация входных данных
        if (typeof amount !== 'number' || isNaN(amount)) {
            throw new Error('Сумма должна быть действительным числом');
        }

        if (!this.isValidCurrencyCode(currency)) {
            throw new Error(`Недействительный код валюты: ${currency}`);
        }

        try {
            return new Intl.NumberFormat(userLocale, {
                style: 'currency',
                currency: currency.toUpperCase(),
                minimumFractionDigits: this.getMinimumFractionDigits(currency),
                maximumFractionDigits: this.getMaximumFractionDigits(currency)
            }).format(amount);
        } catch (error) {
            // Резервный вариант ru-RU, если локаль пользователя не работает
            return new Intl.NumberFormat('ru-RU', {
                style: 'currency',
                currency: currency.toUpperCase()
            }).format(amount);
        }
    }

    formatMultipleLocales(amount, currency) {
        const locales = this.localeMapping[currency.toUpperCase()] || ['ru-RU'];
        const formats = {};

        locales.forEach(locale => {
            try {
                formats[locale] = this.formatForLocale(amount, currency, locale);
            } catch (error) {
                console.warn(`Не удалось отформатировать для локали ${locale}:`, error);
            }
        });

        return formats;
    }

    getCompactFormat(amount, currency, userLocale = 'ru-RU') {
        return new Intl.NumberFormat(userLocale, {
            style: 'currency',
            currency: currency.toUpperCase(),
            notation: 'compact',
            compactDisplay: 'short'
        }).format(amount);
    }

    isValidCurrencyCode(currency) {
        return /^[A-Z]{3}$/.test(currency);
    }

    getMinimumFractionDigits(currency) {
        const zeroDecimalCurrencies = ['JPY', 'KRW', 'VND', 'CLP'];
        return zeroDecimalCurrencies.includes(currency.toUpperCase()) ? 0 : 2;
    }

    getMaximumFractionDigits(currency) {
        const zeroDecimalCurrencies = ['JPY', 'KRW', 'VND', 'CLP'];
        const threeDecimalCurrencies = ['BHD', 'JOD', 'KWD', 'OMR'];
        
        if (zeroDecimalCurrencies.includes(currency.toUpperCase())) {
            return 0;
        } else if (threeDecimalCurrencies.includes(currency.toUpperCase())) {
            return 3;
        } else {
            return 2;
        }
    }

    // Форматирование для бухгалтерского учёта (отрицательные числа в скобках)
    formatAccounting(amount, currency, userLocale = 'ru-RU') {
        return new Intl.NumberFormat(userLocale, {
            style: 'currency',
            currency: currency.toUpperCase(),
            currencySign: 'accounting'
        }).format(amount);
    }
}

Схема базы данных для валютных данных

-- Схема базы данных управления валютами
CREATE TABLE currencies (
    code CHAR(3) PRIMARY KEY,
    numeric_code SMALLINT UNIQUE NOT NULL,
    name_ru VARCHAR(100) NOT NULL,
    name_en VARCHAR(100) NOT NULL,
    symbol VARCHAR(10),
    decimal_places TINYINT DEFAULT 2,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_active (is_active),
    INDEX idx_decimal_places (decimal_places)
);

CREATE TABLE exchange_rates (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    base_currency CHAR(3) NOT NULL,
    target_currency CHAR(3) NOT NULL,
    rate DECIMAL(20, 10) NOT NULL,
    source VARCHAR(50) NOT NULL,
    valid_from TIMESTAMP NOT NULL,
    valid_until TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (base_currency) REFERENCES currencies(code),
    FOREIGN KEY (target_currency) REFERENCES currencies(code),
    INDEX idx_currencies (base_currency, target_currency),
    INDEX idx_valid_period (valid_from, valid_until),
    INDEX idx_created (created_at),
    UNIQUE KEY unique_rate_period (base_currency, target_currency, valid_from)
);

Соответствие требованиям и регулирование

Проверки на соблюдение законодательства о борьбе с отмыванием денег (AML)

// PHP: Сервис проверки соответствия требованиям
class ComplianceService
{
    private array $restrictedCountries = [
        // Страны под санкциями
        'IR', 'KP', 'SY', 'CU', 'MM'
    ];

    public function checkTransactionCompliance(
        float $amount, 
        string $currency, 
        string $fromCountry, 
        string $toCountry
    ): array {
        $checks = [];
        
        // Проверки пороговых сумм
        $checks['amount_threshold'] = $this->checkAmountThresholds($amount, $currency);
        
        // Проверки ограниченных стран
        $checks['restricted_countries'] = $this->checkRestrictedCountries($fromCountry, $toCountry);
        
        // Валютные ограничения
        $checks['currency_restrictions'] = $this->checkCurrencyRestrictions($currency, $fromCountry, $toCountry);
        
        // Расчёт общего соответствия
        $checks['is_compliant'] = !in_array(false, array_column($checks, 'status'));
        $checks['requires_kyc'] = $amount >= $this->getKYCThreshold($currency);
        
        return $checks;
    }

    private function checkAmountThresholds(float $amount, string $currency): array
    {
        $thresholds = [
            'RUB' => 600000, // ~$10,000 USD эквивалент
            'USD' => 10000,
            'EUR' => 10000,
            'default' => 10000 // USD эквивалент
        ];

        $threshold = $thresholds[$currency] ?? $thresholds['default'];
        $requiresReporting = $amount >= $threshold;

        return [
            'status' => true, // Только сумма не блокирует транзакцию
            'requires_reporting' => $requiresReporting,
            'threshold' => $threshold,
            'amount' => $amount
        ];
    }

    private function getKYCThreshold(string $currency): float
    {
        $thresholds = [
            'RUB' => 180000, // ~$3000 USD эквивалент
            'USD' => 3000,
            'EUR' => 3000,
            'default' => 3000
        ];

        return $thresholds[$currency] ?? $thresholds['default'];
    }
}

Лучшие практики

Руководства по обработке валют

  • Всегда используйте коды ISO 4217 - Никогда не используйте символы или сокращения
  • Храните суммы в наименьших единицах - Избегайте проблем с точностью чисел с плавающей запятой
  • Правильно кешируйте курсы валют - Баланс между точностью и производительностью
  • Обрабатывайте округление последовательно - Используйте правильные правила финансового округления
  • Валидируйте коды валют - Проверяйте против официального списка ISO
  • Учитывайте нормативные требования - Реализуйте проверки соответствия
  • Планируйте изменения валют - Исторические курсы и прекращённые валюты

Реализация надёжной обработки валют необходима для любого международного приложения, работающего с финансовыми транзакциями. Следуя стандартам ISO, используя соответствующие библиотеки и реализуя правильную валидацию и кеширование, вы можете создать системы, которые надёжно и точно обрабатывают глобальные платежи.

Последнее обновление: 22 сентября 2025 г.