diff --git a/jobs/Backend/Task/.gitignore b/jobs/Backend/Task/.gitignore
new file mode 100644
index 000000000..a4fe18bdd
--- /dev/null
+++ b/jobs/Backend/Task/.gitignore
@@ -0,0 +1,400 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Currency.cs
deleted file mode 100644
index f375776f2..000000000
--- 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/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs
deleted file mode 100644
index 58c5bb10e..000000000
--- 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 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.AcceptanceTests/ExchangeRateUpdater.AcceptanceTests.csproj b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ExchangeRateUpdater.AcceptanceTests.csproj
new file mode 100644
index 000000000..3e412ae9c
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ExchangeRateUpdater.AcceptanceTests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/Features/ExchangeRateUpdater.feature b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/Features/ExchangeRateUpdater.feature
new file mode 100644
index 000000000..057f814c6
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/Features/ExchangeRateUpdater.feature
@@ -0,0 +1,8 @@
+Feature: ExchangeRateUpdater
+
+ExchangeRateUpdater is an API that allows retrieving exchange rates from the Czech National Bank
+
+Scenario: ExchangeRateUpdater returns today's exchange rates
+ Given we have the ExchangeRateUpdater Api running
+ When we call the api/exchange-rates endpoint
+ Then the result should be today's exchange rates
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/Features/ExchangeRateUpdater.feature.cs b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/Features/ExchangeRateUpdater.feature.cs
new file mode 100644
index 000000000..fa3045977
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/Features/ExchangeRateUpdater.feature.cs
@@ -0,0 +1,117 @@
+// ------------------------------------------------------------------------------
+//
+// This code was generated by Reqnroll (https://www.reqnroll.net/).
+// Reqnroll Version:2.0.0.0
+// Reqnroll Generator Version:2.0.0.0
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+// ------------------------------------------------------------------------------
+#region Designer generated code
+#pragma warning disable
+namespace ExchangeRateUpdater.AcceptanceTests.Features
+{
+ using Reqnroll;
+ using System;
+ using System.Linq;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "2.0.0.0")]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [NUnit.Framework.TestFixtureAttribute()]
+ [NUnit.Framework.DescriptionAttribute("ExchangeRateUpdater")]
+ public partial class ExchangeRateUpdaterFeature
+ {
+
+ private global::Reqnroll.ITestRunner testRunner;
+
+ private static string[] featureTags = ((string[])(null));
+
+ private static global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "ExchangeRateUpdater", "ExchangeRateUpdater is an API that allows retrieving exchange rates from the Czec" +
+ "h National Bank", global::Reqnroll.ProgrammingLanguage.CSharp, featureTags);
+
+#line 1 "ExchangeRateUpdater.feature"
+#line hidden
+
+ [NUnit.Framework.OneTimeSetUpAttribute()]
+ public static async System.Threading.Tasks.Task FeatureSetupAsync()
+ {
+ }
+
+ [NUnit.Framework.OneTimeTearDownAttribute()]
+ public static async System.Threading.Tasks.Task FeatureTearDownAsync()
+ {
+ }
+
+ [NUnit.Framework.SetUpAttribute()]
+ public async System.Threading.Tasks.Task TestInitializeAsync()
+ {
+ testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(featureHint: featureInfo);
+ if (((testRunner.FeatureContext != null)
+ && (testRunner.FeatureContext.FeatureInfo.Equals(featureInfo) == false)))
+ {
+ await testRunner.OnFeatureEndAsync();
+ }
+ if ((testRunner.FeatureContext == null))
+ {
+ await testRunner.OnFeatureStartAsync(featureInfo);
+ }
+ }
+
+ [NUnit.Framework.TearDownAttribute()]
+ public async System.Threading.Tasks.Task TestTearDownAsync()
+ {
+ await testRunner.OnScenarioEndAsync();
+ global::Reqnroll.TestRunnerManager.ReleaseTestRunner(testRunner);
+ }
+
+ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo)
+ {
+ testRunner.OnScenarioInitialize(scenarioInfo);
+ testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext);
+ }
+
+ public async System.Threading.Tasks.Task ScenarioStartAsync()
+ {
+ await testRunner.OnScenarioStartAsync();
+ }
+
+ public async System.Threading.Tasks.Task ScenarioCleanupAsync()
+ {
+ await testRunner.CollectScenarioErrorsAsync();
+ }
+
+ [NUnit.Framework.TestAttribute()]
+ [NUnit.Framework.DescriptionAttribute("ExchangeRateUpdater returns today\'s exchange rates")]
+ public async System.Threading.Tasks.Task ExchangeRateUpdaterReturnsTodaysExchangeRates()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("ExchangeRateUpdater returns today\'s exchange rates", null, tagsOfScenario, argumentsOfScenario, featureTags);
+#line 5
+this.ScenarioInitialize(scenarioInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ testRunner.SkipScenario();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+#line 6
+ await testRunner.GivenAsync("we have the ExchangeRateUpdater Api running", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given ");
+#line hidden
+#line 7
+ await testRunner.WhenAsync("we call the api/exchange-rates endpoint", ((string)(null)), ((global::Reqnroll.Table)(null)), "When ");
+#line hidden
+#line 8
+ await testRunner.ThenAsync("the result should be today\'s exchange rates", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then ");
+#line hidden
+ }
+ await this.ScenarioCleanupAsync();
+ }
+ }
+}
+#pragma warning restore
+#endregion
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ImplicitUsings.cs b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ImplicitUsings.cs
new file mode 100644
index 000000000..ba42b01d2
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ImplicitUsings.cs
@@ -0,0 +1,2 @@
+global using FluentAssertions;
+global using Reqnroll;
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ProcessHelpers.cs b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ProcessHelpers.cs
new file mode 100644
index 000000000..f65f2626f
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/ProcessHelpers.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using System.Management;
+
+namespace ExchangeRateUpdater.AcceptanceTests
+{
+ internal class ProcessHelpers
+ {
+ internal static void KillProcessAndChildren(int processId)
+ {
+ var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessId=" + processId);
+ var managementObjects = searcher.Get();
+ foreach (var managementObject in managementObjects)
+ {
+ KillProcessAndChildren(Convert.ToInt32(managementObject["ProcessId"]));
+ }
+
+ try
+ {
+ Process process = Process.GetProcessById(processId);
+ process.Kill();
+ process.WaitForExit(); // Optionally wait for the process to exit
+ }
+ catch (ArgumentException)
+ {
+ // Process already exited
+ }
+ catch (InvalidOperationException)
+ {
+ // Process already exited
+ }
+ }
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/StepDefinitions/ExchangeRateUpdaterStepDefinitions.cs b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/StepDefinitions/ExchangeRateUpdaterStepDefinitions.cs
new file mode 100644
index 000000000..56fa8bb52
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.AcceptanceTests/StepDefinitions/ExchangeRateUpdaterStepDefinitions.cs
@@ -0,0 +1,65 @@
+using ExchangeRateUpdater.Domain;
+using ExchangeRateUpdater.Infrastructure.Data.Repositories;
+using ExchangeRateUpdater.Infrastructure.HttpClients;
+using Newtonsoft.Json;
+
+namespace ExchangeRateUpdater.AcceptanceTests.StepDefinitions
+{
+ [Binding]
+ public class ExchangeRateUpdaterStepDefinitions
+ {
+ IEnumerable? _obtainedExchangeRates;
+ System.Diagnostics.Process? _process;
+
+
+ [Given("we have the ExchangeRateUpdater Api running")]
+ public void GivenWeHaveTheExchangeRateUpdaterApiRunning()
+ {
+ LaunchExchangeRateUpdaterApi();
+ }
+
+ private void LaunchExchangeRateUpdaterApi()
+ {
+ _process = new System.Diagnostics.Process();
+ System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
+ startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
+ startInfo.FileName = "cmd.exe";
+ startInfo.Arguments = @"/C cd ..\..\..\..\ExchangeRateUpdater.Api\ && dotnet run";
+ _process.StartInfo = startInfo;
+ _process.Start();
+ }
+
+ [When("we call the api\\/exchange-rates endpoint")]
+ public void WhenWeCallTheApiExchange_RatesEndpoint()
+ {
+ using var httpClient = new HttpClient();
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, $" http://localhost:5129/api/exchange-rates");
+
+ var response = httpClient.SendAsync(httpRequestMessage);
+ response.Wait(30000);
+
+ if (response.Result.IsSuccessStatusCode)
+ {
+ var ratesTask = response.Result.Content.ReadAsStringAsync();
+ ratesTask.Wait(30000);
+
+ _obtainedExchangeRates = JsonConvert.DeserializeObject>(ratesTask.Result);
+ }
+ }
+
+ [Then("the result should be today's exchange rates")]
+ public void ThenTheResultShouldBeTodaysExchangeRates()
+ {
+ using var httpClient = new HttpClient();
+ httpClient.BaseAddress = new Uri("https://api.cnb.cz");
+
+ var cnbExchangeRates = (new CnbExchangeRateRepository(new CnbApiClient(httpClient))).GetTodayExchangeRatesAsync();
+ cnbExchangeRates.Wait(30000);
+
+ _obtainedExchangeRates.Should().NotBeNull();
+ _obtainedExchangeRates.Should().BeEquivalentTo(cnbExchangeRates.Result);
+
+ ProcessHelpers.KillProcessAndChildren(_process!.Id);
+ }
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Api/Controllers/ExchangeRatesController.cs b/jobs/Backend/Task/ExchangeRateUpdater.Api/Controllers/ExchangeRatesController.cs
new file mode 100644
index 000000000..90c670f65
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Api/Controllers/ExchangeRatesController.cs
@@ -0,0 +1,37 @@
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates;
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ExchangeRateUpdater.Api.Controllers;
+
+///
+/// Exchange Rates Controller
+///
+///
+[Route("api/exchange-rates")]
+[ApiController]
+public class ExchangeRatesController: ControllerBase
+{
+ private readonly IExchangeRateService _exchangeRateService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The exchange rate application service.
+ public ExchangeRatesController(IExchangeRateService exchangeRateAppService)
+ {
+ _exchangeRateService = exchangeRateAppService;
+ }
+
+ ///
+ /// Gets the exchange rates.
+ ///
+ /// A Http status code 200 response containing the exchange rates for today's date.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesDefaultResponseType]
+ public async Task>> GetExchangeRates()
+ {
+ return (await _exchangeRateService.GetTodayExchangeRatesAsync()).ToList();
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Api/ExchangeRateUpdater.Api.csproj b/jobs/Backend/Task/ExchangeRateUpdater.Api/ExchangeRateUpdater.Api.csproj
new file mode 100644
index 000000000..19a594fdd
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Api/ExchangeRateUpdater.Api.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Api/Program.cs b/jobs/Backend/Task/ExchangeRateUpdater.Api/Program.cs
new file mode 100644
index 000000000..a271cb2a4
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Api/Program.cs
@@ -0,0 +1,36 @@
+using ExchangeRateUpdater.ApplicationServices;
+using ExchangeRateUpdater.Infrastructure;
+
+var config = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .AddEnvironmentVariables()
+ .Build();
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+
+builder.Services.AddSwaggerGen()
+ .AddApplicationServices()
+ .AddInfrastructureServices(config);
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Api/Properties/launchSettings.json b/jobs/Backend/Task/ExchangeRateUpdater.Api/Properties/launchSettings.json
new file mode 100644
index 000000000..3998fc43f
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Api/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:12632",
+ "sslPort": 44306
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5129",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Api/appsettings.json b/jobs/Backend/Task/ExchangeRateUpdater.Api/appsettings.json
new file mode 100644
index 000000000..4bb10c6c2
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Api/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ExchangeRates": {
+ "CnbApi": "https://api.cnb.cz"
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRateUpdater.ApplicationServices.csproj b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRateUpdater.ApplicationServices.csproj
new file mode 100644
index 000000000..166cdb600
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRateUpdater.ApplicationServices.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/Dto/CurrencyDto.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/Dto/CurrencyDto.cs
new file mode 100644
index 000000000..10a4298d1
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/Dto/CurrencyDto.cs
@@ -0,0 +1,13 @@
+namespace ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+
+///
+/// Represents the DTO of a Three-letter ISO 4217 currency code.
+///
+public class CurrencyDto
+{
+ public string Code { get; set; } = null!;
+ public override string ToString()
+ {
+ return Code;
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/Dto/ExchangeRateDto.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/Dto/ExchangeRateDto.cs
new file mode 100644
index 000000000..72b664276
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/Dto/ExchangeRateDto.cs
@@ -0,0 +1,15 @@
+namespace ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+
+///
+/// Represents the DTO of an exchange rate between two currencies.
+///
+public class ExchangeRateDto
+{
+ public CurrencyDto SourceCurrency { get; set; } = null!;
+ public CurrencyDto TargetCurrency { get; set; } = null!;
+ public decimal Value { get; set; }
+ public override string ToString()
+ {
+ return $"{SourceCurrency}/{TargetCurrency}={Value}";
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/ExchangeRateService.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/ExchangeRateService.cs
new file mode 100644
index 000000000..500a390d9
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/ExchangeRateService.cs
@@ -0,0 +1,54 @@
+using AutoMapper;
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+using ExchangeRateUpdater.ApplicationServices.Interfaces;
+using ExchangeRateUpdater.Domain;
+
+namespace ExchangeRateUpdater.ApplicationServices.ExchangeRates;
+
+///
+/// Exchange rate application service.
+/// The service layer intermediates between internal model and exposed API model.
+///
+/// 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 class ExchangeRateService : IExchangeRateService
+{
+ private readonly IMapper _mapper;
+ private readonly IExchangeRateRepository _exchangeRateRepository;
+
+ public ExchangeRateService(IMapper mapper, IExchangeRateRepository exchangeRateRepository)
+ {
+ _mapper = mapper;
+ _exchangeRateRepository = exchangeRateRepository;
+ }
+
+ ///
+ public async Task> GetExchangeRatesAsync(IEnumerable currencies, DateTime date)
+ {
+ var exchangeRates = (await _exchangeRateRepository.GetExchangeRatesAsync(date)).Where(e => currencies.Any(c => c.Code == e.SourceCurrency.Code));
+
+ return _mapper.Map>(exchangeRates);
+ }
+
+ ///
+ public async Task> GetExchangeRatesAsync(DateTime date)
+ {
+ var exchangeRates = await _exchangeRateRepository.GetExchangeRatesAsync(date);
+ return _mapper.Map>(exchangeRates);
+ }
+
+ ///
+ public async Task> GetTodayExchangeRatesAsync(IEnumerable? currencies = null)
+ {
+ var exchangeRates = await _exchangeRateRepository.GetTodayExchangeRatesAsync();
+
+ exchangeRates = currencies != null && currencies.Any() ?
+ exchangeRates.Where(e => currencies.Any(c => c.Code == e.SourceCurrency.Code)) :
+ exchangeRates;
+
+ return _mapper.Map>(exchangeRates);
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/IExchangeRateService.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/IExchangeRateService.cs
new file mode 100644
index 000000000..ab9fdab2b
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ExchangeRates/IExchangeRateService.cs
@@ -0,0 +1,40 @@
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+using ExchangeRateUpdater.Domain;
+
+namespace ExchangeRateUpdater.ApplicationServices.ExchangeRates;
+
+///
+/// Exchange rate application service interface.
+/// The service layer intermediates between internal model and exposed API model.
+///
+/// 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 interface IExchangeRateService
+{
+ ///
+ /// Gets the exchange rates asynchronously.
+ ///
+ /// The currencies to check.
+ /// The date to check.
+ /// An Enumeration of
+ Task> GetExchangeRatesAsync(IEnumerable currencies, DateTime date);
+
+ ///
+ /// Gets the exchange rates asynchronously for a given date.
+ ///
+ /// The date to check.
+ /// An Enumeration of
+ Task> GetExchangeRatesAsync(DateTime date);
+
+ ///
+ /// Gets today's exchange rates asynchronously.
+ ///
+ /// The currencies to check (optional)
+ ///
+ /// An Enumeration of
+ ///
+ Task> GetTodayExchangeRatesAsync(IEnumerable? currencies = null);
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/Interfaces/IExchangeRateRepository.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/Interfaces/IExchangeRateRepository.cs
new file mode 100644
index 000000000..c00a7f45d
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/Interfaces/IExchangeRateRepository.cs
@@ -0,0 +1,23 @@
+using ExchangeRateUpdater.Domain;
+
+namespace ExchangeRateUpdater.ApplicationServices.Interfaces;
+
+///
+/// Czech National Bank exchange rate repository interface.
+///
+///
+public interface IExchangeRateRepository
+{
+ ///
+ /// Gets today's exchange rates asynchronous.
+ ///
+ /// An Enumeration of
+ Task> GetTodayExchangeRatesAsync();
+
+ ///
+ /// Gets the exchange rates asynchronously for a given date.
+ ///
+ /// The date to check.
+ /// An Enumeration of
+ Task> GetExchangeRatesAsync(DateTime date);
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/Mappings/DefaultMapperProfile.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/Mappings/DefaultMapperProfile.cs
new file mode 100644
index 000000000..5032c208a
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/Mappings/DefaultMapperProfile.cs
@@ -0,0 +1,17 @@
+using AutoMapper;
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+using ExchangeRateUpdater.Domain;
+
+namespace ExchangeRateUpdater.ApplicationServices.MapperProfiles;
+
+public class DefaultMapperProfile : Profile
+{
+ ///
+ /// Constructor.
+ ///
+ public DefaultMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ServiceCollectionExtensions.cs b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..92a6ee014
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.ApplicationServices/ServiceCollectionExtensions.cs
@@ -0,0 +1,16 @@
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates;
+using ExchangeRateUpdater.ApplicationServices.MapperProfiles;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ExchangeRateUpdater.ApplicationServices;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddApplicationServices(this IServiceCollection services)
+ {
+ services.AddAutoMapper(typeof(DefaultMapperProfile));
+ services.AddScoped();
+
+ return services;
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Console/ExchangeRateUpdater.Console.csproj b/jobs/Backend/Task/ExchangeRateUpdater.Console/ExchangeRateUpdater.Console.csproj
new file mode 100644
index 000000000..7aba7b3ed
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Console/ExchangeRateUpdater.Console.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+ PreserveNewest
+ true
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Console/Program.cs b/jobs/Backend/Task/ExchangeRateUpdater.Console/Program.cs
new file mode 100644
index 000000000..b1550cc8f
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Console/Program.cs
@@ -0,0 +1,58 @@
+using ExchangeRateUpdater.ApplicationServices;
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates;
+using ExchangeRateUpdater.Domain;
+using ExchangeRateUpdater.Infrastructure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+public class Program
+{
+
+ private static IEnumerable Currencies =
+ [
+ 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 async Task Main(string[] args)
+ {
+ // Application configuration
+ var config = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .AddEnvironmentVariables()
+ .Build();
+
+ // IoC/DI configuration
+ var services = new ServiceCollection();
+ services.AddApplicationServices();
+ services.AddInfrastructureServices(config);
+
+ using var serviceProvider = services.BuildServiceProvider();
+ var exchangeRateService = serviceProvider.GetRequiredService();
+
+ // Console test
+ try
+ {
+ var rates = (await exchangeRateService.GetTodayExchangeRatesAsync(Currencies)).ToList();
+
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Console/appsettings.json b/jobs/Backend/Task/ExchangeRateUpdater.Console/appsettings.json
new file mode 100644
index 000000000..3450f6b57
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Console/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "ExchangeRates": {
+ "CnbApi": "https://api.cnb.cz"
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Domain/CnbExchangeRates.cs b/jobs/Backend/Task/ExchangeRateUpdater.Domain/CnbExchangeRates.cs
new file mode 100644
index 000000000..f06aaec7f
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Domain/CnbExchangeRates.cs
@@ -0,0 +1,29 @@
+namespace ExchangeRateUpdater.Domain;
+
+///
+/// Represents the exchange rates information provided by the Czech National Bank.
+///
+public class CnbExchangeRates
+{
+ public IEnumerable Rates { get; set; } = new List();
+}
+
+///
+/// Represents a Czech National Bank exchange rate entry.
+///
+public class CnbExchangeRate
+{
+ public DateTimeOffset ValidFor { get; set; }
+
+ public int Order { get; set; }
+
+ public string Country { get; set; } = null!;
+
+ public string Currency { get; set; } = null!;
+
+ public int Amount { get; set; }
+
+ public string CurrencyCode { get; set; } = null!;
+
+ public decimal Rate { get; set; }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Domain/Currency.cs b/jobs/Backend/Task/ExchangeRateUpdater.Domain/Currency.cs
new file mode 100644
index 000000000..8ebb1a4a2
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Domain/Currency.cs
@@ -0,0 +1,35 @@
+namespace ExchangeRateUpdater.Domain;
+
+///
+/// Represents the a Three-letter ISO 4217 currency code.
+///
+///
+public class Currency : IEquatable
+{
+ public Currency(string code)
+ {
+ Code = code;
+ }
+
+ ///
+ /// Three-letter ISO 4217 code of the currency.
+ ///
+ public string Code { get; }
+
+ public override string ToString()
+ {
+ return Code;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as Currency);
+ }
+
+ public bool Equals(Currency? other)
+ {
+ return other != null && Code == other.Code;
+ }
+
+ public override int GetHashCode() => Code.GetHashCode();
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Domain/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRateUpdater.Domain/ExchangeRate.cs
new file mode 100644
index 000000000..5f9a83331
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Domain/ExchangeRate.cs
@@ -0,0 +1,41 @@
+namespace ExchangeRateUpdater.Domain;
+
+///
+/// Represents the DTO of an exchange rate between two currencies.
+///
+///
+public class ExchangeRate : IEquatable
+{
+ 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}";
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as ExchangeRate);
+ }
+
+ public bool Equals(ExchangeRate? other)
+ {
+ return other != null &&
+ SourceCurrency.Equals(other.SourceCurrency) &&
+ TargetCurrency.Equals(other.TargetCurrency) &&
+ Value == other.Value;
+ }
+
+ public override int GetHashCode() => HashCode.Combine(SourceCurrency, TargetCurrency, Value);
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj b/jobs/Backend/Task/ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj
new file mode 100644
index 000000000..617ded995
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj
@@ -0,0 +1,7 @@
+
+
+ net8.0
+ enable
+ enable
+
+
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/Data/Repositories/CnbExchangeRateRepository.cs b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/Data/Repositories/CnbExchangeRateRepository.cs
new file mode 100644
index 000000000..c7d8db7dd
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/Data/Repositories/CnbExchangeRateRepository.cs
@@ -0,0 +1,48 @@
+using ExchangeRateUpdater.ApplicationServices.Interfaces;
+using ExchangeRateUpdater.Domain;
+using ExchangeRateUpdater.Infrastructure.HttpClients;
+
+namespace ExchangeRateUpdater.Infrastructure.Data.Repositories;
+
+///
+/// Czech National Bank exchange rate repository.
+///
+///
+public class CnbExchangeRateRepository : IExchangeRateRepository
+{
+ //TODO: Implement a cache if needed.
+
+ private readonly ICnbApiClient _cnbApiClient;
+
+ public CnbExchangeRateRepository(ICnbApiClient cnbApiClient)
+ {
+ _cnbApiClient = cnbApiClient;
+ }
+
+ ///
+ public async Task> GetExchangeRatesAsync(DateTime date)
+ {
+ return (await _cnbApiClient.GetExchangeRatesAsync(date)).ToExchangeRates();
+ }
+
+ ///
+ public async Task> GetTodayExchangeRatesAsync()
+ {
+ return (await _cnbApiClient.GetTodayExchangeRatesAsync()).ToExchangeRates();
+ }
+}
+
+public static class CnbExchangeRatesExtensions
+{
+ ///
+ /// Converts CnbExchangeRates to ExchangeRate enumeration.
+ ///
+ /// The CNB exchange rates.
+ ///
+ /// IEnumerable
+ ///
+ public static IEnumerable ToExchangeRates(this CnbExchangeRates cnbExchangeRates)
+ {
+ return cnbExchangeRates.Rates.Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount));
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/ExchangeRateUpdater.Infrastructure.csproj b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/ExchangeRateUpdater.Infrastructure.csproj
new file mode 100644
index 000000000..65e0e531b
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/ExchangeRateUpdater.Infrastructure.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/CnbApiClient.cs b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/CnbApiClient.cs
new file mode 100644
index 000000000..b6d91e456
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/CnbApiClient.cs
@@ -0,0 +1,47 @@
+using ExchangeRateUpdater.Domain;
+using ExchangeRateUpdater.Infrastructure.HttpClients.Config;
+using Newtonsoft.Json;
+
+namespace ExchangeRateUpdater.Infrastructure.HttpClients;
+
+///
+/// Czech National Bank API client.
+///
+public class CnbApiClient : ICnbApiClient
+{
+ private readonly HttpClient _httpClient;
+
+ string ExchangeRatesRequestUrl => UrlSettings.CnbApiOperations.DailyExchangeRatesUrl;
+
+ public CnbApiClient(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ ///
+ public async Task GetExchangeRatesAsync(DateTime date)
+ {
+ var content = await GetAsync($"{ExchangeRatesRequestUrl}?date={date:yyyy-MM-dd}");
+
+ return JsonConvert.DeserializeObject(content)!; ;
+ }
+
+ ///
+ public async Task GetTodayExchangeRatesAsync()
+ {
+ var content = await GetAsync(ExchangeRatesRequestUrl);
+
+ return JsonConvert.DeserializeObject(content)!;
+ }
+
+ private async Task GetAsync(string requestUri)
+ {
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
+
+ var response = await _httpClient.SendAsync(httpRequestMessage);
+
+ return response.IsSuccessStatusCode ?
+ await response.Content.ReadAsStringAsync() :
+ string.Empty;
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/ICnbApiClient.cs b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/ICnbApiClient.cs
new file mode 100644
index 000000000..580f1c7a5
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/ICnbApiClient.cs
@@ -0,0 +1,22 @@
+using ExchangeRateUpdater.Domain;
+
+namespace ExchangeRateUpdater.Infrastructure.HttpClients;
+
+///
+/// Czech National Bank exchange rates API client definition.
+///
+public interface ICnbApiClient
+{
+ ///
+ /// Gets a collection of today's exchange rates.
+ ///
+ ///
+ Task GetTodayExchangeRatesAsync();
+
+ ///
+ /// Gets a collection of exchange rates for a given date.
+ ///
+ /// The date to retrieve the exchange rates
+ ///
+ Task GetExchangeRatesAsync(DateTime date);
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/Settings/ExchangeRatesSettings.cs b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/Settings/ExchangeRatesSettings.cs
new file mode 100644
index 000000000..d8984b24c
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/Settings/ExchangeRatesSettings.cs
@@ -0,0 +1,7 @@
+namespace ExchangeRateUpdater.Infrastructure.HttpClients.Config;
+
+public class ExchangeRatesSettings
+{
+ public const string SectionName = "ExchangeRates";
+ public string CnbApi { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/Settings/UrlSettings.cs b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/Settings/UrlSettings.cs
new file mode 100644
index 000000000..fa6c29fb0
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/HttpClients/Settings/UrlSettings.cs
@@ -0,0 +1,9 @@
+namespace ExchangeRateUpdater.Infrastructure.HttpClients.Config;
+
+public static class UrlSettings
+{
+ public static class CnbApiOperations
+ {
+ public static string DailyExchangeRatesUrl => "/cnbapi/exrates/daily";
+ }
+}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/ServiceCollectionExtensions.cs b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..6c4ee16fb
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.Infrastructure/ServiceCollectionExtensions.cs
@@ -0,0 +1,24 @@
+using ExchangeRateUpdater.ApplicationServices.Interfaces;
+using ExchangeRateUpdater.Infrastructure.Data.Repositories;
+using ExchangeRateUpdater.Infrastructure.HttpClients;
+using ExchangeRateUpdater.Infrastructure.HttpClients.Config;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ExchangeRateUpdater.Infrastructure;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddScoped();
+
+ services.Configure(configuration.GetSection(ExchangeRatesSettings.SectionName));
+
+ var cnbApi = configuration.GetSection(ExchangeRatesSettings.SectionName).GetValue("CnbApi")!;
+
+ services.AddHttpClient(client => client.BaseAddress = new Uri(cnbApi));
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateRepositoryTests.cs b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateRepositoryTests.cs
new file mode 100644
index 000000000..547c75641
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateRepositoryTests.cs
@@ -0,0 +1,87 @@
+using ExchangeRateUpdater.Domain;
+using ExchangeRateUpdater.Infrastructure.Data.Repositories;
+using ExchangeRateUpdater.Infrastructure.HttpClients;
+using Moq;
+using NUnit.Framework;
+
+namespace ExchangeRateUpdater.UnitTests;
+
+[TestFixture]
+public class ExchangeRateRepositoryTests
+{
+ private Mock _cnbApiClientMock;
+ private CnbExchangeRateRepository _exchangeRateRepository;
+
+ [SetUp]
+ public void Setup()
+ {
+ _cnbApiClientMock = new Mock();
+ _exchangeRateRepository = new CnbExchangeRateRepository(_cnbApiClientMock.Object);
+ }
+
+ [Test]
+ public async Task GetTodayExchangeRatesAsync_WhenApiReturnsValidRates_ReturnsExchangeRates()
+ {
+ // Arrange
+ var exchangeRates = new CnbExchangeRates
+ {
+ Rates =
+ [
+ new() { CurrencyCode = "JPY", Rate = 23.084m, Amount = 1 },
+ new() { CurrencyCode = "EUR", Rate = 25.020m, Amount = 1 },
+ new() { CurrencyCode = "TRY", Rate = 0.68065m, Amount = 1 },
+ ]
+ };
+
+ var expectedExchangeRates = exchangeRates.Rates.Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount)).ToList();
+
+ _cnbApiClientMock.Setup(a => a.GetTodayExchangeRatesAsync()).ReturnsAsync(exchangeRates);
+
+ // Act
+ var result = (await _exchangeRateRepository.GetTodayExchangeRatesAsync()).ToList();
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(expectedExchangeRates.Count));
+ Assert.That(expectedExchangeRates, Is.EquivalentTo(result));
+ }
+
+ [Test]
+ public async Task GetExchangeRatesAsync_WhenDateIsPassed_ReturnsCorrectExchangeRates()
+ {
+ // Arrange
+ var exchangeRates = new CnbExchangeRates
+ {
+ Rates =
+ [
+ new() { CurrencyCode = "JPY", Rate = 23.084m, Amount = 1 },
+ new() { CurrencyCode = "EUR", Rate = 25.020m, Amount = 1 },
+ new() { CurrencyCode = "TRY", Rate = 0.68065m, Amount = 1 },
+ ]
+ };
+
+ var expectedExchangeRates = exchangeRates.Rates.Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount)).ToList();
+
+ _cnbApiClientMock.Setup(a => a.GetExchangeRatesAsync(DateTime.Today)).ReturnsAsync(exchangeRates);
+
+ // Act
+ var result = (await _exchangeRateRepository.GetExchangeRatesAsync(DateTime.Today)).ToList();
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(expectedExchangeRates.Count));
+ Assert.That(expectedExchangeRates, Is.EquivalentTo(result));
+ }
+
+ [Test]
+ public async Task GetTodayExchangeRatesAsync_WhenApiDoesNotReturnRates_ReturnsEmpty()
+ {
+ // Arrange
+ var exchangeRates = new CnbExchangeRates { Rates = Array.Empty() };
+ _cnbApiClientMock.Setup(a => a.GetTodayExchangeRatesAsync()).ReturnsAsync(exchangeRates);
+
+ // Act
+ var result = await _exchangeRateRepository.GetTodayExchangeRatesAsync();
+
+ // Assert
+ Assert.That(result, Is.Empty);
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateServiceTests.cs b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateServiceTests.cs
new file mode 100644
index 000000000..8259e26fd
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateServiceTests.cs
@@ -0,0 +1,107 @@
+using AutoMapper;
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates;
+using ExchangeRateUpdater.ApplicationServices.ExchangeRates.Dto;
+using ExchangeRateUpdater.ApplicationServices.Interfaces;
+using ExchangeRateUpdater.Domain;
+using NUnit.Framework;
+using Moq;
+
+namespace ExchangeRateUpdater.UnitTests;
+
+[TestFixture]
+public class ExchangeRateServiceTests
+{
+ private Mock _mapperMock;
+ private Mock _exchangeRateRepository;
+ private ExchangeRateService _exchangeRateService;
+
+ [SetUp]
+ public void Setup()
+ {
+ _mapperMock = new Mock();
+ _exchangeRateRepository = new Mock();
+ _exchangeRateService = new ExchangeRateService(_mapperMock.Object, _exchangeRateRepository.Object);
+ }
+
+ [Test]
+ public async Task GetTodayExchangeRatesAsync_WhenCurrenciesIsNull_ReturnsAllRates()
+ {
+ // Arrange
+ var exchangeRates = new ExchangeRate[]
+ {
+ new(new Currency("JPY"), new Currency("CZK"), 0.15499m),
+ new(new Currency("EUR"), new Currency("CZK"), 25.020m),
+ new(new Currency("TRY"), new Currency("CZK"), 0.68065m)
+ };
+
+ var exchangeRatesDto = new ExchangeRateDto[]
+ {
+ new ExchangeRateDto { SourceCurrency = new CurrencyDto { Code = "JPY" }, TargetCurrency = new CurrencyDto { Code = "CZK" }, Value = 0.15499m },
+ new ExchangeRateDto { SourceCurrency = new CurrencyDto { Code = "EUR" }, TargetCurrency = new CurrencyDto { Code = "CZK" }, Value = 25.020m },
+ new ExchangeRateDto { SourceCurrency = new CurrencyDto { Code = "TRY" }, TargetCurrency = new CurrencyDto { Code = "CZK" }, Value = 0.68065m }
+ };
+
+ var expectedDtos = exchangeRatesDto.ToList();
+
+ _exchangeRateRepository.Setup(r => r.GetTodayExchangeRatesAsync()).ReturnsAsync(exchangeRates);
+
+ _mapperMock.Setup(m => m.Map>(exchangeRates)).Returns(exchangeRatesDto);
+
+ // Act
+ var result = await _exchangeRateService.GetTodayExchangeRatesAsync();
+
+ // Assert
+ Assert.That(result, Is.EquivalentTo(expectedDtos));
+ _exchangeRateRepository.Verify(r => r.GetTodayExchangeRatesAsync(), Times.Once);
+ }
+
+ [Test]
+ public async Task GetTodayExchangeRatesAsync_WithCurrencies_ReturnsFilteredRates()
+ {
+ // Arrange
+ var exchangeRates = new ExchangeRate[]
+ {
+ new(new Currency("JPY"), new Currency("CZK"), 0.15499m),
+ new(new Currency("EUR"), new Currency("CZK"), 25.020m),
+ new(new Currency("TRY"), new Currency("CZK"), 0.68065m),
+ };
+
+ var currencies = new Currency[] { new("JPY"), new("EUR"), new("ABC") };
+
+ var expectedDto = new ExchangeRateDto[]
+ {
+ new ExchangeRateDto { SourceCurrency = new CurrencyDto { Code = "JPY" }, TargetCurrency = new CurrencyDto { Code = "CZK" },Value = 0.15499m },
+ new ExchangeRateDto { SourceCurrency = new CurrencyDto { Code = "EUR" }, TargetCurrency = new CurrencyDto { Code = "CZK" },Value = 25.020m }
+ };
+
+ _exchangeRateRepository
+ .Setup(r => r.GetTodayExchangeRatesAsync())
+ .ReturnsAsync(exchangeRates);
+
+ _mapperMock
+ .Setup(m => m.Map>(It.IsAny>()))
+ .Returns(expectedDto);
+
+ // Act
+ var result = await _exchangeRateService.GetTodayExchangeRatesAsync(currencies);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(expectedDto));
+ _exchangeRateRepository.Verify(r => r.GetTodayExchangeRatesAsync(), Times.Once);
+ _mapperMock.Verify(m => m.Map>(It.IsAny>()), Times.Once);
+ }
+
+ [Test]
+ public async Task GetTodayExchangeRatesAsync_WhenRepositoryReturnsEmpty_ShouldReturnEmptyList()
+ {
+ // Arrange
+ _exchangeRateRepository.Setup(r => r.GetTodayExchangeRatesAsync()).ReturnsAsync(Array.Empty());
+
+ // Act
+ var result = await _exchangeRateService.GetTodayExchangeRatesAsync();
+
+ // Assert
+ Assert.That(result, Is.Empty);
+ _exchangeRateRepository.Verify(r => r.GetTodayExchangeRatesAsync(), Times.Once);
+ }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateUpdater.UnitTests.csproj b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateUpdater.UnitTests.csproj
new file mode 100644
index 000000000..2365a0dab
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/ExchangeRateUpdater.UnitTests.csproj
@@ -0,0 +1,26 @@
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/Properties/launchSettings.json b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/Properties/launchSettings.json
new file mode 100644
index 000000000..5dabb34f8
--- /dev/null
+++ b/jobs/Backend/Task/ExchangeRateUpdater.UnitTests/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "ExchangeRateUpdater.UnitTests": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:50659;http://localhost:50660"
+ }
+ }
+}
\ No newline at end of file
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
index 89be84daf..936052d7a 100644
--- a/jobs/Backend/Task/ExchangeRateUpdater.sln
+++ b/jobs/Backend/Task/ExchangeRateUpdater.sln
@@ -1,9 +1,29 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
+# Visual Studio Version 17
+VisualStudioVersion = 17.7.34031.279
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DD8EECBE-6791-493A-A8D2-F382EB5F3C37}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater.Domain", "ExchangeRateUpdater.Domain\ExchangeRateUpdater.Domain.csproj", "{2495802D-C617-4AF1-8B77-F96AD00C1C0D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater.Infrastructure", "ExchangeRateUpdater.Infrastructure\ExchangeRateUpdater.Infrastructure.csproj", "{5134FAA8-D78A-415F-997D-FE10A2724AC2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.UnitTests", "ExchangeRateUpdater.UnitTests\ExchangeRateUpdater.UnitTests.csproj", "{6D01BF57-15FB-4DED-9508-55E604AB3492}"
+ ProjectSection(ProjectDependencies) = postProject
+ {5134FAA8-D78A-415F-997D-FE10A2724AC2} = {5134FAA8-D78A-415F-997D-FE10A2724AC2}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Api", "ExchangeRateUpdater.Api\ExchangeRateUpdater.Api.csproj", "{EE2A6C8D-463E-4037-9343-09686663EDA4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Console", "ExchangeRateUpdater.Console\ExchangeRateUpdater.Console.csproj", "{AAAF0AAD-7C3F-4B8F-8051-C161345A6CEA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.ApplicationServices", "ExchangeRateUpdater.ApplicationServices\ExchangeRateUpdater.ApplicationServices.csproj", "{71AED0BF-FA27-48D2-B001-434C9CAFD7DA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.AcceptanceTests", "ExchangeRateUpdater.AcceptanceTests\ExchangeRateUpdater.AcceptanceTests.csproj", "{F237F1E7-FB7B-48FB-8BD9-7D2425DAA2FE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,12 +31,39 @@ Global
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
+ {2495802D-C617-4AF1-8B77-F96AD00C1C0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2495802D-C617-4AF1-8B77-F96AD00C1C0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2495802D-C617-4AF1-8B77-F96AD00C1C0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2495802D-C617-4AF1-8B77-F96AD00C1C0D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5134FAA8-D78A-415F-997D-FE10A2724AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5134FAA8-D78A-415F-997D-FE10A2724AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5134FAA8-D78A-415F-997D-FE10A2724AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5134FAA8-D78A-415F-997D-FE10A2724AC2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D01BF57-15FB-4DED-9508-55E604AB3492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D01BF57-15FB-4DED-9508-55E604AB3492}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D01BF57-15FB-4DED-9508-55E604AB3492}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D01BF57-15FB-4DED-9508-55E604AB3492}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EE2A6C8D-463E-4037-9343-09686663EDA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EE2A6C8D-463E-4037-9343-09686663EDA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EE2A6C8D-463E-4037-9343-09686663EDA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EE2A6C8D-463E-4037-9343-09686663EDA4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AAAF0AAD-7C3F-4B8F-8051-C161345A6CEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAAF0AAD-7C3F-4B8F-8051-C161345A6CEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAAF0AAD-7C3F-4B8F-8051-C161345A6CEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAAF0AAD-7C3F-4B8F-8051-C161345A6CEA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {71AED0BF-FA27-48D2-B001-434C9CAFD7DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {71AED0BF-FA27-48D2-B001-434C9CAFD7DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {71AED0BF-FA27-48D2-B001-434C9CAFD7DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {71AED0BF-FA27-48D2-B001-434C9CAFD7DA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F237F1E7-FB7B-48FB-8BD9-7D2425DAA2FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F237F1E7-FB7B-48FB-8BD9-7D2425DAA2FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F237F1E7-FB7B-48FB-8BD9-7D2425DAA2FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F237F1E7-FB7B-48FB-8BD9-7D2425DAA2FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {CB1A2C41-3347-44F6-8EA2-04658E88DED6}
+ EndGlobalSection
EndGlobal
diff --git a/jobs/Backend/Task/README.md b/jobs/Backend/Task/README.md
new file mode 100644
index 000000000..144fb1c6c
--- /dev/null
+++ b/jobs/Backend/Task/README.md
@@ -0,0 +1,70 @@
+# ExchangeRateUpdater
+
+ExchangeRateUpdater is an exercise solution that allows interacting with Czech National Bank (CNB) API to retrieve currencies exchange rates.
+
+## Projects
+
+### ExchangeRateUpdater.Api
+The API layer of the application. It exposes HTTP endpoints to fetch exchange rates.
+
+This is actually not needed to solve the exercise, but it serves as example of an API that can be consumed by other actors/services in the system.
+
+### ExchangeRateUpdater.ApplicationServices
+This project contains the Service that intermediates between domain layers and exposed API.
+It contains the specification of `IExchangeRateRepository`, the inferface for the repository pattern, that allows the possibility to change data sources easily.
+
+### ExchangeRateUpdater.Console
+A console application used to showcase the exercise.
+
+### ExchangeRateUpdater.Domain
+This project contains the data model (domain entities) of the application.
+
+### ExchangeRateUpdater.Infrastructure
+This project is responsible for data access and external API integrations, including the interaction with the Czech National Bank API.
+It contains the implementation of the Repository pattern interface.
+
+### ExchangeRateUpdater.UnitTests
+A small set of example unit tests that ensure correctness of the application.
+
+### ExchangeRateUpdater.AcceptanceTests
+This project defines a basic acceptance test that specifies the main scenario using Gherkin language and runs a end-to-end test with real instances.
+
+It runs an instance of `ExchangeRateUpdater.Api`, retrieves today's exchange rates and also runs a query against CNB Api and ensures the results are the same.
+
+Limitations: This only works on Windows in this version for demonstration purposes (the example use Windows operating system specific helpers to run processes).
+
+## Future improvements
+
+- **Documentation**: Only main classes and interfaces have been documented for demonstration purposes. In a production environment this should be revisited for completeness and correctness.
+
+- **Logging & performance tracing**: It is essential in enterprise solutions to be able to monitor and measure applications performance and possible errors.
+ - Examples: Serilog, NLog, etc. for logging. NewRelic, Application Insights, etc... for metrics.
+
+- **Cache**: It usually makes sense to implement a cache for the data in the Repository layer (in this case in `CnbExchangeRateRepository`).
+ - This can be achieved using different persistence strategies, like in-Memory caching, or distributed caching (Redis), for example.
+
+- **Retry policy**: It's very common to include a retry policy to exposed APIs.
+
+- **Docker, CI/CD, IaC...**: This has not been included in this exercise but it would be necessary in a modern production environment.
+
+
+## Running the Application
+
+The application can be run through the Visual Studio IDE or the command line.
+If you just run it from the Visual Studio IDE, the Console application is configured as startup project, and it will showcase the exchange rates exercise.
+
+### API
+- Run the command `dotnet run` in the `ExchangeRateUpdater.Api` project directory to run the API.
+ - The API will be available at [https://localhost:5129](http://localhost:5129).
+ - Access [http://localhost:5129/swagger/](http://localhost:5129/swagger/) to see the published `ExchangeRates` endpoint in Swagger.
+
+### Console Application
+- Running `dotnet run` into the `ExchangeRateUpdater.Console` folder will display the requested exchange rates.
+
+### Testing
+- Running `dotnet test` in any tests directory will execute the application tests.
+
+### Configuration
+
+`appsettings.json` file or environment variables are used to configure the application settings.
+In this project, the only setting is the CNB API URL, which is already configured in `appsettings.json` file.
\ No newline at end of file