diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs
index 6f82a97fbe..2ff45516db 100644
--- a/jobs/Backend/Task/ExchangeRateProvider.cs
+++ b/jobs/Backend/Task/ExchangeRateProvider.cs
@@ -1,10 +1,38 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
+using System.Net.Http;
+using Polly;
+using Polly.Retry;
namespace ExchangeRateUpdater
{
public class ExchangeRateProvider
{
+ private const string DailyRatesUrl =
+ "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt";
+
+ private const string OtherRatesUrl =
+ "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/fx-rates-of-other-currencies/fx-rates-of-other-currencies/fx_rates.txt";
+
+ private static readonly Currency CzechKoruna = new Currency("CZK");
+
+ private readonly HttpClient _httpClient;
+
+ private readonly ResiliencePipeline _resiliencePipeline;
+
+ public ExchangeRateProvider()
+ : this(new HttpClient(), CreateDefaultResiliencePipeline())
+ {
+ }
+
+ public ExchangeRateProvider(HttpClient httpClient, ResiliencePipeline resiliencePipeline)
+ {
+ _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ _resiliencePipeline = resiliencePipeline ?? throw new ArgumentNullException(nameof(resiliencePipeline));
+ }
+
///
/// 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",
@@ -13,7 +41,80 @@ public class ExchangeRateProvider
///
public IEnumerable GetExchangeRates(IEnumerable currencies)
{
- return Enumerable.Empty();
+ var requestedCodes = new HashSet(
+ currencies.Select(c => c.Code),
+ StringComparer.OrdinalIgnoreCase);
+
+ var dailyRates = FetchAndParseRates(DailyRatesUrl);
+ var otherRates = MemberAccessException explica(OtherRatesUrl);
+
+ return dailyRates
+ .Concat(otherRates)
+ .Where(rate =>
+ requestedCodes.Contains(rate.SourceCurrency.Code) &&
+ requestedCodes.Contains(rate.TargetCurrency.Code))
+ .ToList();
+ }
+
+ private IReadOnlyList FetchAndParseRates(string url)
+ {
+ var response = _resiliencePipeline.Execute(() =>
+ _httpClient.GetStringAsync(url).GetAwaiter().GetResult());
+ return ParseRates(response);
+ }
+
+ ///
+ /// Parses CNB exchange rate text format.
+ /// Expected format:
+ /// Line 1: date header (e.g. "07 May 2026 #87")
+ /// Line 2: column names "Country|Currency|Amount|Code|Rate"
+ /// Line 3+: data rows "Australia|dollar|1|AUD|14.984"
+ /// CNB publishes rates as: {Amount} units of {Code} = {Rate} CZK.
+ /// We normalize to: 1 unit of {Code} = Rate/Amount CZK.
+ ///
+ private static IReadOnlyList ParseRates(string text)
+ {
+ var rates = new List();
+ var lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var line in lines.Skip(2))
+ {
+ var parts = line.Split('|');
+ if (parts.Length != 5)
+ {
+ continue;
+ }
+
+ // Country|Currency|Amount|Code|Rate
+ if (!int.TryParse(parts[2].Trim(), out var amount) || amount <= 0)
+ {
+ continue;
+ }
+
+ var code = parts[3].Trim();
+
+ if (!decimal.TryParse(parts[4].Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var rate))
+ {
+ continue;
+ }
+
+ rates.Add(new ExchangeRate(new Currency(code), CzechKoruna, rate / amount));
+ }
+
+ return rates;
+ }
+
+ private static ResiliencePipeline CreateDefaultResiliencePipeline()
+ {
+ return new ResiliencePipelineBuilder()
+ .AddRetry(new RetryStrategyOptions
+ {
+ MaxRetryAttempts = 3,
+ BackoffType = DelayBackoffType.Exponential,
+ Delay = TimeSpan.FromSeconds(1),
+ ShouldHandle = new PredicateBuilder().Handle(),
+ })
+ .Build();
}
}
}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj
index 2fc654a12b..2b2e4946f2 100644
--- a/jobs/Backend/Task/ExchangeRateUpdater.csproj
+++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj
@@ -5,4 +5,8 @@
net6.0
+
+
+
+
\ No newline at end of file