From 000f9f982481d3fc460755f0fd484bcc614f9d1f Mon Sep 17 00:00:00 2001 From: Divyam Bhagchandani Date: Mon, 10 Nov 2025 00:52:55 +0100 Subject: [PATCH] Implement CNB exchange rate provider --- jobs/Backend/Task/ExchangeRateProvider.cs | 148 +++++++++++++++++++++- 1 file changed, 141 insertions(+), 7 deletions(-) diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs index 6f82a97fbe..1eb16adee1 100644 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ b/jobs/Backend/Task/ExchangeRateProvider.cs @@ -1,19 +1,153 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Xml.Linq; +using System.Xml; +using System.Globalization; + + + + namespace ExchangeRateUpdater { public class ExchangeRateProvider { + // Czech National Bank XML endpoint for daily exchange rates + private static readonly string BankApiUrl = + "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml"; + + + private const int TimeoutSeconds = 10; + private const string TargetCurrency = "CZK"; + + private static readonly HttpClient _httpClient; + + static ExchangeRateProvider() + { + _httpClient = new HttpClient(); + _httpClient.Timeout = TimeSpan.FromSeconds(TimeoutSeconds); + } + + + /// - /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined - /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", - /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide - /// some of the currencies, ignore them. + /// This method returns exchange rates among the specified currencies that are defined by the source. + /// Provides exchange rates from Czech National Bank's public API. /// public IEnumerable GetExchangeRates(IEnumerable currencies) { - return Enumerable.Empty(); + if (currencies == null) + { + throw new ArgumentNullException(nameof(currencies), "Currency collection cannot be null"); + } + var currencyList = currencies.ToList(); + if (!currencyList.Any()) + { + return Enumerable.Empty(); + } + + var exchangeRatesDict = FetchandParseBankData(); + return GetRequestedCurrenciesExchangeRate(exchangeRatesDict, currencies); + } + + + private List GetRequestedCurrenciesExchangeRate(Dictionary allExchangeRates, IEnumerable requestedCurrencies) + { + var requestedCurrenciesExchangeRate = new List(); + // Used to keep track of the processed currencies and prevent duplicates + var seenCurrencies = new HashSet(); + + foreach (var currency in requestedCurrencies) + { + if (currency == null) + { + continue; + } + + string sourceCurrency = currency.Code; + + if (!seenCurrencies.Add(sourceCurrency)) + { + continue; + } + + if (allExchangeRates.ContainsKey(sourceCurrency)) + { + requestedCurrenciesExchangeRate.Add(new ExchangeRate( + new Currency(sourceCurrency), + new Currency(TargetCurrency), + allExchangeRates[sourceCurrency] + )); + } + } + return requestedCurrenciesExchangeRate; + + } + + private Dictionary FetchandParseBankData() + { + try{ + + LogInfo("Fetching exchange rates provided currencies"); + + var response = _httpClient.GetAsync(BankApiUrl).Result; + response.EnsureSuccessStatusCode(); + var xmlBankData = response.Content.ReadAsStringAsync().Result; + + var parsedXmlData = XDocument.Parse(xmlBankData); + + Dictionary exchangeRates = parsedXmlData.Descendants("radek") + .Where(x => x.Attribute("kod") != null && + x.Attribute("kurz") != null && + x.Attribute("mnozstvi") != null) + .ToDictionary( + x => x.Attribute("kod").Value, + x => + { + // // Use Czech culture for decimal parsing + decimal kurz = decimal.Parse(x.Attribute("kurz").Value, CultureInfo.GetCultureInfo("cs-CZ")); + decimal mnozstvi = decimal.Parse(x.Attribute("mnozstvi").Value); + if (mnozstvi == 0) + { + throw new InvalidOperationException($"Invalid mnozstvi (0) for currency {x.Attribute("kod").Value}"); + } + return kurz / mnozstvi; + } + ); + return exchangeRates; + } + catch (XmlException ex) + { + LogError("Failed to parse XML from the Bank", ex); + throw; + } + catch (FormatException ex) + { + LogError("Invalid number format in Bank data", ex); + throw; + } + catch (Exception ex) + { + LogError("Failed to fetch and parse CNB data", ex); + throw; + } + } + + private void LogError(string message, Exception ex = null) + { + Console.Error.WriteLine($"[ERROR] {message}"); + if (ex != null) + { + Console.Error.WriteLine($"Exception: {ex.Message}"); } } -} + + private void LogInfo(string message) + { + Console.WriteLine($"[INFO] {message}"); + } + + } +} \ No newline at end of file