Guide

Currency Codes and International Payments

Master ISO 4217 currency codes for fintech applications. Learn about currency operations, exchange rate APIs, localization formatting, and integration with payment systems like Stripe and PayPal.

Pavel Volkov
Aug 31, 2025
13 min read

Introduction to Currency Codes and International Payments

Currency codes are fundamental to international finance, e-commerce, and payment processing. The ISO 4217 standard defines three-letter currency codes that enable seamless global transactions, accurate financial reporting, and consistent pricing across different markets.

Understanding ISO 4217 Currency Codes

Structure and Format

ISO 4217 currency codes follow a standardized 3-letter format:

  • Format: XXX (3 uppercase letters)
  • First two letters: Usually correspond to ISO 3166-1 country code
  • Third letter: Usually represents the currency name initial
  • Examples: USD (US Dollar), EUR (Euro), GBP (Great Britain Pound)

Major Currency Codes

Code Currency Country/Region Numeric Code Minor Units
USD US Dollar United States 840 2
EUR Euro European Union 978 2
GBP Pound Sterling United Kingdom 826 2
JPY Japanese Yen Japan 392 0
CNY Chinese Yuan China 156 2

Implementation in Payment Systems

Multi-Currency E-commerce Platform

Building a robust currency handling system:

// PHP: Currency management service
class CurrencyService
{
    private array $supportedCurrencies = [
        'USD' => ['name' => 'US Dollar', 'symbol' => '$', 'decimal_places' => 2],
        'EUR' => ['name' => 'Euro', 'symbol' => '€', 'decimal_places' => 2],
        'GBP' => ['name' => 'Pound Sterling', 'symbol' => '£', 'decimal_places' => 2],
        'JPY' => ['name' => 'Japanese Yen', 'symbol' => '¥', 'decimal_places' => 0],
        'CNY' => ['name' => 'Chinese Yuan', 'symbol' => '¥', 'decimal_places' => 2],
        'RUB' => ['name' => 'Russian Ruble', 'symbol' => '₽', 'decimal_places' => 2]
    ];

    public function formatAmount(float $amount, string $currency): string
    {
        if (!$this->isValidCurrency($currency)) {
            throw new InvalidArgumentException("Unsupported currency: {$currency}");
        }

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

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

        $rates = $exchangeRates ?? $this->getExchangeRates();
        
        // Convert to USD first if not USD
        if ($fromCurrency !== 'USD') {
            $amount = $amount / $rates[$fromCurrency];
        }
        
        // Convert from USD to target currency if not 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
    {
        // Cache exchange rates for 1 hour
        return Cache::remember('exchange_rates', 3600, function () {
            return $this->fetchExchangeRates();
        });
    }

    private function fetchExchangeRates(): array
    {
        // Integration with exchange rate API
        $response = Http::get('https://api.exchangerate-api.com/v4/latest/USD');
        return $response->json()['rates'] ?? [];
    }
}

Stripe Payment Integration

Handling multiple currencies with Stripe:

// JavaScript: Multi-currency payment processing
class PaymentProcessor {
    constructor(stripePublicKey) {
        this.stripe = Stripe(stripePublicKey);
        this.supportedCurrencies = new Set([
            'USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY', 'SEK', 'NZD'
        ]);
    }

    async createPaymentIntent(amount, currency, customerCountry = null) {
        if (!this.supportedCurrencies.has(currency.toUpperCase())) {
            throw new Error(`Currency ${currency} is not supported`);
        }

        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
            }
        };

        // Add country-specific payment methods
        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('Payment intent creation failed:', 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); // Most currencies use 2 decimal places
        }
    }

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

    getPaymentMethodsForCountry(country, currency) {
        const countryMethods = {
            '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'],
            'RU': ['card'],
            'JP': ['card'],
            'CN': ['card', 'alipay', 'wechat_pay']
        };

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

Exchange Rate API Integration

Building a comprehensive exchange rate service:

// Python: Exchange rate service with multiple providers
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:
        """Get exchange rate from base currency to target currency."""
        
        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"Exchange rate not found for {base_currency} to {target_currency}")
        
        # Cache for 1 hour
        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]:
        """Convert amount from one currency to another."""
        
        rate = self.get_exchange_rate(from_currency, to_currency)
        converted_amount = amount * rate
        
        # Round to appropriate decimal places
        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]:
        """Get exchange rates for multiple currencies."""
        
        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 lookup
                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 fetch_exchange_rates(self, base_currency: str) -> Dict[str, float]:
        """Fetch exchange rates from API with fallback."""
        
        # Try primary provider first
        try:
            url = self.api_providers['primary']['url'].format(base=base_currency)
            response = requests.get(url, timeout=10)
            response.raise_for_status()
            
            data = response.json()
            return data.get('rates', {})
            
        except Exception as e:
            print(f"Primary provider failed: {e}")
            
        # Fallback to secondary provider
        try:
            api_key = os.getenv('FIXER_API_KEY')
            if not api_key:
                raise ValueError("Fixer API key not configured")
                
            url = self.api_providers['fallback']['url'].format(
                key=api_key, 
                base=base_currency
            )
            response = requests.get(url, timeout=10)
            response.raise_for_status()
            
            data = response.json()
            return data.get('rates', {})
            
        except Exception as e:
            raise Exception(f"All exchange rate providers failed: {e}")
    
    def get_currency_decimal_places(self, currency: str) -> int:
        """Get number of decimal places for currency."""
        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:
        """Format currency amount for display."""
        currency_symbols = {
            'USD': '$',
            'EUR': '€',
            'GBP': '£',
            'JPY': '¥',
            'CNY': '¥',
            'RUB': '₽',
            'CAD': 'C$',
            'AUD': 'A$'
        }
        
        symbol = currency_symbols.get(currency, currency)
        decimal_places = self.get_currency_decimal_places(currency)
        
        formatted = f"{amount:,.{decimal_places}f}"
        return f"{symbol}{formatted}"

Financial Calculations and Rounding

Precise Financial Calculations

Handling currency precision correctly:

// PHP: Financial calculation service
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
        ];
    }
}

Localization and Formatting

Currency Display by Locale

// JavaScript: International currency formatting
class CurrencyFormatter {
    constructor() {
        this.localeMapping = {
            'USD': ['en-US', 'es-US'],
            'EUR': ['de-DE', 'fr-FR', 'it-IT', 'es-ES'],
            'GBP': ['en-GB'],
            'JPY': ['ja-JP'],
            'CNY': ['zh-CN'],
            'RUB': ['ru-RU'],
            'CAD': ['en-CA', 'fr-CA'],
            'AUD': ['en-AU']
        };
    }

    formatForLocale(amount, currency, userLocale = 'en-US') {
        // Validate inputs
        if (typeof amount !== 'number' || isNaN(amount)) {
            throw new Error('Amount must be a valid number');
        }

        if (!this.isValidCurrencyCode(currency)) {
            throw new Error(`Invalid currency code: ${currency}`);
        }

        try {
            return new Intl.NumberFormat(userLocale, {
                style: 'currency',
                currency: currency.toUpperCase(),
                minimumFractionDigits: this.getMinimumFractionDigits(currency),
                maximumFractionDigits: this.getMaximumFractionDigits(currency)
            }).format(amount);
        } catch (error) {
            // Fallback to en-US if user locale fails
            return new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: currency.toUpperCase()
            }).format(amount);
        }
    }

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

        locales.forEach(locale => {
            try {
                formats[locale] = this.formatForLocale(amount, currency, locale);
            } catch (error) {
                console.warn(`Failed to format for locale ${locale}:`, error);
            }
        });

        return formats;
    }

    getCompactFormat(amount, currency, userLocale = 'en-US') {
        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;
        }
    }

    // Format for accounting (negative numbers in parentheses)
    formatAccounting(amount, currency, userLocale = 'en-US') {
        return new Intl.NumberFormat(userLocale, {
            style: 'currency',
            currency: currency.toUpperCase(),
            currencySign: 'accounting'
        }).format(amount);
    }
}

Database Schema for Currency Data

-- Currency management database schema
CREATE TABLE currencies (
    code CHAR(3) PRIMARY KEY,
    numeric_code SMALLINT UNIQUE NOT NULL,
    name_en VARCHAR(100) NOT NULL,
    name_local VARCHAR(100),
    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)
);

CREATE TABLE currency_country_mapping (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    currency_code CHAR(3) NOT NULL,
    country_code CHAR(2) NOT NULL,
    is_primary BOOLEAN DEFAULT TRUE,
    
    FOREIGN KEY (currency_code) REFERENCES currencies(code),
    INDEX idx_currency (currency_code),
    INDEX idx_country (country_code),
    UNIQUE KEY unique_mapping (currency_code, country_code)
);

Cryptocurrency Integration

Digital Currency Support

// Node.js: Cryptocurrency exchange integration
const ccxt = require('ccxt');

class CryptoCurrencyService {
    constructor() {
        this.exchanges = {
            binance: new ccxt.binance(),
            coinbase: new ccxt.coinbasepro(),
            kraken: new ccxt.kraken()
        };
        
        this.supportedCryptos = [
            'BTC', 'ETH', 'LTC', 'XRP', 'ADA', 'DOT', 'LINK', 'BNB'
        ];
    }

    async getCryptoPrices(cryptos = this.supportedCryptos, fiatCurrency = 'USD') {
        const prices = {};
        
        for (const crypto of cryptos) {
            const symbol = `${crypto}/${fiatCurrency}`;
            
            try {
                // Try multiple exchanges for best price
                const exchangePrices = await Promise.allSettled([
                    this.exchanges.binance.fetchTicker(symbol),
                    this.exchanges.coinbase.fetchTicker(symbol),
                    this.exchanges.kraken.fetchTicker(symbol)
                ]);

                const validPrices = exchangePrices
                    .filter(result => result.status === 'fulfilled')
                    .map(result => result.value.last);

                if (validPrices.length > 0) {
                    prices[crypto] = {
                        price: this.calculateAveragePrice(validPrices),
                        currency: fiatCurrency,
                        last_updated: new Date().toISOString(),
                        sources: validPrices.length
                    };
                }
            } catch (error) {
                console.warn(`Failed to get price for ${crypto}:`, error.message);
            }
        }

        return prices;
    }

    async convertCryptoToFiat(cryptoAmount, cryptoSymbol, fiatCurrency) {
        const prices = await this.getCryptoPrices([cryptoSymbol], fiatCurrency);
        
        if (!prices[cryptoSymbol]) {
            throw new Error(`Price not available for ${cryptoSymbol}`);
        }

        const fiatAmount = cryptoAmount * prices[cryptoSymbol].price;
        
        return {
            crypto_amount: cryptoAmount,
            crypto_symbol: cryptoSymbol,
            fiat_amount: fiatAmount,
            fiat_currency: fiatCurrency,
            exchange_rate: prices[cryptoSymbol].price,
            conversion_time: new Date().toISOString()
        };
    }

    calculateAveragePrice(prices) {
        return prices.reduce((sum, price) => sum + price, 0) / prices.length;
    }

    formatCryptoAmount(amount, symbol, decimals = 8) {
        return `${amount.toFixed(decimals)} ${symbol}`;
    }
}

Compliance and Regulations

Anti-Money Laundering (AML) Checks

// PHP: Compliance checking service
class ComplianceService
{
    private array $restrictedCountries = [
        // OFAC sanctioned countries
        'IR', 'KP', 'SY', 'CU', 'MM'
    ];
    
    private array $cryptoRestrictedCountries = [
        'CN', 'BD', 'NP', 'PK', 'AF'
    ];

    public function checkTransactionCompliance(
        float $amount, 
        string $currency, 
        string $fromCountry, 
        string $toCountry
    ): array {
        $checks = [];
        
        // Amount threshold checks
        $checks['amount_threshold'] = $this->checkAmountThresholds($amount, $currency);
        
        // Restricted country checks
        $checks['restricted_countries'] = $this->checkRestrictedCountries($fromCountry, $toCountry);
        
        // Currency-specific checks
        $checks['currency_restrictions'] = $this->checkCurrencyRestrictions($currency, $fromCountry, $toCountry);
        
        // Calculate overall compliance
        $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 = [
            'USD' => 10000,
            'EUR' => 10000,
            'GBP' => 10000,
            'default' => 10000 // USD equivalent
        ];

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

        return [
            'status' => true, // Amount alone doesn't block transaction
            'requires_reporting' => $requiresReporting,
            'threshold' => $threshold,
            'amount' => $amount
        ];
    }

    private function checkRestrictedCountries(string $fromCountry, string $toCountry): array
    {
        $restricted = array_intersect(
            [$fromCountry, $toCountry], 
            $this->restrictedCountries
        );

        return [
            'status' => empty($restricted),
            'restricted_countries' => $restricted,
            'message' => empty($restricted) ? 'No restrictions' : 'Transaction involves restricted countries'
        ];
    }

    private function getKYCThreshold(string $currency): float
    {
        $thresholds = [
            'USD' => 3000,
            'EUR' => 3000,
            'GBP' => 2000,
            'JPY' => 300000,
            'default' => 3000
        ];

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

Performance Optimization

Caching Strategy

// Redis-based currency caching
class CurrencyCacheManager
{
    private $redis;
    private int $exchangeRateTTL = 3600; // 1 hour
    private int $currencyDataTTL = 86400; // 24 hours

    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }

    public function cacheExchangeRates(string $baseCurrency, array $rates): void
    {
        $key = "exchange_rates:{$baseCurrency}";
        $this->redis->setex($key, $this->exchangeRateTTL, json_encode([
            'rates' => $rates,
            'timestamp' => time(),
            'base_currency' => $baseCurrency
        ]));
    }

    public function getExchangeRates(string $baseCurrency): ?array
    {
        $key = "exchange_rates:{$baseCurrency}";
        $cached = $this->redis->get($key);
        
        return $cached ? json_decode($cached, true) : null;
    }

    public function cacheCurrencyConversion(
        float $amount, 
        string $from, 
        string $to, 
        float $result
    ): void {
        $key = "conversion:{$from}:{$to}:" . md5((string)$amount);
        $data = [
            'amount' => $amount,
            'from' => $from,
            'to' => $to,
            'result' => $result,
            'timestamp' => time()
        ];
        
        $this->redis->setex($key, 1800, json_encode($data)); // 30 minutes
    }
}

Testing Currency Operations

// PHPUnit: Currency service tests
class CurrencyServiceTest extends TestCase
{
    private CurrencyService $currencyService;

    protected function setUp(): void
    {
        $this->currencyService = new CurrencyService();
    }

    public function testCurrencyFormatting(): void
    {
        $this->assertEquals('$1,234.56', $this->currencyService->formatAmount(1234.56, 'USD'));
        $this->assertEquals('€1,234.56', $this->currencyService->formatAmount(1234.56, 'EUR'));
        $this->assertEquals('¥1,235', $this->currencyService->formatAmount(1234.56, 'JPY'));
    }

    public function testCurrencyConversion(): void
    {
        $mockRates = ['EUR' => 0.85, 'GBP' => 0.73];
        
        $result = $this->currencyService->convertAmount(100, 'USD', 'EUR', $mockRates);
        $this->assertEquals(85.0, $result);
        
        $result = $this->currencyService->convertAmount(100, 'USD', 'GBP', $mockRates);
        $this->assertEquals(73.0, $result);
    }

    public function testInvalidCurrency(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->currencyService->formatAmount(100, 'INVALID');
    }

    public function testSameCurrencyConversion(): void
    {
        $result = $this->currencyService->convertAmount(100, 'USD', 'USD');
        $this->assertEquals(100.0, $result);
    }
}

Best Practices

Currency Handling Guidelines

  • Always use ISO 4217 codes - Never use symbols or abbreviations
  • Store amounts in smallest units - Avoid floating point precision issues
  • Cache exchange rates appropriately - Balance accuracy with performance
  • Handle rounding consistently - Use proper financial rounding rules
  • Validate currency codes - Check against official ISO list
  • Consider regulatory requirements - Implement compliance checks
  • Plan for currency changes - Historical rates and discontinued currencies

Common Pitfalls to Avoid

  • Floating point arithmetic - Use decimal/money libraries
  • Hardcoded exchange rates - Always use real-time or recent rates
  • Ignoring minor units - Some currencies have 0 or 3 decimal places
  • Assuming USD as base - Support multiple base currencies
  • Not handling API failures - Always have fallback providers

Implementing robust currency handling is essential for any international application dealing with financial transactions. By following ISO standards, using appropriate libraries, and implementing proper validation and caching, you can build systems that handle global payments reliably and accurately.

Last updated: Sep 12, 2025