diff --git a/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateProviderTests/ExchangeRateProviderTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateProviderTests/ExchangeRateProviderTests.cs
new file mode 100644
index 000000000..54e7ab23e
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateProviderTests/ExchangeRateProviderTests.cs
@@ -0,0 +1,73 @@
+using ExchangeRateUpdater;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Services;
+using Moq;
+using Xunit;
+
+namespace ExchangeRateUpdaterTests.ExchangeRateProviderTests
+{
+
+    public class ExchangeRateProviderTests
+    {
+        [Fact]
+        public async Task GetExchangeRatesAsync_ReturnsRatesFromService_WhenCurrenciesProvided()
+        {
+            // Arrange
+            var currencies = new[]
+            {
+            new Currency("CZK"),
+            new Currency("USD")
+        };
+
+            var expectedRates = new List<ExchangeRate>
+        {
+            new ExchangeRate(new Currency("CZK"), new Currency("USD"), 22.5m)
+        };
+
+            var serviceMock = new Mock<IExchangeRateProviderService>();
+            serviceMock.Setup(s => s.GetExchangeRateAsync(It.Is<IEnumerable<Currency>>(c => c.SequenceEqual(currencies))))
+                       .ReturnsAsync(expectedRates);
+
+            var provider = new ExchangeRateProvider(serviceMock.Object);
+
+            // Act
+            var result = await provider.GetExchangeRatesAsync(currencies);
+
+            // Assert
+            Assert.NotNull(result);
+            Assert.Single(result);
+            Assert.Equal("USD", result.First().TargetCurrency.Code);
+            serviceMock.Verify(s => s.GetExchangeRateAsync(It.IsAny<IEnumerable<Currency>>()), Times.Once);
+        }
+
+        [Fact]
+        public async Task GetExchangeRatesAsync_ReturnsEmpty_WhenCurrencyListIsNull()
+        {
+            // Arrange
+            var serviceMock = new Mock<IExchangeRateProviderService>();
+            var provider = new ExchangeRateProvider(serviceMock.Object);
+
+            // Act
+            var result = await provider.GetExchangeRatesAsync(null);
+
+            // Assert
+            Assert.Empty(result);
+            serviceMock.Verify(s => s.GetExchangeRateAsync(It.IsAny<IEnumerable<Currency>>()), Times.Never);
+        }
+
+        [Fact]
+        public async Task GetExchangeRatesAsync_ReturnsEmpty_WhenCurrencyListIsEmpty()
+        {
+            // Arrange
+            var serviceMock = new Mock<IExchangeRateProviderService>();
+            var provider = new ExchangeRateProvider(serviceMock.Object);
+
+            // Act
+            var result = await provider.GetExchangeRatesAsync(new List<Currency>());
+
+            // Assert
+            Assert.Empty(result);
+            serviceMock.Verify(s => s.GetExchangeRateAsync(It.IsAny<IEnumerable<Currency>>()), Times.Never);
+        }
+    }
+}
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateProviderTests/Integration/ExchangeRateProviderIntegrationTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateProviderTests/Integration/ExchangeRateProviderIntegrationTests.cs
new file mode 100644
index 000000000..3e4cda4d4
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateProviderTests/Integration/ExchangeRateProviderIntegrationTests.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Services;
+using ExchangeRateUpdaterTests.StartupTests;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using RichardSzalay.MockHttp;
+using Xunit;
+
+namespace ExchangeRateUpdater.Tests.Integration
+{
+    public class ExchangeRateProviderIntegrationTests
+    {
+        [Fact]
+        public async Task GetExchangeRatesAsync_ReturnsCombinedRates_FromAllFetchers()
+        {
+            // Arrange
+
+            var mockHttp = new MockHttpMessageHandler();
+
+            mockHttp.When("https://mock-daily")
+                .Respond("text/plain", @"
+                11 Jun 2025 #123
+                Country|Currency|Amount|Code|Rate
+                United States|Dollar|1|USD|22,123");
+
+            mockHttp.When("https://mock-other")
+                .Respond("text/plain", @"
+                11 Jun 2025 #123
+                Country|Currency|Amount|Code|Rate|Source
+                Switzerland|Franc|1|CHF|25,456|SNB");
+
+            var httpClient = mockHttp.ToHttpClient();
+
+            var inMemorySettings = new Dictionary<string, string>
+            {
+                ["CzechBankSettings:DailyRatesUrl"] = "https://mock-daily",
+                ["CzechBankSettings:OtherCurrencyRatesUrl"] = "https://mock-other",
+                ["CzechBankSettings:TimeoutSeconds"] = "10",
+                ["CzechBankSettings:RetryCount"] = "2"
+            };
+
+            var config = new ConfigurationBuilder()
+                .AddInMemoryCollection(inMemorySettings)
+                .Build();
+
+            var services = new ServiceCollection();
+            var startup = new StartupForTest(config);
+            startup.ConfigureServices(services);
+
+            services.AddSingleton(httpClient);
+
+            var provider = services.BuildServiceProvider();
+
+            var httpFactory = new TestHttpClientFactory(httpClient);
+            services.AddSingleton<IHttpClientFactory>(httpFactory);
+
+            var finalProvider = services.BuildServiceProvider();
+
+            var exchangeRateProvider = finalProvider.GetRequiredService<IExchangeRateProviderService>();
+
+            // Act
+            var currencies = new[] { new Currency("USD"), new Currency("CHF") };
+            var rates = await exchangeRateProvider.GetExchangeRateAsync(currencies);
+
+            // Assert
+            Assert.NotEmpty(rates);
+            Assert.Contains(rates, r => r.TargetCurrency.Code == "USD");
+            Assert.Contains(rates, r => r.TargetCurrency.Code == "CHF");
+        }
+
+        // IHttpClientFactory override
+        private class TestHttpClientFactory : IHttpClientFactory
+        {
+            private readonly HttpClient _client;
+
+            public TestHttpClientFactory(HttpClient client)
+            {
+                _client = client;
+            }
+
+            public HttpClient CreateClient(string name) => _client;
+        }
+    }
+}
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateUpdaterTests.csproj b/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateUpdaterTests.csproj
new file mode 100644
index 000000000..930ef3def
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/ExchangeRateUpdaterTests.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+	<PropertyGroup>
+		<TargetFramework>net6.0</TargetFramework>
+		<IsPackable>false</IsPackable>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<Nullable>enable</Nullable>
+	</PropertyGroup>
+
+	<ItemGroup>
+		<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.4" />
+		<PackageReference Include="Moq" Version="4.20.72" />
+		<PackageReference Include="xunit" Version="2.4.2" />
+		<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2" />
+		<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
+	</ItemGroup>
+
+	<ItemGroup>
+		<ProjectReference Include="..\Task\ExchangeRateUpdater.csproj" />
+	</ItemGroup>
+
+</Project>
\ No newline at end of file
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/HttpClients/ExchangeRateFetcherTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/HttpClients/ExchangeRateFetcherTests.cs
new file mode 100644
index 000000000..e692ec8a2
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/HttpClients/ExchangeRateFetcherTests.cs
@@ -0,0 +1,33 @@
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.HttpClients;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using RichardSzalay.MockHttp;
+using Xunit;
+
+namespace ExchangeRateUpdaterTests.HttpClients
+{
+    public class ExchangeRateFetcherTests
+    {
+        [Fact]
+        public async Task FetchAsync_ReturnsExpectedRawText()
+        {
+            var mockHttp = new MockHttpMessageHandler();
+            mockHttp.When("https://mock-daily")
+                    .Respond("text/plain", "Mock CNB Data");
+            var mockLogger = new Mock<ILogger<DailyExchangeRateFetcher>>();
+
+            var httpClient = mockHttp.ToHttpClient();
+
+            var fetcher = new DailyExchangeRateFetcher(
+                httpClient,
+                Options.Create(new CzechBankSettings { DailyRatesUrl = "https://mock-daily" }),
+                mockLogger.Object
+            );
+
+            var result = await fetcher.FetchAsync();
+            Assert.Equal("Mock CNB Data", result);
+        }
+    }
+}
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/Parsers/CzechNationalBankTextRateParserTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/Parsers/CzechNationalBankTextRateParserTests.cs
new file mode 100644
index 000000000..5474d605b
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/Parsers/CzechNationalBankTextRateParserTests.cs
@@ -0,0 +1,104 @@
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Parsers;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace ExchangeRateUpdater.Tests.Parsers
+{
+    public class CzechNationalBankTextRateParserTests
+    {
+        private readonly CzechNationalBankTextRateParser _parser;
+
+        public CzechNationalBankTextRateParserTests()
+        {
+            var mockLogger = new Mock<ILogger<CzechNationalBankTextRateParser>>();
+            _parser = new CzechNationalBankTextRateParser(5, mockLogger.Object);
+        }
+
+        [Fact]
+        public void Parse_ParsesValidLine_Correctly()
+        {
+            var raw = @"
+11 Jun 2025 #123
+Country|Currency|Amount|Code|Rate
+United States|Dollar|1|USD|22,123
+";
+
+            var filter = new List<Currency> { new Currency("USD") };
+            var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();
+
+            Assert.Single(result);
+            var rate = result[0];
+            Assert.Equal("CZK", rate.SourceCurrency.Code);
+            Assert.Equal("USD", rate.TargetCurrency.Code);
+            Assert.Equal(22.123m, rate.Value);
+        }
+
+        [Fact]
+        public void Parse_SkipsLines_WithTooFewColumns()
+        {
+            var raw = @"
+11 Jun 2025 #123
+Country|Currency|Amount|Code|Rate
+Broken|Line|Only|3
+United States|Dollar|1|USD|22,123
+";
+
+            var filter = new List<Currency> { new Currency("USD") };
+            var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();
+
+            Assert.Single(result);
+            Assert.Equal("USD", result[0].TargetCurrency.Code);
+        }
+
+        [Fact]
+        public void Parse_SkipsCurrencies_NotInFilter()
+        {
+            var raw = @"
+11 Jun 2025 #123
+Country|Currency|Amount|Code|Rate
+United States|Dollar|1|USD|22,123
+Eurozone|Euro|1|EUR|25,456
+";
+
+            var filter = new List<Currency> { new Currency("EUR") };
+            var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();
+
+            Assert.Single(result);
+            Assert.Equal("EUR", result[0].TargetCurrency.Code);
+        }
+
+        [Fact]
+        public void Parse_NormalizesRate_BasedOnAmount()
+        {
+            var raw = @"
+11 Jun 2025 #123
+Country|Currency|Amount|Code|Rate
+Japan|Yen|100|JPY|50,000
+";
+
+            var filter = new List<Currency> { new Currency("JPY") };
+            var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();
+
+            Assert.Single(result);
+            Assert.Equal("JPY", result[0].TargetCurrency.Code);
+            Assert.Equal(0.500, (double)result[0].Value); // 50,000 / 100
+        }
+
+        [Fact]
+        public void Parse_ReturnsEmpty_WhenNoLinesMatch()
+        {
+            var raw = @"
+11 Jun 2025 #123
+Country|Currency|Amount|Code|Rate
+United States|Dollar|1|USD|22,123
+";
+
+            var filter = new List<Currency> { new Currency("EUR") }; // no EUR present
+            var result = _parser.Parse(raw, filter, new Currency("CZK")).ToList();
+
+            Assert.Empty(result);
+        }
+    }
+}
\ No newline at end of file
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/ProviderServiceTests/ExchangeRateProviderServiceTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/ProviderServiceTests/ExchangeRateProviderServiceTests.cs
new file mode 100644
index 000000000..e67774b82
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/ProviderServiceTests/ExchangeRateProviderServiceTests.cs
@@ -0,0 +1,123 @@
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.HttpClients;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Parsers;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using RichardSzalay.MockHttp;
+using Xunit;
+
+namespace ExchangeRateUpdater.Tests
+{
+    public class ExchangeRateProviderServiceTests
+    {
+        [Fact]
+        public async Task GetExchangeRateAsync_ReturnsRates_FromMultipleFetchers()
+        {
+            // Arrange
+            var provider = CreateProviderWithError(dailyFails: false, otherFails: false);
+
+            var currencies = new[]
+            {
+                new Currency("USD"),
+                new Currency("CHF")
+            };
+
+            // Act
+            var rates = await provider.GetExchangeRateAsync(currencies);
+
+            // Assert
+            Assert.NotEmpty(rates);
+            Assert.Contains(rates, r => r.TargetCurrency.Code == "USD");
+            Assert.Contains(rates, r => r.TargetCurrency.Code == "CHF");
+        }
+
+        [Fact]
+        public async Task GetExchangeRateAsync_FallbackToCache_WhenFetchFails()
+        {
+            // Arrange
+            var memoryCache = new MemoryCache(new MemoryCacheOptions());
+            var provider = CreateProviderWithError(dailyFails: false, otherFails: false, memoryCache);
+
+            var currencies = new[] { new Currency("USD") };
+
+            // Prime the cache
+            var initial = await provider.GetExchangeRateAsync(currencies);
+            Assert.NotEmpty(initial);
+
+            // Now simulate both fetchers failing
+            var providerWithFailure = CreateProviderWithError(
+                dailyFails: true,
+                otherFails: true,
+                memoryCache);
+
+            // Act
+            var fallbackResult = await providerWithFailure.GetExchangeRateAsync(currencies);
+
+            // Assert
+            Assert.NotEmpty(fallbackResult);
+            Assert.Equal(initial.Count, fallbackResult.Count); // cache fallback works
+        }
+
+        [Fact]
+        public async Task GetExchangeRateAsync_Throws_WhenNoCacheAndAllFetchersFail()
+        {
+            // Arrange
+            var provider = CreateProviderWithError(dailyFails: true, otherFails: true);
+
+            var requested = new[] { new Currency("USD") };
+
+            // Act & Assert
+            await Assert.ThrowsAsync<ApplicationException>(async() =>
+            {
+                await provider.GetExchangeRateAsync(requested);
+            });
+        }
+
+        private ExchangeRateProviderService CreateProviderWithError(bool dailyFails, bool otherFails, IMemoryCache memoryCache = null)
+        {
+            var mockHttp = new MockHttpMessageHandler();
+
+            if (dailyFails)
+                mockHttp.When("https://mock-daily").Respond(System.Net.HttpStatusCode.InternalServerError);
+            else
+                mockHttp.When("https://mock-daily").Respond("text/plain", "Country|Currency|Amount|Code|Rate\nDate\nUnited States|dollar|1|USD|22,345\n");
+
+            if (otherFails)
+                mockHttp.When("https://mock-other").Respond(System.Net.HttpStatusCode.InternalServerError);
+            else
+                mockHttp.When("https://mock-other").Respond("text/plain", "Country|Currency|Amount|Code|Rate|Source\nDate\nSwitzerland|franc|1|CHF|25,123|SNB\n");
+
+            var httpClient = mockHttp.ToHttpClient();
+
+            var mockDailyLogger = new Mock<ILogger<DailyExchangeRateFetcher>>();
+            var mockOtherLogger = new Mock<ILogger<OtherCurrencyExchangeRateFetcher>>();
+
+            var dailyFetcher = new DailyExchangeRateFetcher(
+                httpClient,
+                Options.Create(new CzechBankSettings { DailyRatesUrl = "https://mock-daily" }), mockDailyLogger.Object);
+
+            var otherFetcher = new OtherCurrencyExchangeRateFetcher(
+                httpClient,
+                Options.Create(new CzechBankSettings { OtherCurrencyRatesUrl = "https://mock-other" }), mockOtherLogger.Object);
+
+            var fetchers = new List<IExhangeRateFetcher> { dailyFetcher, otherFetcher };
+
+            var loggerFactory = LoggerFactory.Create(builder => builder.AddDebug());
+            var parserLogger = loggerFactory.CreateLogger<CzechNationalBankTextRateParser>();
+            var parser = new CzechNationalBankTextRateParser(5, parserLogger);
+
+            var providerLogger = loggerFactory.CreateLogger<ExchangeRateProviderService>();
+
+            return new ExchangeRateProviderService(
+                fetchers,
+                parser,
+                memoryCache ?? new MemoryCache(new MemoryCacheOptions()),
+                providerLogger
+            );
+        }
+    }
+}
\ No newline at end of file
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/StartupTests/StartupForTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/StartupTests/StartupForTests.cs
new file mode 100644
index 000000000..35760c318
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/StartupTests/StartupForTests.cs
@@ -0,0 +1,72 @@
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.HttpClients;
+using ExchangeRateUpdater.Parsers;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Polly;
+
+namespace ExchangeRateUpdaterTests.StartupTests
+{
+    public class StartupForTest : Startup
+    {
+        public new IConfiguration Configuration { get; }
+
+        public StartupForTest(IConfiguration configuration)
+        {
+            Configuration = configuration;
+        }
+
+        public new void ConfigureServices(IServiceCollection services)
+        {
+            services.Configure<CzechBankSettings>(Configuration.GetSection("CzechBankSettings"));
+
+            services.AddMemoryCache();
+            services.AddSingleton<IExchangeRateParser>(sp =>
+                new CzechNationalBankTextRateParser(5, sp.GetRequiredService<ILogger<CzechNationalBankTextRateParser>>()));
+
+            services.AddHttpClient<DailyExchangeRateFetcher>()
+                .ConfigureHttpClient((provider, client) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+                    client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
+                })
+                .AddPolicyHandler((provider, request) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+                    return Polly.Extensions.Http.HttpPolicyExtensions
+                        .HandleTransientHttpError()
+                        .WaitAndRetryAsync(settings.RetryCount,
+                            retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
+                });
+
+            services.AddHttpClient<OtherCurrencyExchangeRateFetcher>()
+                .ConfigureHttpClient((provider, client) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+                    client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
+                })
+                .AddPolicyHandler((provider, request) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+                    return Polly.Extensions.Http.HttpPolicyExtensions
+                        .HandleTransientHttpError()
+                        .WaitAndRetryAsync(settings.RetryCount,
+                            retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
+                });
+
+            services.AddSingleton<IExhangeRateFetcher, DailyExchangeRateFetcher>();
+            services.AddSingleton<IExhangeRateFetcher, OtherCurrencyExchangeRateFetcher>();
+            services.AddSingleton<IExchangeRateProviderService, ExchangeRateProviderService>();
+
+            services.AddLogging(logging =>
+            {
+                logging.ClearProviders();
+                logging.AddConfiguration(Configuration.GetSection("Logging"));
+                logging.AddConsole();
+            });
+        }
+    }
+}
diff --git a/jobs/Backend/ExchangeRateUpdaterTests/StartupTests/StartupTests.cs b/jobs/Backend/ExchangeRateUpdaterTests/StartupTests/StartupTests.cs
new file mode 100644
index 000000000..93af46cfa
--- /dev/null
+++ b/jobs/Backend/ExchangeRateUpdaterTests/StartupTests/StartupTests.cs
@@ -0,0 +1,81 @@
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.HttpClients;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Parsers;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace ExchangeRateUpdaterTests.StartupTests
+{
+    public class StartupTests
+    {
+        [Fact]
+        public void ConfigureServices_RegistersDependenciesCorrectly()
+        {
+            var startup = new Startup();
+            var services = new ServiceCollection();
+            startup.ConfigureServices(services);
+
+            var provider = services.BuildServiceProvider();
+
+            var providerService = provider.GetRequiredService<IExchangeRateProviderService>();
+            Assert.NotNull(providerService);
+
+            var cache = provider.GetRequiredService<IMemoryCache>();
+            Assert.NotNull(cache);
+
+            var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>();
+            Assert.NotNull(settings);
+            Assert.False(string.IsNullOrWhiteSpace(settings.Value.DailyRatesUrl));
+            Assert.False(string.IsNullOrWhiteSpace(settings.Value.OtherCurrencyRatesUrl));
+            Assert.True(settings.Value.TimeoutSeconds > 0);
+            Assert.True(settings.Value.RetryCount >= 0);
+
+            var parser = provider.GetRequiredService<IExchangeRateParser>();
+            Assert.NotNull(parser);
+
+            var fetchers = provider.GetServices<IExhangeRateFetcher>().ToList();
+            Assert.NotEmpty(fetchers);
+
+            bool hasDaily = fetchers.Any(f => f is DailyExchangeRateFetcher);
+            bool hasOther = fetchers.Any(f => f is OtherCurrencyExchangeRateFetcher);
+
+            Assert.True(hasDaily);
+            Assert.True(hasOther);
+
+            var httpFactory = provider.GetRequiredService<IHttpClientFactory>();
+            Assert.NotNull(httpFactory);
+
+            var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
+            Assert.NotNull(loggerFactory);
+
+            var logger = loggerFactory.CreateLogger<StartupTests>();
+            Assert.NotNull(logger);
+        }
+
+        [Fact]
+        public async Task ConfigureServices_Throws_WhenUrlsAreMissing()
+        {
+            var config = new ConfigurationBuilder()
+                .AddInMemoryCollection(new Dictionary<string, string?>())
+                .Build();
+
+            var services = new ServiceCollection();
+
+            var startup = new StartupForTest(config);
+            startup.ConfigureServices(services);
+
+            var provider = services.BuildServiceProvider();
+            // Act + Assert: provider resolution should throw due to missing URLs
+            Assert.Throws<ArgumentNullException>(() =>
+            {
+                var _ = provider.GetRequiredService<IExchangeRateProviderService>();
+            });
+        }
+    }
+}
diff --git a/jobs/Backend/Task/.gitignore b/jobs/Backend/Task/.gitignore
new file mode 100644
index 000000000..dc661aeb1
--- /dev/null
+++ b/jobs/Backend/Task/.gitignore
@@ -0,0 +1,2 @@
+/.vs
+publish/
\ No newline at end of file
diff --git a/jobs/Backend/Task/CHANGELOG.md b/jobs/Backend/Task/CHANGELOG.md
new file mode 100644
index 000000000..fe76f0855
--- /dev/null
+++ b/jobs/Backend/Task/CHANGELOG.md
@@ -0,0 +1,47 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com),
+and this project adheres to [Semantic Versioning](https://semver.org).
+
+## [Unreleased]
+
+### Added
+- 2 HttpClients which fetch the Czech National Bank data sources
+- Parser for parsing data from the http call
+- Integration Test testing entire flow with mocked Http Responses
+- The `CHANGELOG` file itself
+- `Deployment.md` and `Dockerfile` for deployment steps
+- Detailed `README` file
+
+### Fixed
+- Single Responsibility Principle was being violated by the `ExchangeRateProviderService`
+- Moved the `ExchangeRateProvider` to Services folder keeping root clean
+- Unit tests which were failing from the new `ExchangeRateProvider` implementation
+- csproj organisation and using .net 9 packages on a project with target of .net 6
+- `appsettings` to folow the removal of `HttpServiceSettings`
+- Use Dependency Injection on `Program.cs`
+- Spelling mistake in test file name
+- Git ignore the publish folder
+
+### Removed
+- `HttpServiceSettings`; moved into the `CzechBankSettings`
+
+---
+
+## [1.0.0] - 2025-05-22
+
+### Added
+- Initial implementation of `ExchangeRateProvider`
+- Fetch rates from CNB daily and other endpoints
+- Dependency injection setup in console app
+- Logging and configuration via `appsettings.json`
+- HTTP timeout and retry support via Polly, configurable from appsettings
+- Unit tests for `ExchangeRateProvider`
+- Structured logging using Microsoft.Extensions.Logging
+- Cache fallback logic
+
+### Fixed
+- PlatformNotSupportedException from EventLog on non-Windows systems
+- ArgumentOutOfRangeException for timeout value
diff --git a/jobs/Backend/Task/Configuration/CzechBankSettings.cs b/jobs/Backend/Task/Configuration/CzechBankSettings.cs
new file mode 100644
index 000000000..fec7ae7d2
--- /dev/null
+++ b/jobs/Backend/Task/Configuration/CzechBankSettings.cs
@@ -0,0 +1,10 @@
+namespace ExchangeRateUpdater.Configuration
+{
+    public class CzechBankSettings
+    {
+        public string DailyRatesUrl { get; set; }
+        public string OtherCurrencyRatesUrl { get; set; }
+        public int TimeoutSeconds { get; set; } = 10;
+        public int RetryCount { get; set; } = 3;
+    }
+}
diff --git a/jobs/Backend/Task/Configuration/Startup.cs b/jobs/Backend/Task/Configuration/Startup.cs
new file mode 100644
index 000000000..d802d2237
--- /dev/null
+++ b/jobs/Backend/Task/Configuration/Startup.cs
@@ -0,0 +1,95 @@
+using System;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Polly.Extensions.Http;
+using Polly;
+using ExchangeRateUpdater.Parsers;
+using ExchangeRateUpdater.HttpClients;
+
+namespace ExchangeRateUpdater.Configuration
+{
+    public class Startup
+    {
+        public IConfiguration Configuration { get; }
+
+        public Startup()
+        {
+            var builder = new ConfigurationBuilder()
+                .AddJsonFile("appsettings.json", optional: false);
+
+            Configuration = builder.Build();
+        }
+
+        public void ConfigureServices(IServiceCollection services)
+        {
+            // Bind configurations
+            services.Configure<CzechBankSettings>(Configuration.GetSection("CzechBankSettings"));
+            services.AddSingleton<ExchangeRateProvider>();
+
+
+            // Add dependencies
+            services.AddMemoryCache();
+
+            // Register the parser
+            services.AddSingleton<IExchangeRateParser>(sp => new CzechNationalBankTextRateParser(5,
+            sp.GetRequiredService<ILogger<CzechNationalBankTextRateParser>>()));
+            
+            // === Add resilient HTTP clients with Polly ===
+            services.AddHttpClient<DailyExchangeRateFetcher>()
+                .ConfigureHttpClient((provider, client) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+                    client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
+                })
+                .AddPolicyHandler((provider, request) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+
+                    return HttpPolicyExtensions
+                        .HandleTransientHttpError()
+                        .WaitAndRetryAsync(
+                            retryCount: settings.RetryCount,
+                            sleepDurationProvider: retryAttempt =>
+                                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
+                        );
+                });
+
+            services.AddHttpClient<OtherCurrencyExchangeRateFetcher>()
+                .ConfigureHttpClient((provider, client) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+                    client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
+                })
+                .AddPolicyHandler((provider, request) =>
+                {
+                    var settings = provider.GetRequiredService<IOptions<CzechBankSettings>>().Value;
+
+                    return HttpPolicyExtensions
+                        .HandleTransientHttpError()
+                        .WaitAndRetryAsync(
+                            retryCount: settings.RetryCount,
+                            sleepDurationProvider: retryAttempt =>
+                                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
+                        );
+                });
+
+            // Register fetchers
+            services.AddSingleton<IExhangeRateFetcher, DailyExchangeRateFetcher>();
+            services.AddSingleton<IExhangeRateFetcher, OtherCurrencyExchangeRateFetcher>();
+
+            // Register main provider
+            services.AddSingleton<IExchangeRateProviderService, ExchangeRateProviderService>();
+
+            // Add logging
+            services.AddLogging(logging =>
+            {
+                logging.ClearProviders();
+                logging.AddConfiguration(Configuration.GetSection("Logging"));
+                logging.AddConsole();
+            });
+        }
+    }
+}
diff --git a/jobs/Backend/Task/Dockerfile b/jobs/Backend/Task/Dockerfile
new file mode 100644
index 000000000..5acbc69b7
--- /dev/null
+++ b/jobs/Backend/Task/Dockerfile
@@ -0,0 +1,8 @@
+# Use official .NET runtime image
+FROM mcr.microsoft.com/dotnet/runtime:6.0
+
+WORKDIR /app
+
+COPY ./publish/ .
+
+ENTRYPOINT ["dotnet", "ExchangeRateUpdater.dll"]
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
-    {
-        /// <summary>
-        /// 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.
-        /// </summary>
-        public IEnumerable<ExchangeRate> GetExchangeRates(IEnumerable<Currency> currencies)
-        {
-            return Enumerable.Empty<ExchangeRate>();
-        }
-    }
-}
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj
index 2fc654a12..b2359b985 100644
--- a/jobs/Backend/Task/ExchangeRateUpdater.csproj
+++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj
@@ -1,8 +1,29 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <OutputType>Exe</OutputType>
-    <TargetFramework>net6.0</TargetFramework>
-  </PropertyGroup>
+	<PropertyGroup>
+		<OutputType>Exe</OutputType>
+		<TargetFramework>net6.0</TargetFramework>
+		<PublishSingleFile>true</PublishSingleFile>
+		<SelfContained>true</SelfContained>
+		<RuntimeIdentifier>win-x64</RuntimeIdentifier> <!-- or linux-x64 for Linux servers -->
+	</PropertyGroup>
+
+	<ItemGroup>
+		<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.3" />
+		<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
+		<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.1" />
+		<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.36" />
+		<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.1" />
+		<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.1" />
+	</ItemGroup>
+
+	<ItemGroup>
+		<None Update="appsettings.json">
+			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+		</None>
+		<None Update="appsettings.development.json">
+			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+		</None>
+	</ItemGroup>
 
 </Project>
\ No newline at end of file
diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln
index 89be84daf..03c459cd2 100644
--- a/jobs/Backend/Task/ExchangeRateUpdater.sln
+++ b/jobs/Backend/Task/ExchangeRateUpdater.sln
@@ -1,10 +1,19 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35707.178
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD84211C-7857-4F86-AF9D-8EF05EE3C34B}"
+	ProjectSection(SolutionItems) = preProject
+		.gitignore = .gitignore
+		CHANGELOG.md = CHANGELOG.md
+		README.md = README.md
+	EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdaterTests", "..\ExchangeRateUpdaterTests\ExchangeRateUpdaterTests.csproj", "{3F178CA6-0C53-4407-9285-6862F4A26158}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -15,6 +24,10 @@ 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
+		{3F178CA6-0C53-4407-9285-6862F4A26158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3F178CA6-0C53-4407-9285-6862F4A26158}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3F178CA6-0C53-4407-9285-6862F4A26158}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3F178CA6-0C53-4407-9285-6862F4A26158}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/jobs/Backend/Task/Extensions/CurrencyExtensions.cs b/jobs/Backend/Task/Extensions/CurrencyExtensions.cs
new file mode 100644
index 000000000..646a287ac
--- /dev/null
+++ b/jobs/Backend/Task/Extensions/CurrencyExtensions.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ExchangeRateUpdater.Models;
+
+namespace ExchangeRateUpdater.Extensions
+{
+    public static class CurrencyExtensions
+    {
+        /// <summary>
+        /// Checks if the list of Currency contains a currency with the given code.
+        /// </summary>
+        public static bool Contains(this IEnumerable<Currency> currencies, string currencyCode)
+        {
+            if (currencyCode is null)
+                throw new ArgumentNullException(nameof(currencyCode));
+
+            return currencies.Any(c =>
+                string.Equals(c.Code, currencyCode, StringComparison.OrdinalIgnoreCase));
+        }
+    }
+}
diff --git a/jobs/Backend/Task/HttpClients/DailyExchangeRateFetcher.cs b/jobs/Backend/Task/HttpClients/DailyExchangeRateFetcher.cs
new file mode 100644
index 000000000..0c81f7016
--- /dev/null
+++ b/jobs/Backend/Task/HttpClients/DailyExchangeRateFetcher.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace ExchangeRateUpdater.HttpClients
+{
+    public class DailyExchangeRateFetcher : IExhangeRateFetcher
+    {
+        private readonly HttpClient _httpClient;
+        private readonly string _url;
+        private readonly ILogger<DailyExchangeRateFetcher> _logger;
+
+        public DailyExchangeRateFetcher(
+            HttpClient httpClient,
+            IOptions<CzechBankSettings> options,
+            ILogger<DailyExchangeRateFetcher> logger)
+        {
+            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+            _url = options.Value.DailyRatesUrl ?? throw new ArgumentNullException(nameof(options.Value.DailyRatesUrl));
+            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        }
+
+        public async Task<string> FetchAsync()
+        {
+            _logger.LogInformation("Fetching daily exchange rates from {Url}", _url);
+
+            try
+            {
+                var start = DateTimeOffset.UtcNow;
+
+                var response = await _httpClient.GetAsync(_url);
+                response.EnsureSuccessStatusCode();
+
+                var content = await response.Content.ReadAsStringAsync();
+
+                var duration = DateTimeOffset.UtcNow - start;
+                _logger.LogInformation("Fetched daily exchange rates in {Duration} ms", duration.TotalMilliseconds);
+
+                return content;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error fetching daily exchange rates from {Url}", _url);
+                throw;
+            }
+        }
+    }
+}
diff --git a/jobs/Backend/Task/HttpClients/IExhangeRateFetcher.cs b/jobs/Backend/Task/HttpClients/IExhangeRateFetcher.cs
new file mode 100644
index 000000000..8a64c18c5
--- /dev/null
+++ b/jobs/Backend/Task/HttpClients/IExhangeRateFetcher.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ExchangeRateUpdater.HttpClients
+{
+    public interface IExhangeRateFetcher
+    {
+        Task<string> FetchAsync();
+    }
+}
diff --git a/jobs/Backend/Task/HttpClients/OtherCurrencyExchangeRateFetcher.cs b/jobs/Backend/Task/HttpClients/OtherCurrencyExchangeRateFetcher.cs
new file mode 100644
index 000000000..c2b45760b
--- /dev/null
+++ b/jobs/Backend/Task/HttpClients/OtherCurrencyExchangeRateFetcher.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace ExchangeRateUpdater.HttpClients
+{
+    public class OtherCurrencyExchangeRateFetcher : IExhangeRateFetcher
+    {
+        private readonly HttpClient _httpClient;
+        private readonly string _url;
+        private readonly ILogger<OtherCurrencyExchangeRateFetcher> _logger;
+
+        public OtherCurrencyExchangeRateFetcher(
+            HttpClient httpClient,
+            IOptions<CzechBankSettings> options,
+            ILogger<OtherCurrencyExchangeRateFetcher> logger)
+        {
+            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+            _url = options.Value.OtherCurrencyRatesUrl ?? throw new ArgumentNullException(nameof(options.Value.OtherCurrencyRatesUrl));
+            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        }
+
+        public async Task<string> FetchAsync()
+        {
+            _logger.LogInformation("Fetching other currency exchange rates from {Url}", _url);
+
+            try
+            {
+                var start = DateTimeOffset.UtcNow;
+
+                var response = await _httpClient.GetAsync(_url);
+                response.EnsureSuccessStatusCode();
+
+                var content = await response.Content.ReadAsStringAsync();
+
+                var duration = DateTimeOffset.UtcNow - start;
+                _logger.LogInformation("Fetched other currency exchange rates in {Duration} ms", duration.TotalMilliseconds);
+
+                return content;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error fetching other currency exchange rates from {Url}", _url);
+                throw;
+            }
+        }
+    }
+}
diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Models/Currency.cs
similarity index 89%
rename from jobs/Backend/Task/Currency.cs
rename to jobs/Backend/Task/Models/Currency.cs
index f375776f2..8336d740e 100644
--- a/jobs/Backend/Task/Currency.cs
+++ b/jobs/Backend/Task/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/Models/ExchangeRate.cs
similarity index 93%
rename from jobs/Backend/Task/ExchangeRate.cs
rename to jobs/Backend/Task/Models/ExchangeRate.cs
index 58c5bb10e..2133586d4 100644
--- a/jobs/Backend/Task/ExchangeRate.cs
+++ b/jobs/Backend/Task/Models/ExchangeRate.cs
@@ -1,4 +1,4 @@
-namespace ExchangeRateUpdater
+namespace ExchangeRateUpdater.Models
 {
     public class ExchangeRate
     {
diff --git a/jobs/Backend/Task/Parsers/CzechNationalBankTextRateParser.cs b/jobs/Backend/Task/Parsers/CzechNationalBankTextRateParser.cs
new file mode 100644
index 000000000..94c6729cb
--- /dev/null
+++ b/jobs/Backend/Task/Parsers/CzechNationalBankTextRateParser.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Extensions;
+using ExchangeRateUpdater.Models;
+using Microsoft.Extensions.Logging;
+
+namespace ExchangeRateUpdater.Parsers
+{
+    public class CzechNationalBankTextRateParser : IExchangeRateParser
+    {
+        private readonly int _expectedColumns;
+        private readonly ILogger<CzechNationalBankTextRateParser> _logger;
+
+        public CzechNationalBankTextRateParser(int expectedColumns, ILogger<CzechNationalBankTextRateParser> logger)
+        {
+            _expectedColumns = expectedColumns;
+            _logger = logger;
+        }
+
+        public IEnumerable<ExchangeRate> Parse(string raw, IEnumerable<Currency> filter, Currency sourceCurrency)
+        {
+            var parsedRates = new List<ExchangeRate>();
+            var filterSet = new HashSet<string>(
+                filter.Select(c => c.Code),
+                StringComparer.OrdinalIgnoreCase);
+
+            var lines = raw.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+            _logger.LogDebug("Parsing CNB text: total lines (including header): {LineCount}", lines.Length);
+
+            for (int i = 2; i < lines.Length; i++) // skip header + date line
+            {
+                var parts = lines[i].Split('|');
+
+                if (parts.Length < _expectedColumns)
+                {
+                    _logger.LogWarning("Skipping line {LineIndex}: expected {ExpectedColumns} columns but got {ActualColumns}",
+                        i, _expectedColumns, parts.Length);
+                    continue;
+                }
+
+                string code = parts[3].Trim();
+                if (!filterSet.Contains(code))
+                {
+                    _logger.LogDebug("Skipping currency {CurrencyCode} as it's not in the filter list.", code);
+                    continue;
+                }
+
+                try
+                {
+                    int amount = int.Parse(parts[2].Trim());
+                    decimal rate = decimal.Parse(
+                        parts[4].Trim().Replace(',', '.'),
+                        CultureInfo.InvariantCulture);
+
+                    decimal normalized = rate / amount;
+
+                    var exchangeRate = new ExchangeRate(sourceCurrency, new Currency(code), normalized);
+                    parsedRates.Add(exchangeRate);
+
+                    _logger.LogDebug("Parsed: {Source}/{Target} = {Value}",
+                        sourceCurrency, code, normalized);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Failed to parse line {LineIndex}: {LineContent}", i, lines[i]);
+                }
+            }
+
+            _logger.LogInformation("Finished parsing. Extracted {ParsedCount} exchange rates.", parsedRates.Count);
+            return parsedRates;
+        }
+    }
+}
diff --git a/jobs/Backend/Task/Parsers/IExchangeRateParser.cs b/jobs/Backend/Task/Parsers/IExchangeRateParser.cs
new file mode 100644
index 000000000..a8194d451
--- /dev/null
+++ b/jobs/Backend/Task/Parsers/IExchangeRateParser.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using ExchangeRateUpdater.Models;
+
+namespace ExchangeRateUpdater.Parsers
+{
+    public interface IExchangeRateParser
+    {
+        IEnumerable<ExchangeRate> Parse(string content, IEnumerable<Currency> filter, Currency source);
+    }
+}
diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs
index 379a69b1f..1e2b2c327 100644
--- a/jobs/Backend/Task/Program.cs
+++ b/jobs/Backend/Task/Program.cs
@@ -1,6 +1,12 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using ExchangeRateUpdater.Configuration;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
 
 namespace ExchangeRateUpdater
 {
@@ -21,10 +27,20 @@ public static class Program
 
         public static void Main(string[] args)
         {
+            var startup = new Startup();
+
+            using IHost host = Host.CreateDefaultBuilder(args)
+                .ConfigureServices(startup.ConfigureServices)
+                .Build();
+
+            // Get services
+            var logger = host.Services.GetRequiredService<ILoggerFactory>()
+                             .CreateLogger("Main");
+
             try
             {
-                var provider = new ExchangeRateProvider();
-                var rates = provider.GetExchangeRates(currencies);
+                var provider = host.Services.GetRequiredService<ExchangeRateProvider>();
+                var rates = provider.GetExchangeRatesAsync(currencies).Result;
 
                 Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:");
                 foreach (var rate in rates)
diff --git a/jobs/Backend/Task/README.md b/jobs/Backend/Task/README.md
new file mode 100644
index 000000000..02d7e394f
--- /dev/null
+++ b/jobs/Backend/Task/README.md
@@ -0,0 +1,72 @@
+# CzechNationalBank ExchangeRateUpdater
+
+A robust .NET 6 console application that pulls daily and other currency exchange rates from the Czech National Bank, parses and caches them, and exposes a reusable provider for integration into other systems.
+
+---
+
+## Features
+
+- ✅ Fetches official CNB daily and other currency exchange rates
+- ✅ Handles parsing of CNB's custom text formats
+- ✅ Uses resilient HTTP client with Polly retry and timeout policies
+- ✅ Caches rates until the next official update (2:30 PM daily)
+- ✅ Fully unit-tested and integration-tested
+- ✅ Runs standalone or inside a Docker container
+
+---
+
+## Getting Started
+
+### Requirements
+
+- [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)
+- [Docker Desktop](https://www.docker.com/products/docker-desktop) (optional)
+
+---
+
+### Build & Run Locally
+
+`bash`
+# Build & run
+dotnet build
+dotnet run --project ExchangeRateUpdater
+
+---
+
+## Run with Docker
+
+### Publish build
+dotnet publish -c Release -o ./publish
+
+### Build Docker image
+docker build -t exchangerate-updater .
+
+### Run in Docker
+docker run --rm exchangerate-updater
+
+--- 
+
+# Configuration
+The app reads from `appsettings.json`
+
+Override these settings at runtime with env variables eg:
+
+`docker run --rm -e CzechBankSettings__DailyRatesUrl=https://mock exchangerate-updater`
+
+---
+
+## Run Tests
+
+`dotnet test ExchangeRateUpdaterTests`
+
+---
+
+## Deployment
+
+See [deployment.md](./deployment.md) for detailed instructions
+
+---
+
+## Changelog
+
+[Changelog](./CHANGELOG.md)
\ 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 000000000..60d404c21
--- /dev/null
+++ b/jobs/Backend/Task/Services/ExchangeRateProvider.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Models;
+
+namespace ExchangeRateUpdater.Services
+{
+    public class ExchangeRateProvider
+    {
+        private readonly IExchangeRateProviderService _service;
+
+        public ExchangeRateProvider(IExchangeRateProviderService service)
+        {
+            _service = service ?? throw new ArgumentNullException(nameof(service));
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public async Task<IEnumerable<ExchangeRate>> GetExchangeRatesAsync(IEnumerable<Currency> currencies)
+        {
+            if (currencies == null || !currencies.Any())
+            {
+                return Enumerable.Empty<ExchangeRate>();
+            }
+            return await _service.GetExchangeRateAsync(currencies);
+        }
+    }
+}
diff --git a/jobs/Backend/Task/Services/ExchangeRateProviderService.cs b/jobs/Backend/Task/Services/ExchangeRateProviderService.cs
new file mode 100644
index 000000000..94d7a2bfd
--- /dev/null
+++ b/jobs/Backend/Task/Services/ExchangeRateProviderService.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+using ExchangeRateUpdater.HttpClients;
+using ExchangeRateUpdater.Models;
+using ExchangeRateUpdater.Parsers;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+
+namespace ExchangeRateUpdater.Services
+{
+    public class ExchangeRateProviderService : IExchangeRateProviderService
+    {
+        private readonly IEnumerable<IExhangeRateFetcher> _fetchers;
+        private readonly IExchangeRateParser _parser;
+        private readonly ILogger<ExchangeRateProviderService> _logger;
+        private readonly IMemoryCache _cache;
+        private const string CacheKey = nameof(CacheKey);
+        private const string SourceCurrency = "CZK";
+
+        public ExchangeRateProviderService(
+        IEnumerable<IExhangeRateFetcher> fetchers,
+        IExchangeRateParser parser,
+        IMemoryCache cache,
+        ILogger<ExchangeRateProviderService> logger)
+        {
+            _fetchers = fetchers;
+            _parser = parser;
+            _cache = cache;
+            _logger = logger;
+        }
+
+        public async Task<List<ExchangeRate>> GetExchangeRateAsync(IEnumerable<Currency> currencies)
+        {
+            var targetCodes = new HashSet<string>(
+                currencies.Select(c => c.Code),
+                StringComparer.OrdinalIgnoreCase
+            );
+
+            
+            if (_cache.TryGetValue(CacheKey, out List<ExchangeRate> cachedRates))
+            {
+                _logger.LogInformation("Returning filtered exchange rates from cache.");
+                return cachedRates
+                    .Where(rate => targetCodes.Contains(rate.TargetCurrency.Code))
+                    .ToList();
+            }
+
+            // Parallel fetch
+            var fetchTasks = _fetchers.Select(s => s.FetchAsync()).ToList();
+
+            string[] content;
+            try
+            {
+                content = await Task.WhenAll(fetchTasks);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "One or more sources failed during fetch.");
+                // If we have fallback cache, return that:
+                if (_cache.TryGetValue(CacheKey, out List<ExchangeRate> fallback))
+                {
+                    _logger.LogWarning("Returning cached rates due to fetch failure.");
+                    return fallback.Where(rate => targetCodes.Contains(rate.TargetCurrency.Code)).ToList();
+                }
+
+                throw new ApplicationException("Failed to fetch exchange rates and no cached fallback is available.", ex);
+            }
+
+            var exchangeRates = new List<ExchangeRate>();
+            foreach (var raw in content)
+            {
+                exchangeRates.AddRange(_parser.Parse(raw, currencies, new Currency(SourceCurrency)));
+            }
+
+
+
+            var now = DateTimeOffset.UtcNow;
+            var nextUpdate = new DateTimeOffset(now.Date.AddHours(13.5)); // 2:30 PM CET Daily update according to their website
+            if (now >= nextUpdate) nextUpdate = nextUpdate.AddDays(1);
+
+            _cache.Set(CacheKey, exchangeRates, nextUpdate);
+
+            _logger.LogInformation("Successfully fetched exchange rates until {NextUpdate}.", nextUpdate);
+            return exchangeRates
+               .Where(rate => targetCodes.Contains(rate.TargetCurrency.Code))
+               .ToList();
+        }
+    }
+}
diff --git a/jobs/Backend/Task/Services/IExchangeRateProviderService.cs b/jobs/Backend/Task/Services/IExchangeRateProviderService.cs
new file mode 100644
index 000000000..09a9302d5
--- /dev/null
+++ b/jobs/Backend/Task/Services/IExchangeRateProviderService.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using ExchangeRateUpdater.Models;
+
+ namespace ExchangeRateUpdater.Services;
+
+public interface IExchangeRateProviderService
+{
+    Task<List<ExchangeRate>> GetExchangeRateAsync(IEnumerable<Currency> currencies);
+}
\ 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 000000000..4c8786c1a
--- /dev/null
+++ b/jobs/Backend/Task/appsettings.development.json
@@ -0,0 +1,13 @@
+{
+    "Logging": {
+        "LogLevel": {
+            "Default": "Debug"
+        }
+    },
+    "CzechBankSettings": {
+        "DailyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
+        "OtherCurrencyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/fx-rates-of-other-currencies/fx-rates-of-other-currencies/fx_rates.txt",
+        "TimeoutSeconds": 10,
+        "RetryCount": 3
+    }
+}
\ 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 000000000..4fd3d6668
--- /dev/null
+++ b/jobs/Backend/Task/appsettings.json
@@ -0,0 +1,16 @@
+{
+    "Logging": {
+        "LogLevel": {
+            "Default": "Information",
+            "ExchangeRateUpdater": "Debug",
+            "Microsoft": "Warning",
+            "System": "Warning"
+        }
+    },
+    "CzechBankSettings": {
+        "DailyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt",
+        "OtherCurrencyRatesUrl": "https://www.cnb.cz/en/financial-markets/foreign-exchange-market/fx-rates-of-other-currencies/fx-rates-of-other-currencies/fx_rates.txt",
+        "TimeoutSeconds": 10,
+        "RetryCount": 3
+    }
+}
\ No newline at end of file
diff --git a/jobs/Backend/Task/deployment.md b/jobs/Backend/Task/deployment.md
new file mode 100644
index 000000000..a39d7222b
--- /dev/null
+++ b/jobs/Backend/Task/deployment.md
@@ -0,0 +1,12 @@
+# Deployment Guide: ExchangeRateUpdater
+
+This document explains how to deploy and run the Exchange Rate Updater console app in production.
+
+---
+
+## Build & Publish
+
+Publish as a **self-contained single file executable** for your target OS:
+
+```bash
+dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish