diff --git a/jobs/Backend/Task/Clients/CnpApiClient.cs b/jobs/Backend/Task/Clients/CnpApiClient.cs
new file mode 100644
index 0000000000..1e18c96cab
--- /dev/null
+++ b/jobs/Backend/Task/Clients/CnpApiClient.cs
@@ -0,0 +1,53 @@
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.Exceptions;
+using Microsoft.Extensions.Options;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+using System.Runtime.ConstrainedExecution;
+
+namespace ExchangeRateUpdater.Clients
+{
+ ///
+ /// Czech National Bank specific implementation of exchange rate API client.
+ /// Fetches data from CNB's daily exchange rate fixing endpoint.
+ ///
+ public class CnbApiClient : IExchangeRateApiClient
+ {
+ private readonly HttpClient _httpClient;
+ private readonly ExchangeRateApiSettings _settings;
+
+ public CnbApiClient(HttpClient httpClient, IOptions settings)
+ {
+ _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ _settings = settings?.Value ?? throw new ArgumentNullException(nameof(settings));
+
+ _httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
+ }
+
+ public async Task GetDailyRatesAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var response = await _httpClient.GetAsync(_settings.DailyRatesUrl, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadAsStringAsync(cancellationToken);
+ }
+ //Consistent error handling regardless of which client implementation is used
+ catch (HttpRequestException ex)
+ {
+ throw new ExchangeRateException(
+ "Failed to retrieve exchange rates from CNB API.",
+ ex);
+ }
+ catch (TaskCanceledException ex)
+ {
+ throw new ExchangeRateException(
+ "Request to CNB API timed out.",
+ ex);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Clients/IExchangeRateApiClient.cs b/jobs/Backend/Task/Clients/IExchangeRateApiClient.cs
new file mode 100644
index 0000000000..0c852cc2bd
--- /dev/null
+++ b/jobs/Backend/Task/Clients/IExchangeRateApiClient.cs
@@ -0,0 +1,17 @@
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace ExchangeRateUpdater.Clients
+{
+ ///
+ /// Generic contract for fetching exchange rate data from any source.
+ /// Implementation can target different central banks or financial data providers.
+ ///
+ public interface IExchangeRateApiClient
+ {
+ ///
+ /// Fetches raw exchange rate data from the source.
+ ///
+ Task GetDailyRatesAsync(CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Configuration/ExchangeRateApiSettings.cs b/jobs/Backend/Task/Configuration/ExchangeRateApiSettings.cs
new file mode 100644
index 0000000000..d06a5548eb
--- /dev/null
+++ b/jobs/Backend/Task/Configuration/ExchangeRateApiSettings.cs
@@ -0,0 +1,12 @@
+namespace ExchangeRateUpdater.Configuration
+{
+ ///
+ /// Configuration settings for exchange rate API access - Generic structure usable for any exchange rate source.
+ /// Compile time binding via IOptions pattern better than raw string keys
+ ///
+ public class ExchangeRateApiSettings
+ {
+ public string DailyRatesUrl { get; set; } = string.Empty;
+ public int TimeoutSeconds { get; set; } = 30;
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs
deleted file mode 100644
index f375776f25..0000000000
--- a/jobs/Backend/Task/Currency.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace ExchangeRateUpdater
-{
- public class Currency
- {
- public Currency(string code)
- {
- Code = code;
- }
-
- ///
- /// Three-letter ISO 4217 code of the currency.
- ///
- public string Code { get; }
-
- public override string ToString()
- {
- return Code;
- }
- }
-}
diff --git a/jobs/Backend/Task/Exceptions/ExchangeRateException.cs b/jobs/Backend/Task/Exceptions/ExchangeRateException.cs
new file mode 100644
index 0000000000..d7165ef323
--- /dev/null
+++ b/jobs/Backend/Task/Exceptions/ExchangeRateException.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ExchangeRateUpdater.Exceptions
+{
+ ///
+ /// Custom exception for exchange rate operations.
+ /// caller can distinguish domain errors from infrastructure errors
+ ///
+ public class ExchangeRateException : Exception
+ {
+ public ExchangeRateException(string message) : base(message) { }
+ public ExchangeRateException(string message, Exception innerException)
+ : base(message, innerException) { }
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs
deleted file mode 100644
index 58c5bb10e0..0000000000
--- a/jobs/Backend/Task/ExchangeRate.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace ExchangeRateUpdater
-{
- public class ExchangeRate
- {
- public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value)
- {
- SourceCurrency = sourceCurrency;
- TargetCurrency = targetCurrency;
- Value = value;
- }
-
- public Currency SourceCurrency { get; }
-
- public Currency TargetCurrency { get; }
-
- public decimal Value { get; }
-
- public override string ToString()
- {
- return $"{SourceCurrency}/{TargetCurrency}={Value}";
- }
- }
-}
diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs
deleted file mode 100644
index 6f82a97fbe..0000000000
--- a/jobs/Backend/Task/ExchangeRateProvider.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace ExchangeRateUpdater
-{
- public class ExchangeRateProvider
- {
- ///
- /// 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.
- ///
- public IEnumerable GetExchangeRates(IEnumerable currencies)
- {
- return Enumerable.Empty();
- }
- }
-}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj
index 2fc654a12b..832083d5c5 100644
--- a/jobs/Backend/Task/ExchangeRateUpdater.csproj
+++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj
@@ -5,4 +5,26 @@
net6.0
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln
index 89be84daff..f5967b0622 100644
--- a/jobs/Backend/Task/ExchangeRateUpdater.sln
+++ b/jobs/Backend/Task/ExchangeRateUpdater.sln
@@ -1,9 +1,11 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35122.118
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.XUnitTests", "..\ExchangeRateUpdater.XUnitTests\ExchangeRateUpdater.XUnitTests.csproj", "{ABAEC4CE-7FFB-4DAE-AFED-CBCD6D699BF4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,8 +17,15 @@ Global
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ABAEC4CE-7FFB-4DAE-AFED-CBCD6D699BF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ABAEC4CE-7FFB-4DAE-AFED-CBCD6D699BF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ABAEC4CE-7FFB-4DAE-AFED-CBCD6D699BF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ABAEC4CE-7FFB-4DAE-AFED-CBCD6D699BF4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {7A601B6F-E9B2-4165-8DB1-8AB0DD43684F}
+ EndGlobalSection
EndGlobal
diff --git a/jobs/Backend/Task/Models/Currency.cs b/jobs/Backend/Task/Models/Currency.cs
new file mode 100644
index 0000000000..5d0a73c578
--- /dev/null
+++ b/jobs/Backend/Task/Models/Currency.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace ExchangeRateUpdater.Models
+{
+ ///
+ /// Immutable value object representing a currency.
+ ///
+ public class Currency
+ {
+ //Validate in constructor. Don't allow invalid states.
+ public Currency(string code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ throw new ArgumentException("Currency code cannot be null or empty.", nameof(code));
+
+ //Normalize to uppercase (usd : USD) for consistency
+ Code = code.ToUpperInvariant();
+ }
+
+ ///
+ /// Three-letter ISO 4217 code of the currency.
+ ///
+ public string Code { get; }
+
+ public override string ToString()
+ {
+ return Code;
+ }
+
+ // Equality based on Code to use it in HashSet/Dictionary
+ public override bool Equals(object? obj) =>
+ obj is Currency other && Code == other.Code;
+
+ // Required when overriding Equals: Ensures Dictionary/HashSet work correctly
+ public override int GetHashCode() => Code.GetHashCode();
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Models/ExchangeRate.cs b/jobs/Backend/Task/Models/ExchangeRate.cs
new file mode 100644
index 0000000000..5bef2260d3
--- /dev/null
+++ b/jobs/Backend/Task/Models/ExchangeRate.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Runtime.ConstrainedExecution;
+
+namespace ExchangeRateUpdater.Models
+{
+ ///
+ /// Immutable value object representing an exchange rate between two currencies.
+ ///
+ public class ExchangeRate
+ {
+ //validations in constructor
+ public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value)
+ {
+ SourceCurrency = sourceCurrency ?? throw new ArgumentNullException(nameof(sourceCurrency));
+ TargetCurrency = targetCurrency ?? throw new ArgumentNullException(nameof(targetCurrency));
+
+ if (value <= 0)
+ throw new ArgumentException("Exchange rate value must be positive.", nameof(value));
+
+ Value = value;
+ }
+
+ public Currency SourceCurrency { get; }
+ public Currency TargetCurrency { get; }
+ public decimal Value { get; }
+
+ //consistent output(4 decimal places)
+ public override string ToString() =>
+ $"{SourceCurrency}/{TargetCurrency}={Value:F4}";
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Parsers/CnbDataParser.cs b/jobs/Backend/Task/Parsers/CnbDataParser.cs
new file mode 100644
index 0000000000..a2d4b8735e
--- /dev/null
+++ b/jobs/Backend/Task/Parsers/CnbDataParser.cs
@@ -0,0 +1,86 @@
+using ExchangeRateUpdater.Exceptions;
+using ExchangeRateUpdater.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System;
+
+namespace ExchangeRateUpdater.Parsers
+{
+ ///
+ /// Parses CNB daily exchange rates text format.
+ /// Format: Country|Currency|Amount|Code|Rate
+ /// Example: USA|dollar|1|USD|24.123
+ ///
+ public class CnbDataParser : IExchangeRateDataParser
+ {
+ private const int ExpectedColumnCount = 5;
+ private const int AmountIndex = 2;
+ private const int CodeIndex = 3;
+ private const int RateIndex = 4;
+ private static readonly Currency CzkCurrency = new("CZK");
+
+ public IEnumerable Parse(string rawData, IEnumerable targetCurrencies)
+ {
+ if (string.IsNullOrWhiteSpace(rawData))
+ throw new ExchangeRateException("CNB data is empty.");
+
+ // enhances performance and ensures that the filtering process is accurate and efficient.
+ // HashSet for O(1) lookup performance instead of O(n) with List.Contains--> Checking if currency is requested happens for every parsed line
+ var currencyCodesSet = new HashSet(
+ targetCurrencies.Select(c => c.Code),
+ StringComparer.OrdinalIgnoreCase);
+
+ //Parse all lines and then filter
+ return ParseLines(rawData)
+ .Where(rate => currencyCodesSet.Contains(rate.SourceCurrency.Code))
+ .ToList();
+ }
+
+ private IEnumerable ParseLines(string rawData)
+ {
+ var lines = rawData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // first 2 lines: date and column headers
+ return lines.Skip(2)
+ .Select(ParseLine)
+ .Where(rate => rate != null)
+ .Cast();
+ }
+
+ private ExchangeRate? ParseLine(string line)
+ {
+ try
+ {
+ var columns = line.Split('|');
+
+ if (columns.Length != ExpectedColumnCount)
+ return null;
+
+ var currencyCode = columns[CodeIndex].Trim();
+ var amount = ParseDecimal(columns[AmountIndex]);
+ var rate = ParseDecimal(columns[RateIndex]);
+
+ // CNB rates are: {amount} {currency} = {rate} CZK
+ // We want: 1 {currency} = {rate/amount} CZK. We normalize it to 1 unit.
+ var normalizedRate = rate / amount;
+
+ return new ExchangeRate(
+ sourceCurrency: new Currency(currencyCode),
+ targetCurrency: CzkCurrency,
+ value: normalizedRate);
+ }
+ catch
+ {
+ // Skip malformed lines
+ return null;
+ }
+ }
+
+ private decimal ParseDecimal(string value)
+ {
+ // CNB uses comma as decimal separator
+ var normalized = value.Trim().Replace(',', '.');
+ return decimal.Parse(normalized, System.Globalization.CultureInfo.InvariantCulture);
+ }
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Parsers/IExchangeRateDataParser.cs b/jobs/Backend/Task/Parsers/IExchangeRateDataParser.cs
new file mode 100644
index 0000000000..b3f38bc3ab
--- /dev/null
+++ b/jobs/Backend/Task/Parsers/IExchangeRateDataParser.cs
@@ -0,0 +1,20 @@
+using ExchangeRateUpdater.Models;
+using System.Collections.Generic;
+
+namespace ExchangeRateUpdater.Parsers
+{
+ ///
+ /// Generic contract for parsing exchange rate data from any format.
+ /// Implementations can handle different data formats (TXT, XML, JSON, CSV).
+ ///
+ public interface IExchangeRateDataParser
+ {
+ ///
+ /// Parses raw data into structured exchange rates.
+ ///
+ /// Raw text data from the API
+ /// Currencies to filter for
+ /// Collection of exchange rates
+ IEnumerable Parse(string rawData, IEnumerable targetCurrencies);
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs
index 379a69b1f8..cc6d354155 100644
--- a/jobs/Backend/Task/Program.cs
+++ b/jobs/Backend/Task/Program.cs
@@ -1,12 +1,27 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Clients;
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Parsers;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
namespace ExchangeRateUpdater
{
+ ///
+ /// TOP DECISIONS:
+ /// - Settings for Environment-specific configuration
+ /// - Dependency Injection for testability and loose coupling
+ /// - IHttpClientFactory pattern to prevent socket exhaustion
+ ///
public static class Program
{
- private static IEnumerable currencies = new[]
+ private static readonly IEnumerable currencies = new[]
{
new Currency("USD"),
new Currency("EUR"),
@@ -19,12 +34,15 @@ public static class Program
new Currency("XYZ")
};
- public static void Main(string[] args)
+ public static async Task Main(string[] args)
{
+ var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
+ var services = ConfigureServices(environment);
+
try
{
- var provider = new ExchangeRateProvider();
- var rates = provider.GetExchangeRates(currencies);
+ var provider = services.GetRequiredService();
+ var rates = await provider.GetExchangeRatesAsync(currencies);
Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
foreach (var rate in rates)
@@ -37,7 +55,31 @@ public static void Main(string[] args)
Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'.");
}
+ Console.WriteLine("\nPress any key to exit...");
Console.ReadLine();
}
+
+ private static ServiceProvider ConfigureServices(string environment)
+ {
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
+ .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
+ .Build();
+
+ var services = new ServiceCollection();
+
+ // Options Pattern: Strongly-typed configuration with validation support, Type-safe, compile-time checking
+ services.Configure(configuration.GetSection("ExchangeRateApi"));
+
+ // HttpClient and Parser with generic interfaces for easy abstraction and testing, and swappable with other implementations of api or parser
+ services.AddHttpClient();
+ services.AddTransient();
+
+ // Provider has no interface: It's already generic, just orchestrates dependencies. Contains zero business logic
+ services.AddTransient();
+
+ return services.BuildServiceProvider();
+ }
}
-}
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Properties/launchSettings.json b/jobs/Backend/Task/Properties/launchSettings.json
new file mode 100644
index 0000000000..af678b4ea3
--- /dev/null
+++ b/jobs/Backend/Task/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "ExchangeRateUpdater": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Services/ExchangeRateProvider.cs b/jobs/Backend/Task/Services/ExchangeRateProvider.cs
new file mode 100644
index 0000000000..897effe921
--- /dev/null
+++ b/jobs/Backend/Task/Services/ExchangeRateProvider.cs
@@ -0,0 +1,46 @@
+using ExchangeRateUpdater.Clients;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Parsers;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+
+namespace ExchangeRateUpdater.Services
+{
+ ///
+ /// Generic exchange rate provider that orchestrates data fetching and parsing.
+ /// It is reusable across different exchange rate sources by injecting different implementations
+ /// of IExchangeRateApiClient and IExchangeRateDataParser.
+ ///
+ public class ExchangeRateProvider
+ {
+ private readonly IExchangeRateApiClient _apiClient;
+ private readonly IExchangeRateDataParser _dataParser;
+
+ public ExchangeRateProvider(IExchangeRateApiClient apiClient, IExchangeRateDataParser dataParser)
+ {
+ _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
+ _dataParser = dataParser ?? throw new ArgumentNullException(nameof(dataParser));
+ }
+
+ ///
+ /// Gets exchange rates for specified currencies.
+ /// Returns only rates explicitly provided by the source (no calculated/reverse rates).
+ /// Cancellation token allows cooperative cancellation of async operations
+ ///
+ public async Task> GetExchangeRatesAsync(
+ IEnumerable currencies,
+ CancellationToken cancellationToken = default)
+ {
+ var currenciesList = currencies?.ToList() ?? throw new ArgumentNullException(nameof(currencies));
+
+ if (!currenciesList.Any())
+ return Enumerable.Empty();
+
+ var rawData = await _apiClient.GetDailyRatesAsync(cancellationToken);
+ return _dataParser.Parse(rawData, currenciesList);
+ }
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/appsettings.Development.json b/jobs/Backend/Task/appsettings.Development.json
new file mode 100644
index 0000000000..ecb99b52f8
--- /dev/null
+++ b/jobs/Backend/Task/appsettings.Development.json
@@ -0,0 +1,6 @@
+{
+ "ExchangeRateApi": {
+ "DailyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
+ "TimeoutSeconds": 30
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/appsettings.Production.json b/jobs/Backend/Task/appsettings.Production.json
new file mode 100644
index 0000000000..ecb99b52f8
--- /dev/null
+++ b/jobs/Backend/Task/appsettings.Production.json
@@ -0,0 +1,6 @@
+{
+ "ExchangeRateApi": {
+ "DailyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
+ "TimeoutSeconds": 30
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/appsettings.json b/jobs/Backend/Task/appsettings.json
new file mode 100644
index 0000000000..ecb99b52f8
--- /dev/null
+++ b/jobs/Backend/Task/appsettings.json
@@ -0,0 +1,6 @@
+{
+ "ExchangeRateApi": {
+ "DailyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
+ "TimeoutSeconds": 30
+ }
+}
\ No newline at end of file