diff --git a/.gitignore b/.gitignore
index fd3586545..d05410c5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@
node_modules
bower_components
npm-debug.log
+*.vsidx
+/jobs/Backend/Task/.vs/ExchangeRates/v17/TestStore/0
diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs
deleted file mode 100644
index 6f82a97fb..000000000
--- 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
deleted file mode 100644
index 2fc654a12..000000000
--- a/jobs/Backend/Task/ExchangeRateUpdater.csproj
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- Exe
- net6.0
-
-
-
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln
deleted file mode 100644
index 89be84daf..000000000
--- a/jobs/Backend/Task/ExchangeRateUpdater.sln
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {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
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/jobs/Backend/Task/ExchangeRates.sln b/jobs/Backend/Task/ExchangeRates.sln
new file mode 100644
index 000000000..395ea2ead
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRates.sln
@@ -0,0 +1,28 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangesRatesApp", "src\ExchangesRatesApp.csproj", "{0C2EC045-59FC-4AE7-B39E-C3FD037096A6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRatesTests", "test\ExchangeRatesTests.csproj", "{CE380A25-4484-41B4-BEC0-FC3292A46DF7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {0C2EC045-59FC-4AE7-B39E-C3FD037096A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C2EC045-59FC-4AE7-B39E-C3FD037096A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C2EC045-59FC-4AE7-B39E-C3FD037096A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C2EC045-59FC-4AE7-B39E-C3FD037096A6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE380A25-4484-41B4-BEC0-FC3292A46DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE380A25-4484-41B4-BEC0-FC3292A46DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE380A25-4484-41B4-BEC0-FC3292A46DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE380A25-4484-41B4-BEC0-FC3292A46DF7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs
deleted file mode 100644
index 379a69b1f..000000000
--- a/jobs/Backend/Task/Program.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace ExchangeRateUpdater
-{
- public static class Program
- {
- private static IEnumerable currencies = new[]
- {
- new Currency("USD"),
- new Currency("EUR"),
- new Currency("CZK"),
- new Currency("JPY"),
- new Currency("KES"),
- new Currency("RUB"),
- new Currency("THB"),
- new Currency("TRY"),
- new Currency("XYZ")
- };
-
- public static void Main(string[] args)
- {
- try
- {
- var provider = new ExchangeRateProvider();
- var rates = provider.GetExchangeRates(currencies);
-
- Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
- foreach (var rate in rates)
- {
- Console.WriteLine(rate.ToString());
- }
- }
- catch (Exception e)
- {
- Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'.");
- }
-
- Console.ReadLine();
- }
- }
-}
diff --git a/jobs/Backend/Task/src/ExchangesRatesApp.csproj b/jobs/Backend/Task/src/ExchangesRatesApp.csproj
new file mode 100644
index 000000000..1ee008290
--- /dev/null
+++ b/jobs/Backend/Task/src/ExchangesRatesApp.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/jobs/Backend/Task/src/Models/CnbRateDailyResponse.cs b/jobs/Backend/Task/src/Models/CnbRateDailyResponse.cs
new file mode 100644
index 000000000..b48ca4671
--- /dev/null
+++ b/jobs/Backend/Task/src/Models/CnbRateDailyResponse.cs
@@ -0,0 +1,18 @@
+namespace ExchangeRateUpdater.Models
+{
+ public class CnbRateDailyResponse
+ {
+ public IEnumerable Rates { get; set; }
+ }
+
+ public class CnbRate
+ {
+ public string ValidFor { get; set; }
+ public int Order { get; set; }
+ public string Country { get; set; }
+ public string Currency { get; set; }
+ public int Amount { get; set; }
+ public string CurrencyCode { get; set; }
+ public decimal Rate { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/src/Models/Currency.cs
similarity index 89%
rename from jobs/Backend/Task/Currency.cs
rename to jobs/Backend/Task/src/Models/Currency.cs
index f375776f2..8336d740e 100644
--- a/jobs/Backend/Task/Currency.cs
+++ b/jobs/Backend/Task/src/Models/Currency.cs
@@ -1,4 +1,4 @@
-namespace ExchangeRateUpdater
+namespace ExchangeRateUpdater.Models
{
public class Currency
{
diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/src/Models/ExchangeRate.cs
similarity index 93%
rename from jobs/Backend/Task/ExchangeRate.cs
rename to jobs/Backend/Task/src/Models/ExchangeRate.cs
index 58c5bb10e..2133586d4 100644
--- a/jobs/Backend/Task/ExchangeRate.cs
+++ b/jobs/Backend/Task/src/Models/ExchangeRate.cs
@@ -1,4 +1,4 @@
-namespace ExchangeRateUpdater
+namespace ExchangeRateUpdater.Models
{
public class ExchangeRate
{
diff --git a/jobs/Backend/Task/src/Options/CnbApiOptions.cs b/jobs/Backend/Task/src/Options/CnbApiOptions.cs
new file mode 100644
index 000000000..18fed5a4b
--- /dev/null
+++ b/jobs/Backend/Task/src/Options/CnbApiOptions.cs
@@ -0,0 +1,10 @@
+namespace ExchangeRateUpdater.Options
+{
+ public class CnbApiOptions
+ {
+ public const string OptionsName = "CnbApi";
+ public const string ClientName = "CnbClient";
+
+ public string BaseAddress { get; set; }
+ }
+}
diff --git a/jobs/Backend/Task/src/Options/CurrenciesOptions.cs b/jobs/Backend/Task/src/Options/CurrenciesOptions.cs
new file mode 100644
index 000000000..ebdc40c8b
--- /dev/null
+++ b/jobs/Backend/Task/src/Options/CurrenciesOptions.cs
@@ -0,0 +1,9 @@
+namespace ExchangeRateUpdater.Options
+{
+ public class CurrenciesOptions
+ {
+ public const string OptionsName = "CurrenciesOptions";
+
+ public IEnumerable Currencies { get; set; }
+ }
+}
diff --git a/jobs/Backend/Task/src/Program.cs b/jobs/Backend/Task/src/Program.cs
new file mode 100644
index 000000000..f5059187b
--- /dev/null
+++ b/jobs/Backend/Task/src/Program.cs
@@ -0,0 +1,64 @@
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Options;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Http;
+
+namespace ExchangeRateUpdater
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = CreateHostBuilder(args).Build();
+
+ var rates = host.Services.GetService().GetExchangeRates(System.Threading.CancellationToken.None).GetAwaiter().GetResult();
+
+ foreach (var rate in rates)
+ {
+ Console.WriteLine(rate.ToString());
+ }
+ }
+
+ private static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ var _configuration = new ConfigurationBuilder()
+ .AddJsonFile("local.settings.json",
+ optional: true,
+ reloadOnChange: true)
+ .Build();
+
+ var hostBuilder = Host.CreateDefaultBuilder(args)
+ .ConfigureAppConfiguration((context, builder) =>
+ {
+ builder.SetBasePath(Directory.GetCurrentDirectory());
+ })
+ .ConfigureServices((context, services) =>
+ {
+ // Remove httpClient default Console.WriteLine behaviour
+ services.RemoveAll();
+
+ // Options
+ services.Configure(_configuration.GetSection(CurrenciesOptions.OptionsName));
+
+ var cnbApiOptions = new CnbApiOptions();
+ _configuration.GetSection(CnbApiOptions.OptionsName).Bind(cnbApiOptions);
+
+ // Services
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // CnbHttpClient
+ services.AddHttpClient(CnbApiOptions.ClientName, x =>
+ {
+ x.BaseAddress = new Uri(cnbApiOptions.BaseAddress);
+ });
+ });
+
+ return hostBuilder;
+ }
+ }
+}
diff --git a/jobs/Backend/Task/src/Services/CnbApiService.cs b/jobs/Backend/Task/src/Services/CnbApiService.cs
new file mode 100644
index 000000000..e05c9e16f
--- /dev/null
+++ b/jobs/Backend/Task/src/Services/CnbApiService.cs
@@ -0,0 +1,46 @@
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Options;
+using Newtonsoft.Json;
+using Microsoft.Extensions.Options;
+
+namespace ExchangeRateUpdater.Services
+{
+ public class CnbApiService : ICnbApiService
+ {
+ private readonly string GET_RATES_ENDPOINT = "/cnbapi/exrates/daily";
+
+ private readonly HttpClient _client;
+
+ public CnbApiService(IHttpClientFactory clientFactory)
+ {
+ _client = clientFactory.CreateClient(CnbApiOptions.ClientName);
+ }
+
+ public async Task GetExchangeRate(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var httpRequest = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri(GET_RATES_ENDPOINT, UriKind.Relative)
+ };
+
+ var httpResponse = await _client.SendAsync(httpRequest, cancellationToken);
+
+ if (!httpResponse.IsSuccessStatusCode)
+ {
+ return new CnbRateDailyResponse
+ {
+ Rates = new List()
+ };
+ }
+
+ var response = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
+
+ var cnbRates = JsonConvert.DeserializeObject(response);
+
+ return cnbRates;
+ }
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/src/Services/ExchangeRateProvider.cs b/jobs/Backend/Task/src/Services/ExchangeRateProvider.cs
new file mode 100644
index 000000000..0a0e4aa3c
--- /dev/null
+++ b/jobs/Backend/Task/src/Services/ExchangeRateProvider.cs
@@ -0,0 +1,58 @@
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Options;
+using Microsoft.Extensions.Options;
+
+namespace ExchangeRateUpdater.Services
+{
+ public class ExchangeRateProvider : IExchangeRateProvider
+ {
+ ///
+ /// 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.
+ ///
+
+ private readonly ICnbApiService _cnbApiService;
+ private readonly CurrenciesOptions _currenciesOptions;
+
+ public ExchangeRateProvider(
+ ICnbApiService cnbApiService,
+ IOptionsMonitor currenciesOptions
+ )
+ {
+ _cnbApiService = cnbApiService;
+ _currenciesOptions = currenciesOptions.CurrentValue;
+ }
+
+ public async Task> GetExchangeRates(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // GET CNB RATES
+ var cnbRates = await _cnbApiService.GetExchangeRate(cancellationToken);
+
+ var result = new List();
+
+ foreach (var currencyCode in _currenciesOptions.Currencies)
+ {
+ var currencyTarget = new Currency(currencyCode);
+
+ var rate = cnbRates.Rates.FirstOrDefault(x => x.CurrencyCode.Equals(currencyTarget.Code));
+
+ if (rate == null)
+ {
+ continue;
+ }
+
+ var value = rate.Rate / rate.Amount;
+
+ var exchangeRate = new ExchangeRate(new Currency("CZK"), currencyTarget, value);
+
+ result.Add(exchangeRate);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/jobs/Backend/Task/src/Services/ICnbApiService.cs b/jobs/Backend/Task/src/Services/ICnbApiService.cs
new file mode 100644
index 000000000..56c76e559
--- /dev/null
+++ b/jobs/Backend/Task/src/Services/ICnbApiService.cs
@@ -0,0 +1,9 @@
+using ExchangeRateUpdater.Models;
+
+namespace ExchangeRateUpdater.Services
+{
+ public interface ICnbApiService
+ {
+ Task GetExchangeRate(CancellationToken cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/src/Services/IExchangeRateProvider.cs b/jobs/Backend/Task/src/Services/IExchangeRateProvider.cs
new file mode 100644
index 000000000..741e7309a
--- /dev/null
+++ b/jobs/Backend/Task/src/Services/IExchangeRateProvider.cs
@@ -0,0 +1,9 @@
+using ExchangeRateUpdater.Models;
+
+namespace ExchangeRateUpdater.Services
+{
+ public interface IExchangeRateProvider
+ {
+ Task> GetExchangeRates(CancellationToken cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/src/local.settings.json b/jobs/Backend/Task/src/local.settings.json
new file mode 100644
index 000000000..4fbd502b5
--- /dev/null
+++ b/jobs/Backend/Task/src/local.settings.json
@@ -0,0 +1,18 @@
+{
+ "CnbApi": {
+ "BaseAddress": "https://api.cnb.cz"
+ },
+ "CurrenciesOptions": {
+ "Currencies": [
+ "USD",
+ "EUR",
+ "CZK",
+ "JPY",
+ "KES",
+ "RUB",
+ "THB",
+ "TRY",
+ "XYZ"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/test/ExchangeRatesTests.csproj b/jobs/Backend/Task/test/ExchangeRatesTests.csproj
new file mode 100644
index 000000000..2e98757cb
--- /dev/null
+++ b/jobs/Backend/Task/test/ExchangeRatesTests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/test/Services/ExchangeRateProviderTests.cs b/jobs/Backend/Task/test/Services/ExchangeRateProviderTests.cs
new file mode 100644
index 000000000..5e4fbc8e7
--- /dev/null
+++ b/jobs/Backend/Task/test/Services/ExchangeRateProviderTests.cs
@@ -0,0 +1,93 @@
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Options;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace ExchangeRateUpdaterTest.Services;
+
+public class ExchangeRateUpdaterTests
+{
+ private readonly Mock _cnbApiServiceMock;
+ private readonly Mock> _currenciesOptionsMock;
+ private readonly ExchangeRateProvider _exchangeRateProvider;
+
+ public ExchangeRateUpdaterTests()
+ {
+ _cnbApiServiceMock = new Mock();
+
+ _currenciesOptionsMock = new Mock>();
+ _currenciesOptionsMock
+ .Setup(x => x.CurrentValue)
+ .Returns(new CurrenciesOptions
+ {
+ Currencies = new List
+ {
+ "USD",
+ "EUR"
+ }
+ });
+
+ _exchangeRateProvider = new ExchangeRateProvider(_cnbApiServiceMock.Object, _currenciesOptionsMock.Object);
+ }
+
+
+ [Fact]
+ public async Task GetExchangeRates_ShouldReturnExchangeRates_WhenCurrenciesAreProvided()
+ {
+ // Arrange
+ var cancellationToken = CancellationToken.None;
+
+ var cnbRates = new CnbRateDailyResponse
+ {
+ Rates = new List
+ {
+ new CnbRate { CurrencyCode = "USD", Rate = 22.5m, Amount = 1 },
+ new CnbRate { CurrencyCode = "EUR", Rate = 25.5m, Amount = 1 }
+ }
+ };
+
+ _cnbApiServiceMock
+ .Setup(service => service.GetExchangeRate(cancellationToken))
+ .ReturnsAsync(cnbRates);
+
+ // Act
+ var result = await _exchangeRateProvider.GetExchangeRates(cancellationToken);
+
+ // Assert
+ var exchangeRates = result.ToList();
+ Assert.Equal(2, exchangeRates.Count);
+ Assert.Contains(exchangeRates, rate => rate.TargetCurrency.Code == "USD" && rate.Value == 22.5m);
+ Assert.Contains(exchangeRates, rate => rate.TargetCurrency.Code == "EUR" && rate.Value == 25.5m);
+ }
+
+
+ [Fact]
+ public async Task GetExchangeRates_ShouldIgnoreUndefinedCurrencies()
+ {
+ // Arrange
+ var cancellationToken = CancellationToken.None;
+
+ var cnbRates = new CnbRateDailyResponse
+ {
+ Rates = new List
+ {
+ new CnbRate { CurrencyCode = "USD", Rate = 22.5m, Amount = 1 }
+ }
+ };
+
+ _cnbApiServiceMock
+ .Setup(service => service.GetExchangeRate(cancellationToken))
+ .ReturnsAsync(cnbRates);
+
+ // Act
+ var result = await _exchangeRateProvider.GetExchangeRates(cancellationToken);
+
+ // Assert
+ var exchangeRates = result.ToList();
+ Assert.Single(exchangeRates);
+ Assert.Contains(exchangeRates, rate => rate.TargetCurrency.Code == "USD" && rate.Value == 22.5m);
+ Assert.DoesNotContain(exchangeRates, rate => rate.TargetCurrency.Code == "EUR");
+ }
+}
\ No newline at end of file