package util import ( "encoding/json" "fmt" "io" "net/http" "sync" "time" ) // ExchangeRateResponse represents the response from the exchange rate API type ExchangeRateResponse struct { Success bool `json:"success"` Base string `json:"base"` Date string `json:"date"` Rates map[string]float64 `json:"rates"` } // CachedRate stores exchange rate with timestamp type CachedRate struct { Rate float64 Timestamp time.Time } // Currency conversion cache var ( exchangeRateCache = make(map[string]CachedRate) cacheMutex sync.RWMutex cacheValidDuration = 1 * time.Hour // Cache rates for 1 hour ) // GetExchangeRate fetches the exchange rate from fromCurrency to toCurrency func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) { // If currencies are the same, return 1.0 if fromCurrency == toCurrency { return 1.0, nil } cacheKey := fmt.Sprintf("%s_%s", fromCurrency, toCurrency) // Check cache first cacheMutex.RLock() if cached, exists := exchangeRateCache[cacheKey]; exists { if time.Since(cached.Timestamp) < cacheValidDuration { cacheMutex.RUnlock() return cached.Rate, nil } } cacheMutex.RUnlock() // Fetch from API if not in cache or cache is expired rate, err := fetchExchangeRateFromAPI(fromCurrency, toCurrency) if err != nil { return 0, err } // Cache the result cacheMutex.Lock() exchangeRateCache[cacheKey] = CachedRate{ Rate: rate, Timestamp: time.Now(), } cacheMutex.Unlock() return rate, nil } // fetchExchangeRateFromAPI fetches exchange rate from a free API func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error) { // Using exchangerate-api.com (completely free, no API key required) url := fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency) req, err := http.NewRequest("GET", url, nil) if err != nil { return 0, fmt.Errorf("error creating request: %v", err) } req.Header.Set("User-Agent", "Portfolio-Tracker/1.0") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return 0, fmt.Errorf("error fetching exchange rate: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return 0, fmt.Errorf("API returned status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return 0, fmt.Errorf("error reading response: %v", err) } // Parse exchangerate-api.com response format var apiResp struct { Base string `json:"base"` Date string `json:"date"` Rates map[string]float64 `json:"rates"` } if err := json.Unmarshal(body, &apiResp); err != nil { return 0, fmt.Errorf("error parsing JSON: %v", err) } rate, exists := apiResp.Rates[toCurrency] if !exists { return 0, fmt.Errorf("currency %s not found in response", toCurrency) } return rate, nil } // ConvertCurrency converts an amount from one currency to another func ConvertCurrency(amount float64, fromCurrency, toCurrency string) (float64, error) { if fromCurrency == toCurrency { return amount, nil } rate, err := GetExchangeRate(fromCurrency, toCurrency) if err != nil { return 0, err } return amount * rate, nil } // FormatCurrencyAmount formats a currency amount with the currency symbol func FormatCurrencyAmount(amount float64, currency string) string { return fmt.Sprintf("%.2f %s", amount, currency) } // GetCurrencySymbol returns the symbol for common currencies func GetCurrencySymbol(currency string) string { symbols := map[string]string{ "USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥", "CHF": "CHF", "CAD": "C$", "AUD": "A$", "CNY": "¥", "INR": "₹", "KRW": "₩", "SEK": "kr", "NOK": "kr", "DKK": "kr", "PLN": "zł", "CZK": "Kč", "HUF": "Ft", } if symbol, exists := symbols[currency]; exists { return symbol } return currency // Return currency code if symbol not found } // FormatCurrencyWithSymbol formats amount with currency symbol func FormatCurrencyWithSymbol(amount float64, currency string) string { symbol := GetCurrencySymbol(currency) if symbol == currency { // If no symbol found, put currency after amount return fmt.Sprintf("%.2f %s", amount, currency) } // For most currencies, put symbol before amount switch currency { case "EUR": return fmt.Sprintf("%.2f %s", amount, symbol) default: return fmt.Sprintf("%s%.2f", symbol, amount) } } // ConvertAndFormat converts currency and formats it nicely func ConvertAndFormat(amount float64, fromCurrency, toCurrency string) string { if fromCurrency == toCurrency { return FormatCurrencyWithSymbol(amount, toCurrency) } convertedAmount, err := ConvertCurrency(amount, fromCurrency, toCurrency) if err != nil { // If conversion fails, return original amount with note return fmt.Sprintf("%.2f %s (conv. error)", amount, fromCurrency) } return FormatCurrencyWithSymbol(convertedAmount, toCurrency) } // BatchConvertCurrency converts multiple amounts in one API call for efficiency func BatchConvertCurrency(amounts []float64, fromCurrency, toCurrency string) ([]float64, error) { if fromCurrency == toCurrency { return amounts, nil } rate, err := GetExchangeRate(fromCurrency, toCurrency) if err != nil { return nil, err } converted := make([]float64, len(amounts)) for i, amount := range amounts { converted[i] = amount * rate } return converted, nil } // ClearCache clears the exchange rate cache (useful for testing or manual refresh) func ClearCache() { cacheMutex.Lock() defer cacheMutex.Unlock() exchangeRateCache = make(map[string]CachedRate) } // GetCacheInfo returns information about cached exchange rates func GetCacheInfo() map[string]time.Time { cacheMutex.RLock() defer cacheMutex.RUnlock() info := make(map[string]time.Time) for key, cached := range exchangeRateCache { info[key] = cached.Timestamp } return info }