from typing import Dict, Optional import requests from datetime import datetime, date, timedelta import json class CurrencyService: """Service for handling currency conversions and exchange rates""" def __init__(self): self.cache = {} self.cache_expiry = {} self.cache_duration_hours = 24 # Cache rates for 24 hours # Fallback exchange rates (for offline mode or API failures) self.fallback_rates = { 'USD': 1.0, 'EUR': 0.85, 'GBP': 0.73, 'JPY': 110.0, 'CNY': 6.45, 'INR': 74.0, 'AED': 3.67, 'CAD': 1.25, 'AUD': 1.35, 'CHF': 0.92, 'SEK': 8.6, 'NOK': 8.5, 'DKK': 6.2, 'SGD': 1.35, 'HKD': 7.8, 'NZD': 1.45, 'ZAR': 15.0, 'MXN': 20.0, 'BRL': 5.2 } def _is_cache_valid(self, currency_pair: str) -> bool: """Check if cached rate is still valid""" if currency_pair not in self.cache_expiry: return False expiry_time = self.cache_expiry[currency_pair] return datetime.now() < expiry_time def _cache_rate(self, currency_pair: str, rate: float): """Cache an exchange rate with expiry time""" self.cache[currency_pair] = rate expiry_time = datetime.now().replace(microsecond=0) + \ timedelta(hours=self.cache_duration_hours) self.cache_expiry[currency_pair] = expiry_time def get_exchange_rate(self, from_currency: str, to_currency: str, date: Optional[date] = None) -> float: """ Get exchange rate from one currency to another Args: from_currency: Source currency code (e.g., 'USD') to_currency: Target currency code (e.g., 'EUR') date: Historical date for the rate (optional, defaults to current rate) Returns: Exchange rate (how many units of to_currency equal 1 unit of from_currency) """ if from_currency == to_currency: return 1.0 # Check cache first cache_key = f"{from_currency}_{to_currency}" reverse_cache_key = f"{to_currency}_{from_currency}" if self._is_cache_valid(cache_key): return self.cache[cache_key] if self._is_cache_valid(reverse_cache_key): # We have the reverse rate, calculate the forward rate reverse_rate = self.cache[reverse_cache_key] if reverse_rate > 0: forward_rate = 1.0 / reverse_rate self._cache_rate(cache_key, forward_rate) return forward_rate # Try to fetch from API try: rate = self._fetch_rate_from_api(from_currency, to_currency) if rate: self._cache_rate(cache_key, rate) return rate except Exception as e: print(f"Failed to fetch exchange rate from API: {e}") # Fallback to static rates # Fallback rates are stored as "how many units = 1 USD" # So to convert from_currency to to_currency: # 1 from_currency = (1/from_rate) USD # 1 USD = to_rate to_currency # Therefore: 1 from_currency = (to_rate / from_rate) to_currency from_rate = self.fallback_rates.get(from_currency, 1.0) to_rate = self.fallback_rates.get(to_currency, 1.0) if from_rate > 0: rate = to_rate / from_rate self._cache_rate(cache_key, rate) return rate return 1.0 def _fetch_rate_from_api(self, from_currency: str, to_currency: str) -> Optional[float]: """ Fetch exchange rate from external API Using a free API for demonstration - in production, consider using a paid service """ try: # Using exchangerate-api.com free tier url = f"https://v6.exchangerate-api.com/v6/latest/{from_currency}" response = requests.get(url, timeout=10) response.raise_for_status() data = response.json() if 'conversion_rates' in data and to_currency in data['conversion_rates']: return data['conversion_rates'][to_currency] except requests.exceptions.RequestException as e: print(f"API request failed: {e}") except (KeyError, ValueError) as e: print(f"Invalid API response: {e}") return None def convert_amount(self, amount: float, from_currency: str, to_currency: str) -> float: """ Convert amount from one currency to another Args: amount: Amount in source currency from_currency: Source currency code to_currency: Target currency code Returns: Converted amount in target currency """ if amount <= 0: return 0.0 rate = self.get_exchange_rate(from_currency, to_currency) return amount * rate def get_supported_currencies(self) -> Dict[str, str]: """Get dictionary of supported currencies with their names""" return { 'USD': 'US Dollar', 'EUR': 'Euro', 'GBP': 'British Pound', 'JPY': 'Japanese Yen', 'CNY': 'Chinese Yuan', 'INR': 'Indian Rupee', 'AED': 'UAE Dirham', 'CAD': 'Canadian Dollar', 'AUD': 'Australian Dollar', 'CHF': 'Swiss Franc', 'SEK': 'Swedish Krona', 'NOK': 'Norwegian Krone', 'DKK': 'Danish Krone', 'SGD': 'Singapore Dollar', 'HKD': 'Hong Kong Dollar', 'NZD': 'New Zealand Dollar', 'ZAR': 'South African Rand', 'MXN': 'Mexican Peso', 'BRL': 'Brazilian Real' } def format_currency(self, amount: float, currency_code: str) -> str: """Format amount with appropriate currency symbol and formatting""" symbols = { 'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'INR': '₹', 'AED': 'د.إ', 'CAD': 'C$', 'AUD': 'A$', 'CHF': 'CHF', 'SEK': 'SEK', 'NOK': 'NOK', 'DKK': 'DKK', 'SGD': 'S$', 'HKD': 'HK$', 'NZD': 'NZ$', 'ZAR': 'R', 'MXN': 'MX$', 'BRL': 'R$' } symbol = symbols.get(currency_code, currency_code) # Format based on currency conventions if currency_code in ['JPY', 'CNY']: # No decimal places for these currencies return f"{symbol}{int(round(amount))}" else: # Two decimal places for most currencies return f"{symbol}{amount:.2f}" # Singleton instance currency_service = CurrencyService()