diff --git a/.gitignore b/.gitignore index fd3586545..dc1332829 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,12 @@ node_modules bower_components npm-debug.log +/jobs/Backend/Task/.vs/ExchangeRateUpdater/CopilotIndices/17.14.939.21063/SemanticSymbols.db +*.db-shm +*.db-wal +*.vsidx +*.v2 +/jobs/Backend/Task/FileCache +*.bin +/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/000.testlog +/.vs diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs new file mode 100644 index 000000000..5eb1babee --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/CzeExchangeRateProviderTests.cs @@ -0,0 +1,205 @@ +using System.Net; +using System.Xml.Serialization; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Models.Cache; +using ExchangeRateUpdater.Models.Countries.CZE; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; + +namespace ExchangeRateUpdater.Tests; + +public class CzeExchangeRateProviderTests +{ + private readonly Mock _cacheMock = new(); + private readonly Mock _dateTimeProviderMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly Mock> _optionsMock = new(); + private readonly HttpClient _httpClient; + + private readonly CzeSettings _settings = new() + { + BaseUrl = "http://fake-url", + TtlInSeconds = 60, + UpdateHourInLocalTime = "06:00:00" + }; + + private readonly List _currencies = new() + { + new Currency("USD"), + new Currency("EUR") + }; + + public CzeExchangeRateProviderTests() + { + _optionsMock.Setup(o => o.Value).Returns(_settings); + + var handlerMock = new Mock(MockBehavior.Strict); + _httpClient = new HttpClient(handlerMock.Object); + + _dateTimeProviderMock.Setup(d => d.UtcNow).Returns(new DateTimeOffset(2025, 8, 2, 0, 0, 0, TimeSpan.Zero)); + } + + [Fact] + public async Task GetExchangeRates_ReturnsEmpty_WhenBaseUrlIsEmpty() + { + _settings.BaseUrl = null; + var provider = CreateProvider(); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.Empty(result); + _loggerMock.VerifyLog(LogLevel.Warning, "Base URL is not configured", Times.Once()); + } + + [Fact] + public async Task GetExchangeRates_ReturnsCachedData_IfCacheValid() + { + var cachedResponse = new CzeExchangeRatesResponse + { + Table = new CzeExchangeRateTable + { + Rates = new List + { + new() { Code = "USD", Amount = 1, RateRaw = "22,0" }, + new() { Code = "EUR", Amount = 1, RateRaw = "24.0" }, + } + } + }; + + var cacheObject = new CacheObject + { + Data = cachedResponse, + DataExtractionTimeUTC = DateTimeOffset.UtcNow.AddHours(1) + }; + + _cacheMock.Setup(c => c.GetAsync>(It.IsAny())) + .ReturnsAsync(cacheObject); + + var provider = CreateProvider(); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.NotEmpty(result); + Assert.Contains(result, r => r.TargetCurrency.Code == "USD" && r.Value == 22); + Assert.Contains(result, r => r.TargetCurrency.Code == "EUR" && r.Value == 24); + } + + [Fact] + public async Task GetExchangeRates_FetchesFromRemoteAndCaches_WhenNoValidCache() + { + var response = new CzeExchangeRatesResponse + { + Table = new CzeExchangeRateTable + { + Rates = new List + { + new() { Code = "USD", Amount = 2, RateRaw = "44,0" }, + new() { Code = "EUR", Amount = 1, RateRaw = "24.0" }, + } + } + }; + + var memoryStream = SerializeToXmlStream(response); + + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StreamContent(memoryStream) + }); + + _cacheMock.Setup(c => c.GetAsync>(It.IsAny())) + .ReturnsAsync((CacheObject)null); + + _cacheMock.Setup(c => c.SetAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var provider = CreateProvider(handlerMock.Object); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.NotEmpty(result); + Assert.Contains(result, r => r.TargetCurrency.Code == "USD" && r.Value == 22); + Assert.Contains(result, r => r.TargetCurrency.Code == "EUR" && r.Value == 24); + + _cacheMock.Verify(); + } + + [Fact] + public async Task GetExchangeRates_ReturnsEmpty_WhenRemoteFetchFails() + { + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException("Network error")); + + _cacheMock.Setup(c => c.GetAsync>(It.IsAny())) + .ReturnsAsync((CacheObject?)null); + + var provider = CreateProvider(handlerMock.Object); + + var result = await provider.GetExchangeRates(_currencies); + + Assert.Empty(result); + } + + [Fact] + public void GetUpdateHourInUTC_ReturnsCorrectUtcDateTime() + { + var fixedNow = new DateTimeOffset(2024, 1, 1, 10, 0, 0, TimeSpan.Zero); + _dateTimeProviderMock.Setup(d => d.UtcNow).Returns(fixedNow); + + var provider = CreateProvider(); + + var result = provider.GetType() + .GetMethod("GetUpdateHourInUTC", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(provider, null); + + Assert.IsType(result); + + var utcResult = (DateTimeOffset)result; + + var czechZone = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); + var expectedLocal = new DateTime(fixedNow.Year, fixedNow.Month, fixedNow.Day, 6, 0, 0); + var expectedUtc = new DateTimeOffset(expectedLocal, czechZone.GetUtcOffset(expectedLocal)).ToUniversalTime(); + + Assert.Equal(expectedUtc, utcResult); + } + + private static MemoryStream SerializeToXmlStream(CzeExchangeRatesResponse response) + { + var serializer = new XmlSerializer(typeof(CzeExchangeRatesResponse)); + var ms = new MemoryStream(); + using (var writer = new StreamWriter(ms, new System.Text.UTF8Encoding(true), 1024, leaveOpen: true)) + { + serializer.Serialize(writer, response); + } + ms.Position = 0; + return ms; + } + + private CzeExchangeRateProvider CreateProvider(HttpMessageHandler? handler = null) + { + var client = handler == null ? _httpClient : new HttpClient(handler); + + return new CzeExchangeRateProvider( + client, + _optionsMock.Object, + _cacheMock.Object, + _dateTimeProviderMock.Object, + _loggerMock.Object); + } +} \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj new file mode 100644 index 000000000..a419bc35c --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs new file mode 100644 index 000000000..3ea240019 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/FileCacheServiceTests.cs @@ -0,0 +1,50 @@ +using ExchangeRateUpdater.Services.Cache; + +namespace ExchangeRateUpdater.Tests; + +public class FileCacheServiceTests : IDisposable +{ + private readonly string _tempFilePath; + private readonly FileCacheService _fileCacheService; + + public FileCacheServiceTests() + { + _tempFilePath = Path.Combine(Path.GetTempPath(), $"testcache_{Guid.NewGuid()}.json"); + _fileCacheService = new FileCacheService(_tempFilePath); + } + + [Fact] + public async Task SetAndGetAsync_StoresAndRetrievesValue() + { + var key = "test-key"; + var testObject = new TestCacheObject { Value = "Hello, World!" }; + var ttl = TimeSpan.FromMinutes(5); + + await _fileCacheService.SetAsync(key, testObject, ttl); + + var cached = await _fileCacheService.GetAsync(key); + + Assert.NotNull(cached); + Assert.Equal(testObject.Value, cached.Value); + } + + [Fact] + public async Task GetAsync_ReturnsNull_WhenKeyDoesNotExist() + { + var cached = await _fileCacheService.GetAsync("non-existent-key"); + Assert.Null(cached); + } + + public void Dispose() + { + if (File.Exists(_tempFilePath)) + { + File.Delete(_tempFilePath); + } + } + + private class TestCacheObject + { + public string Value { get; set; } + } +} diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs b/jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs new file mode 100644 index 000000000..c2d1c611d --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/LoggerExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging; +using Moq; + +namespace ExchangeRateUpdater.Tests; +public static class LoggerExtensions +{ + public static void VerifyLog(this Mock> loggerMock, LogLevel level, string messageFragment, Times times) + { + loggerMock.Verify(x => x.Log( + level, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(messageFragment)), + It.IsAny(), + (Func)It.IsAny()), + times); + } +} \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs b/jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs new file mode 100644 index 000000000..0928d48e2 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/StartupTests.cs @@ -0,0 +1,80 @@ +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models.Countries.CZE; +using ExchangeRateUpdater.Services.Cache; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace ExchangeRateUpdater.Tests; + +public class StartupTests +{ + [Fact] + public void ConfigureServices_RegistersExpectedServices() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + {"CacheSettings:Provider", "file"}, + {"CacheSettings:FileCachePath", "testcache.json"}, + {"ExchangeProviders:CZE:BaseUrl", "http://example.com"}, + {"ExchangeProviders:CZE:TtlInSeconds", "60"}, + {"ExchangeProviders:CZE:UpdateHourInLocalTime", "06:00:00"} + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act + Startup.ConfigureServices(services, configuration); + var provider = services.BuildServiceProvider(); + + // Assert + var app = provider.GetService(); + Assert.NotNull(app); + + var dateTimeProvider = provider.GetService(); + Assert.NotNull(dateTimeProvider); + + var cacheService = provider.GetService(); + Assert.NotNull(cacheService); + Assert.IsType(cacheService); + + + var czeSettings = provider.GetService>(); + Assert.NotNull(czeSettings); + Assert.Equal("http://example.com", czeSettings.Value.BaseUrl); + + var httpClientFactory = provider.GetService(); + Assert.NotNull(httpClientFactory); + var httpClient = httpClientFactory.CreateClient(nameof(CzeExchangeRateProvider)); + Assert.NotNull(httpClient); + } + + [Fact] + public void AddCache_RegistersRedisCache_WhenProviderIsRedis() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + {"CacheSettings:Provider", "redis"}, + {"CacheSettings:RedisConfiguration", "localhost:6379"} + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act + Startup.ConfigureServices(services, configuration); + var provider = services.BuildServiceProvider(); + + // Assert + var cacheService = provider.GetService(); + Assert.NotNull(cacheService); + Assert.IsType(cacheService); + } +} diff --git a/jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs b/jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdater.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 b/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 000000000..a9e8fdb52 Binary files /dev/null and b/jobs/Backend/Task/.vs/ExchangeRateUpdater/DesignTimeBuild/.dtbcache.v2 differ diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config b/jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config new file mode 100644 index 000000000..0d88f0db3 --- /dev/null +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/config/applicationhost.config @@ -0,0 +1,1016 @@ + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json new file mode 100644 index 000000000..9ed221850 --- /dev/null +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.backup.json @@ -0,0 +1,436 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\filecacheservicetests.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\startuptests.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:app.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\czeexchangerateprovidertests.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\interfaces\\idatetimeprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:interfaces\\idatetimeprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:interfaces\\iexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\loggerextensions.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{1414190F-2DEA-401A-A614-E26DCD42C432}|..\\ExchangeRateUpdater.Tests\\ExchangeRateUpdater.Tests.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\exchangerateupdater.tests\\usings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\exchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 28, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:129:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:130:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:131:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:136:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:131:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:134:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:135:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:136:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:137:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:138:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:140:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:139:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:130:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + }, + { + "$type": "Bookmark", + "Name": "ST:133:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:1:0:{5a4e9529-b6a0-46b5-be4f-0f0b239bc0eb}" + }, + { + "$type": "Bookmark", + "Name": "ST:323948898:0:{81164725-9a96-4ece-a4cb-440d8fd285e5}" + }, + { + "$type": "Bookmark", + "Name": "ST:5:0:{d212f56b-c48a-434c-a121-1c5d80b59b9f}" + }, + { + "$type": "Bookmark", + "Name": "ST:135:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:134:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:133:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{56df62a4-05a3-4e5b-aa1a-99371ccfb997}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{e1b7d1f8-9b3c-49b1-8f4f-bfc63a88835d}" + }, + { + "$type": "Bookmark", + "Name": "ST:129:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + }, + { + "$type": "Document", + "DocumentIndex": 2, + "Title": "StartupTests.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\StartupTests.cs", + "ViewState": "AgIAACoAAAAAAAAAAAAgwE4AAAAFAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:24:23.772Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "FileCacheServiceTests.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\FileCacheServiceTests.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:23:16.61Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "Startup.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeDocumentMoniker": "Startup.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeToolTip": "Startup.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAACMAAABfAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:21:45.442Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 3, + "Title": "Program.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs", + "RelativeDocumentMoniker": "Program.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs", + "RelativeToolTip": "Program.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:21:42.091Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 5, + "Title": "CzeExchangeRateProviderTests.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\CzeExchangeRateProviderTests.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAIoAAABKAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:57:01.588Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 10, + "Title": "IDateTimeProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IDateTimeProvider.cs", + "RelativeDocumentMoniker": "Interfaces\\IDateTimeProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IDateTimeProvider.cs", + "RelativeToolTip": "Interfaces\\IDateTimeProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:55:17.525Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 11, + "Title": "IExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Interfaces\\IExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Interfaces\\IExchangeRateProvider.cs", + "RelativeToolTip": "Interfaces\\IExchangeRateProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:55:15.094Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 14, + "Title": "ExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\ExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\ExchangeRateProvider.cs", + "RelativeToolTip": "Services\\ExchangeRateProvider.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:55:10.992Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 12, + "Title": "LoggerExtensions.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\LoggerExtensions.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:02:02.386Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 4, + "Title": "App.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", + "RelativeDocumentMoniker": "App.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\App.cs", + "RelativeToolTip": "App.cs", + "ViewState": "AgIAABcAAAAAAAAAAAAQwDEAAABRAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:52:36.833Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 13, + "Title": "Usings.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\Usings.cs", + "RelativeDocumentMoniker": "..\\ExchangeRateUpdater.Tests\\Usings.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\ExchangeRateUpdater.Tests\\Usings.cs", + "RelativeToolTip": "..\\ExchangeRateUpdater.Tests\\Usings.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:57:28.228Z", + "EditorCaption": "" + } + ] + }, + { + "DockedWidth": 945, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:1:0:{d212f56b-c48a-434c-a121-1c5d80b59b9f}" + } + ] + }, + { + "DockedWidth": 945, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:132:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + } + ] + } + ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "64a7b947-f702-48e2-8c80-163b64d61eba", + "Display": 1, + "X": 1007, + "Y": -40, + "Width": 858, + "Height": 742, + "WindowState": 0 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 7, + "Title": "ExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\ExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeToolTip": "Models\\ExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAiAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:05:21.722Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 8, + "Title": "CzeExchangeRateTable.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAfAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:04:44.734Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 9, + "Title": "CzeExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:03:19.974Z", + "EditorCaption": "" + } + ] + } + ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "4992753e-af97-424d-a7c0-074954eebdd2", + "Display": 1, + "X": 682, + "Y": 82, + "Width": 1313, + "Height": 880, + "WindowState": 2 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 6, + "Title": "CzeExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ViewState": "AgIAAEIAAAAAAAAAAAAawFQAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:54:59.523Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json new file mode 100644 index 000000000..801bc754c --- /dev/null +++ b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/DocumentLayout.json @@ -0,0 +1,288 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:startup.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:services\\countries\\cze\\czeexchangerateprovider.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\exchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangeratetable.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + }, + { + "AbsoluteMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|c:\\users\\josemigueld\u00EDazbello\\documents\\github\\mews\\jobs\\backend\\task\\models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", + "RelativeMoniker": "D:0:0:{7B2695D6-D24C-4460-A58E-A10F08550CE0}|ExchangeRateUpdater.csproj|solutionrelative:models\\countries\\cze\\czeexchangerate.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 26, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:129:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:130:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:131:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:136:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:131:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:134:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:135:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:136:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:137:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:138:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:140:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:139:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:130:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + }, + { + "$type": "Bookmark", + "Name": "ST:133:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:1:0:{5a4e9529-b6a0-46b5-be4f-0f0b239bc0eb}" + }, + { + "$type": "Bookmark", + "Name": "ST:323948898:0:{81164725-9a96-4ece-a4cb-440d8fd285e5}" + }, + { + "$type": "Bookmark", + "Name": "ST:5:0:{d212f56b-c48a-434c-a121-1c5d80b59b9f}" + }, + { + "$type": "Bookmark", + "Name": "ST:135:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:134:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:133:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{56df62a4-05a3-4e5b-aa1a-99371ccfb997}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{e1b7d1f8-9b3c-49b1-8f4f-bfc63a88835d}" + }, + { + "$type": "Bookmark", + "Name": "ST:129:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "Program.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs", + "RelativeDocumentMoniker": "Program.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Program.cs*", + "RelativeToolTip": "Program.cs*", + "ViewState": "AgIAAAAAAAAAAAAAAAAAACAAAAATAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:29:16.461Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 1, + "Title": "Startup.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeDocumentMoniker": "Startup.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Startup.cs", + "RelativeToolTip": "Startup.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAACMAAABfAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:21:45.442Z", + "EditorCaption": "" + } + ] + }, + { + "DockedWidth": 945, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:1:0:{d212f56b-c48a-434c-a121-1c5d80b59b9f}" + } + ] + }, + { + "DockedWidth": 945, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:132:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + } + ] + } + ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "64a7b947-f702-48e2-8c80-163b64d61eba", + "Display": 1, + "X": 1007, + "Y": -40, + "Width": 858, + "Height": 742, + "WindowState": 0 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 3, + "Title": "ExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\ExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\ExchangeRate.cs", + "RelativeToolTip": "Models\\ExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAiAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:05:21.722Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 4, + "Title": "CzeExchangeRateTable.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRateTable.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAfAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:04:44.734Z", + "EditorCaption": "" + }, + { + "$type": "Document", + "DocumentIndex": 5, + "Title": "CzeExchangeRate.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeDocumentMoniker": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Models\\Countries\\CZE\\CzeExchangeRate.cs", + "RelativeToolTip": "Models\\Countries\\CZE\\CzeExchangeRate.cs", + "ViewState": "AgIAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T16:03:19.974Z", + "EditorCaption": "" + } + ] + } + ] + }, + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "FloatingWindowState": { + "Id": "4992753e-af97-424d-a7c0-074954eebdd2", + "Display": 1, + "X": 682, + "Y": 82, + "Width": 1313, + "Height": 880, + "WindowState": 2 + }, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 0, + "Children": [ + { + "$type": "Document", + "DocumentIndex": 2, + "Title": "CzeExchangeRateProvider.cs", + "DocumentMoniker": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeDocumentMoniker": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ToolTip": "C:\\Users\\JoseMiguelD\u00EDazBello\\Documents\\github\\mews\\jobs\\Backend\\Task\\Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "RelativeToolTip": "Services\\Countries\\CZE\\CzeExchangeRateProvider.cs", + "ViewState": "AgIAABQAAAAAAAAAAAAEwCkAAAAAAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", + "WhenOpened": "2025-08-02T15:54:59.523Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest new file mode 100644 index 000000000..e92ede29d Binary files /dev/null and b/jobs/Backend/Task/.vs/ExchangeRateUpdater/v17/TestStore/0/testlog.manifest differ diff --git a/jobs/Backend/Task/App.cs b/jobs/Backend/Task/App.cs new file mode 100644 index 000000000..67ed6112c --- /dev/null +++ b/jobs/Backend/Task/App.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ExchangeRateUpdater.Factories; +using ExchangeRateUpdater.Models; +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater; + +public class App +{ + private readonly TextWriter _output; + private readonly ExchangeRateProviderFactory _factory; + private readonly ILogger _logger; + + private static readonly IEnumerable currencies = new[] + { + new Currency("USD"), + new Currency("EUR"), + new Currency("CZK"), + new Currency("JPY"), + new Currency("KES"), + new Currency("RUB"), + new Currency("THB"), + new Currency("TRY"), + new Currency("XYZ") + }; + + private static readonly CountryIsoAlpha3 country = CountryIsoAlpha3.CZE; + + public App( + ILogger logger, + TextWriter output, + ExchangeRateProviderFactory factory) + { + _logger = logger; + _output = output; + _factory = factory; + } + + public async Task Run() + { + try + { + _logger.LogInformation("Application started execution."); + var provider = _factory.CreateProvider(country); + var rates = await provider.GetExchangeRates(currencies); + var count = rates.Count(); + _logger.LogInformation("Successfully retrieved {Count} exchange rates.", count); + _output.WriteLine($"Successfully retrieved {count} exchange rates:"); + foreach (var rate in rates) + { + _output.WriteLine(rate.ToString()); + } + _logger.LogInformation("Application execution finalized successfully."); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while retrieving exchange rates."); + _output.WriteLine($"Could not retrieve exchange rates: '{e.Message}'"); + } + } +} 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.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2fc654a12..9c3ac81e0 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -5,4 +5,36 @@ net6.0 + + + + + + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 89be84daf..7de0b2e7d 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36327.8 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Tests", "..\ExchangeRateUpdater.Tests\ExchangeRateUpdater.Tests.csproj", "{1414190F-2DEA-401A-A614-E26DCD42C432}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,8 +17,15 @@ Global {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU + {1414190F-2DEA-401A-A614-E26DCD42C432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1414190F-2DEA-401A-A614-E26DCD42C432}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1414190F-2DEA-401A-A614-E26DCD42C432}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1414190F-2DEA-401A-A614-E26DCD42C432}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {20BD7F17-12B4-44E7-93B3-9CE53B800D85} + EndGlobalSection EndGlobal diff --git a/jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs b/jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs new file mode 100644 index 000000000..c0fd5242b --- /dev/null +++ b/jobs/Backend/Task/Factories/ExchangeRateProviderFactory.cs @@ -0,0 +1,27 @@ +using System; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Services; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.DependencyInjection; + +namespace ExchangeRateUpdater.Factories; + +public class ExchangeRateProviderFactory +{ + private readonly IServiceProvider _serviceProvider; + + public ExchangeRateProviderFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IExchangeRateProvider CreateProvider(CountryIsoAlpha3 country) + { + return country switch + { + CountryIsoAlpha3.CZE => _serviceProvider.GetRequiredService(), + _ => _serviceProvider.GetRequiredService() + }; + } +} diff --git a/jobs/Backend/Task/Interfaces/ICacheService.cs b/jobs/Backend/Task/Interfaces/ICacheService.cs new file mode 100644 index 000000000..784441cdb --- /dev/null +++ b/jobs/Backend/Task/Interfaces/ICacheService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Interfaces; + +public interface ICacheService +{ + Task GetAsync(string key); + Task SetAsync(string key, T value, TimeSpan ttl); +} diff --git a/jobs/Backend/Task/Interfaces/IDateTimeProvider.cs b/jobs/Backend/Task/Interfaces/IDateTimeProvider.cs new file mode 100644 index 000000000..5699c99bb --- /dev/null +++ b/jobs/Backend/Task/Interfaces/IDateTimeProvider.cs @@ -0,0 +1,8 @@ +using System; + +namespace ExchangeRateUpdater.Interfaces; + +public interface IDateTimeProvider +{ + DateTimeOffset UtcNow { get; } +} diff --git a/jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs b/jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs new file mode 100644 index 000000000..9b285292b --- /dev/null +++ b/jobs/Backend/Task/Interfaces/IExchangeRateProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ExchangeRateUpdater.Models; + +namespace ExchangeRateUpdater.Interfaces; + +public interface IExchangeRateProvider +{ + Task> GetExchangeRates(IEnumerable currencies); +} diff --git a/jobs/Backend/Task/Models/Cache/CacheObject.cs b/jobs/Backend/Task/Models/Cache/CacheObject.cs new file mode 100644 index 000000000..9377ff848 --- /dev/null +++ b/jobs/Backend/Task/Models/Cache/CacheObject.cs @@ -0,0 +1,9 @@ +using System; + +namespace ExchangeRateUpdater.Models.Cache; + +public class CacheObject +{ + public T Data { get; set; } + public DateTimeOffset DataExtractionTimeUTC { get; set; } +} diff --git a/jobs/Backend/Task/Models/Cache/CacheSettings.cs b/jobs/Backend/Task/Models/Cache/CacheSettings.cs new file mode 100644 index 000000000..f80352d90 --- /dev/null +++ b/jobs/Backend/Task/Models/Cache/CacheSettings.cs @@ -0,0 +1,7 @@ +namespace ExchangeRateUpdater.Models.Cache; + +public class CacheSettings +{ + public string Provider { get; set; } = "Memory"; // or "Redis" + public string RedisConfiguration { get; set; } = string.Empty; +} diff --git a/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRate.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRate.cs new file mode 100644 index 000000000..63d4824e4 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRate.cs @@ -0,0 +1,24 @@ +using System.Xml.Serialization; + +namespace ExchangeRateUpdater.Models.Countries.CZE; + +public class CzeExchangeRate +{ + [XmlAttribute("kod")] + public string Code { get; set; } + + [XmlAttribute("mena")] + public string CurrencyName { get; set; } + + [XmlAttribute("mnozstvi")] + public int Amount { get; set; } + + [XmlAttribute("kurz")] + public string RateRaw { get; set; } // Ej: "13,854" + + [XmlAttribute("zeme")] + public string Country { get; set; } + + [XmlIgnore] + public decimal Rate => decimal.Parse(RateRaw.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture); +} diff --git a/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRateTable.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRateTable.cs new file mode 100644 index 000000000..610c2c030 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRateTable.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace ExchangeRateUpdater.Models.Countries.CZE; + +public class CzeExchangeRateTable +{ + [XmlAttribute("typ")] + public string Type { get; set; } + + [XmlElement("radek")] + public List Rates { get; set; } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRatesResponse.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRatesResponse.cs new file mode 100644 index 000000000..bc40e4e9f --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeExchangeRatesResponse.cs @@ -0,0 +1,19 @@ +using System.Xml.Serialization; + +namespace ExchangeRateUpdater.Models.Countries.CZE; + +[XmlRoot("kurzy")] +public class CzeExchangeRatesResponse +{ + [XmlAttribute("banka")] + public string Bank { get; set; } + + [XmlAttribute("datum")] + public string Date { get; set; } + + [XmlAttribute("poradi")] + public string Sequence { get; set; } + + [XmlElement("tabulka")] + public CzeExchangeRateTable Table { get; set; } +} diff --git a/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs b/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs new file mode 100644 index 000000000..31939fa66 --- /dev/null +++ b/jobs/Backend/Task/Models/Countries/CZE/CzeSettings.cs @@ -0,0 +1,8 @@ +namespace ExchangeRateUpdater.Models.Countries.CZE; + +public class CzeSettings +{ + public string BaseUrl { get; set; } = string.Empty; + public int TtlInSeconds { get; set; } = 0; + public string UpdateHourInLocalTime { get; set; } = "00:00:00"; +} diff --git a/jobs/Backend/Task/Models/CountryIsoAlpha3.cs b/jobs/Backend/Task/Models/CountryIsoAlpha3.cs new file mode 100644 index 000000000..ccffeeb6d --- /dev/null +++ b/jobs/Backend/Task/Models/CountryIsoAlpha3.cs @@ -0,0 +1,6 @@ +namespace ExchangeRateUpdater.Models; + +public enum CountryIsoAlpha3 +{ + CZE +} diff --git a/jobs/Backend/Task/Models/Currency.cs b/jobs/Backend/Task/Models/Currency.cs new file mode 100644 index 000000000..0f8d897b1 --- /dev/null +++ b/jobs/Backend/Task/Models/Currency.cs @@ -0,0 +1,19 @@ +namespace ExchangeRateUpdater.Models; + +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/Models/ExchangeRate.cs b/jobs/Backend/Task/Models/ExchangeRate.cs new file mode 100644 index 000000000..a415ffa1d --- /dev/null +++ b/jobs/Backend/Task/Models/ExchangeRate.cs @@ -0,0 +1,22 @@ +namespace ExchangeRateUpdater.Models; + +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/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f..23bc10735 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,43 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater; + +public static class Program { - public static class Program + public static async Task Main(string[] args) { - private static IEnumerable currencies = new[] - { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") - }; + var host = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + var env = hostingContext.HostingEnvironment; - public static void Main(string[] args) - { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((context, logging) => + { + logging.ClearProviders(); + logging.AddConfiguration(context.Configuration.GetSection("Logging")); + logging.AddSimpleConsole(options => { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) + options.TimestampFormat = "[HH:mm:ss] "; + options.IncludeScopes = false; + options.SingleLine = true; + options.UseUtcTimestamp = false; + }); + }) + .ConfigureServices((context, services) => { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); - } + Startup.ConfigureServices(services, context.Configuration); + }) + .Build(); - Console.ReadLine(); - } + var app = host.Services.GetRequiredService(); + await app.Run(); } } diff --git a/jobs/Backend/Task/Properties/launchSettings.json b/jobs/Backend/Task/Properties/launchSettings.json new file mode 100644 index 000000000..364905a25 --- /dev/null +++ b/jobs/Backend/Task/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "MyConsoleApp": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "dev" + } + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/ReadMe.md b/jobs/Backend/Task/ReadMe.md new file mode 100644 index 000000000..0b553f18b --- /dev/null +++ b/jobs/Backend/Task/ReadMe.md @@ -0,0 +1,111 @@ +# Exchange Rate Provider - Modular & Extensible System + +This project introduces a modular system for retrieving and managing exchange rates from multiple countries, with a focus on clean architecture, environment configuration, and maintainability. The initial implementation supports only the **Czech Republic**, but is fully extensible to other countries. + +--- + +## Architecture Overview + +A **factory pattern** has been introduced to encapsulate the logic of retrieving exchange rates per country. Currently, only the Czech Republic (CZE) is implemented, but support for other countries can be easily added by implementing the `IExchangeRateProvider` interface. + +--- + +## Dependency Injection + +The project has been updated to use dependency injection for all services and components, enabling better testability, separation of concerns, and integration with ASP.NET Core or other DI containers. + +--- + +## Logging + +Logging has been configured by default to output to the console. This configuration is environment-specific and can be adjusted for other targets such as files, cloud logging platforms, or databases depending on the deployment context. + +--- + +## Environment Configuration + +Environment-specific configuration has been implemented, starting with the `Development` environment. The `appsettings.Development.json` file includes: + +- Minimal log configuration +- Exchange rate provider settings (currently for CZE only), including: + - Source URL + - Minimum cache duration + - Local update time +- Cache configuration (in development, a JSON file-based cache is used) + +For production environments, the configuration can be easily adapted to use more robust systems such as **Redis** for caching. + +--- + +## Caching + +A caching layer has been added to reduce the number of calls to the **Czech National Bank (CNB)** API. This prevents unnecessary requests and conserves network bandwidth. Cached values are stored with timestamps to determine validity based on the update time. + +--- + +## Time Management (UTC Standardization) + +To handle time zones correctly and consistently across environments, all timestamps are transformed and stored in **UTC**. This standardization helps ensure consistency in time comparisons, regardless of local server configuration. + +A custom interface `IDateTimeProvider` has been introduced to abstract UTC time retrieval, aiding unit testing and mocking. + +Because the `appsettings` uses **local time** strings (e.g., `"08:00:00"`), the [TimeZoneConverter](https://www.nuget.org/packages/TimeZoneConverter) NuGet package has been installed. This enables reliable conversion of the time zone `"Central European Standard Time"` into platform-compatible `TimeZoneInfo` objects, ensuring support across Windows and Linux environments. + +--- + +## Unit Testing + +Unit tests have been added to validate core functionality such as: +- Time zone conversions +- Cache validity checks +- Factory provider resolution +- Rate retrieval and mapping + +These tests ensure the system behaves correctly and can be safely extended. + +--- + +## Adding a New Country + +To support a new country's exchange rates: + +1. **Add it to the enum**: + Extend `CountryIsoAlpha3` with the new country code. + +2. **Implement the provider**: + Create a new class that implements `IExchangeRateProvider`. + +3. **Register in the factory**: + Add your new implementation to the `ExchangeRateProviderFactory`. + +4. **Add configuration**: + Extend the `ExchangeProviders` section in `appsettings.*.json` with required fields such as: + - URL + - Cache settings + - Update schedule + +5. **Create settings model**: + Define a `Settings` class matching the configuration structure for the new country. + +6. **Register services**: + Add service registration in the `AddExchangeProviders` method or module. + +--- + +## Project Highlights + +- Factory pattern for country-specific logic +- Dependency injection support +- Configurable per environment +- Unified UTC time handling +- Efficient caching +- Unit-tested architecture +- Easily extensible for additional countries + +--- + + +## Conclusion + +This architecture enables scalable and environment-aware handling of exchange rates for multiple countries, promoting clean design and operational flexibility. By adhering to SOLID principles, the system is easy to extend, test, and configure for real-world use. + diff --git a/jobs/Backend/Task/Services/Cache/FileCacheService.cs b/jobs/Backend/Task/Services/Cache/FileCacheService.cs new file mode 100644 index 000000000..747ea8711 --- /dev/null +++ b/jobs/Backend/Task/Services/Cache/FileCacheService.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using System.Text.Json; + +namespace ExchangeRateUpdater.Services.Cache; + +public class FileCacheService : ICacheService +{ + private readonly string _filePath; + private readonly object _lock = new(); + private readonly Dictionary _cache; + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + + public FileCacheService(string filePath) + { + _filePath = filePath; + Directory.CreateDirectory(Path.GetDirectoryName(_filePath)); + if (File.Exists(_filePath)) + { + var json = File.ReadAllText(_filePath); + _cache = JsonSerializer.Deserialize>(json, _jsonOptions) + ?? new Dictionary(); + } + else + { + _cache = new Dictionary(); + } + + CleanupExpiredEntries(); + } + + public Task GetAsync(string key) + { + lock (_lock) + { + if (_cache.TryGetValue(key, out var entry)) + { + if (entry.ExpiresAt == null || entry.ExpiresAt > DateTime.UtcNow) + { + return Task.FromResult(JsonSerializer.Deserialize(entry.JsonValue, _jsonOptions)); + } + else + { + _cache.Remove(key); + Save(); + } + } + } + + return Task.FromResult(default); + } + + public Task SetAsync(string key, T value, TimeSpan ttl) + { + var entry = new CacheEntry + { + JsonValue = JsonSerializer.Serialize(value, _jsonOptions), + ExpiresAt = DateTime.UtcNow.Add(ttl) + }; + + lock (_lock) + { + _cache[key] = entry; + Save(); + } + + return Task.CompletedTask; + } + + private void Save() + { + var json = JsonSerializer.Serialize(_cache, _jsonOptions); + File.WriteAllText(_filePath, json); + } + + private void CleanupExpiredEntries() + { + var expiredKeys = _cache + .Where(kv => kv.Value.ExpiresAt != null && kv.Value.ExpiresAt <= DateTime.UtcNow) + .Select(kv => kv.Key) + .ToList(); + + foreach (var key in expiredKeys) + _cache.Remove(key); + + if (expiredKeys.Any()) + Save(); + } + + private class CacheEntry + { + public string JsonValue { get; set; } = string.Empty; + public DateTime? ExpiresAt { get; set; } + } +} diff --git a/jobs/Backend/Task/Services/Cache/RedisCacheService.cs b/jobs/Backend/Task/Services/Cache/RedisCacheService.cs new file mode 100644 index 000000000..89bbd9da2 --- /dev/null +++ b/jobs/Backend/Task/Services/Cache/RedisCacheService.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using Microsoft.Extensions.Caching.Distributed; + +namespace ExchangeRateUpdater.Services.Cache; + +public class RedisCacheService : ICacheService +{ + private readonly IDistributedCache _distributedCache; + + public RedisCacheService(IDistributedCache distributedCache) + { + _distributedCache = distributedCache; + } + + public async Task GetAsync(string key) + { + var json = await _distributedCache.GetStringAsync(key); + return json is null ? default : JsonSerializer.Deserialize(json); + } + + public async Task SetAsync(string key, T value, TimeSpan ttl) + { + var json = JsonSerializer.Serialize(value); + await _distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = ttl + }); + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs new file mode 100644 index 000000000..80250f117 --- /dev/null +++ b/jobs/Backend/Task/Services/Countries/CZE/CzeExchangeRateProvider.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; +using ExchangeRateUpdater.Models.Cache; +using ExchangeRateUpdater.Models.Countries.CZE; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TimeZoneConverter; + +namespace ExchangeRateUpdater.Services.Countries.CZE; + +public class CzeExchangeRateProvider : IExchangeRateProvider +{ + private const string BaseCurrencyCode = "CZK"; + private const string CacheKey = "CZE_data"; + private const string TimeZone = "Central European Standard Time"; + private readonly HttpClient _httpClient; + public readonly CzeSettings _settings; + private readonly ICacheService _cache; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; + + public CzeExchangeRateProvider( + HttpClient httpClient, + IOptionsSnapshot options, + ICacheService cache, + IDateTimeProvider dateTimeProvider, + ILogger logger) + { + _httpClient = httpClient; + _settings = options.Value; + _cache = cache; + _dateTimeProvider = dateTimeProvider; + _logger = logger; + } + + public async Task> GetExchangeRates(IEnumerable currencies) + { + _logger.LogInformation("Starting exchange rate retrieval."); + + if (string.IsNullOrWhiteSpace(_settings.BaseUrl)) + { + _logger.LogWarning("Base URL is not configured. Aborting exchange rate retrieval."); + return Enumerable.Empty(); + } + + var cachedRates = await TryGetValidCachedRatesAsync(currencies); + if (cachedRates is not null) + { + _logger.LogInformation("Valid cached exchange rates found. Returning cached data."); + return cachedRates; + } + + _logger.LogInformation("No valid cache found. Fetching exchange rates from remote source."); + + var latestRates = await FetchRatesFromCzechBankAsync(); + if (latestRates is null) + { + _logger.LogWarning("Failed to retrieve or deserialize exchange rates from remote source."); + return Enumerable.Empty(); + } + + _logger.LogInformation("Successfully fetched exchange rates. Caching results."); + await CacheRatesAsync(latestRates); + + _logger.LogInformation("Returning newly fetched exchange rates."); + return MapToExchangeRates(latestRates, currencies); + } + + private async Task> TryGetValidCachedRatesAsync(IEnumerable currencies) + { + var cached = await _cache.GetAsync>(CacheKey); + if (cached is null) + { + _logger.LogDebug("No cache entry found for exchange rates."); + return null; + } + + var updateCutoff = GetUpdateHourInUTC(); + if (cached.DataExtractionTimeUTC > updateCutoff) + { + _logger.LogDebug("Cached exchange rates are still valid (extracted at {ExtractionTime}, cutoff is {Cutoff}).", + cached.DataExtractionTimeUTC, updateCutoff); + return MapToExchangeRates(cached.Data, currencies); + } + + _logger.LogDebug("Cached exchange rates are expired (extracted at {ExtractionTime}, cutoff is {Cutoff}).", + cached.DataExtractionTimeUTC, updateCutoff); + + return null; + } + + private async Task FetchRatesFromCzechBankAsync() + { + try + { + _logger.LogDebug("Sending HTTP request to {Url}.", _settings.BaseUrl); + + var response = await _httpClient.GetAsync(_settings.BaseUrl); + response.EnsureSuccessStatusCode(); + + _logger.LogDebug("Received successful HTTP response."); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = XmlReader.Create(stream, new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Parse + }); + + var serializer = new XmlSerializer(typeof(CzeExchangeRatesResponse)); + var deserialized = serializer.Deserialize(reader) as CzeExchangeRatesResponse; + + _logger.LogDebug("Successfully deserialized exchange rates XML."); + return deserialized; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while fetching or parsing exchange rates from {Url}.", _settings.BaseUrl); + return null; + } + } + + private async Task CacheRatesAsync(CzeExchangeRatesResponse rates) + { + var cacheObject = new CacheObject + { + Data = rates, + DataExtractionTimeUTC = _dateTimeProvider.UtcNow + }; + + var ttl = TimeSpan.FromSeconds(_settings.TtlInSeconds); + await _cache.SetAsync(CacheKey, cacheObject, ttl); + _logger.LogDebug("Exchange rates cached with TTL of {TtlSeconds} seconds.", _settings.TtlInSeconds); + } + + private IEnumerable MapToExchangeRates( + CzeExchangeRatesResponse response, + IEnumerable requestedCurrencies) + { + var requestedCodes = requestedCurrencies.Select(c => c.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); + + return response.Table.Rates + .Where(rate => requestedCodes.Contains(rate.Code)) + .Select(rate => + { + var source = new Currency(BaseCurrencyCode); + var target = new Currency(rate.Code); + var value = rate.Rate / rate.Amount; + + return new ExchangeRate(source, target, value); + }); + } + + private DateTimeOffset GetUpdateHourInUTC() + { + TimeSpan localUpdateTime = TimeSpan.ParseExact( + _settings.UpdateHourInLocalTime, + "c", + CultureInfo.InvariantCulture); + + var timeZoneInfo = TZConvert.GetTimeZoneInfo(TimeZone); + + var currentUtc = _dateTimeProvider.UtcNow; + var localDate = TimeZoneInfo.ConvertTime(currentUtc, timeZoneInfo).Date; + + DateTime localDateTime = localDate.Add(localUpdateTime); + + var localDateTimeOffset = new DateTimeOffset(localDateTime, timeZoneInfo.GetUtcOffset(localDateTime)); + + return localDateTimeOffset.ToUniversalTime(); + } +} diff --git a/jobs/Backend/Task/Services/DateTimeProvider.cs b/jobs/Backend/Task/Services/DateTimeProvider.cs new file mode 100644 index 000000000..0ad14960b --- /dev/null +++ b/jobs/Backend/Task/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using System; +using ExchangeRateUpdater.Interfaces; + +namespace ExchangeRateUpdater.Services; + +public class DateTimeProvider : IDateTimeProvider +{ + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; +} diff --git a/jobs/Backend/Task/Services/ExchangeRateProvider.cs b/jobs/Backend/Task/Services/ExchangeRateProvider.cs new file mode 100644 index 000000000..01f83c283 --- /dev/null +++ b/jobs/Backend/Task/Services/ExchangeRateProvider.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models; + +namespace ExchangeRateUpdater.Services; + +public class ExchangeRateProvider : IExchangeRateProvider +{ + public Task> GetExchangeRates(IEnumerable currencies) + { + return Task.FromResult(Enumerable.Empty()); + } +} diff --git a/jobs/Backend/Task/Startup.cs b/jobs/Backend/Task/Startup.cs new file mode 100644 index 000000000..a15e5a615 --- /dev/null +++ b/jobs/Backend/Task/Startup.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using ExchangeRateUpdater.Factories; +using ExchangeRateUpdater.Interfaces; +using ExchangeRateUpdater.Models.Cache; +using ExchangeRateUpdater.Models.Countries.CZE; +using ExchangeRateUpdater.Services; +using ExchangeRateUpdater.Services.Cache; +using ExchangeRateUpdater.Services.Countries.CZE; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace ExchangeRateUpdater; + +public static class Startup +{ + public static void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(Console.Out); + services.AddExhangeProviders(configuration); + services.AddCache(configuration); + } + + public static void AddExhangeProviders(this IServiceCollection services, IConfiguration configuration) + { + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddHttpClient(); + services.Configure(configuration.GetSection("ExchangeProviders:CZE")); + } + + public static void AddCache(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("CacheSettings")); + + var provider = configuration.GetValue("CacheSettings:Provider") ?? "file"; + + switch (provider.Trim().ToLowerInvariant()) + { + case "redis": + services.AddStackExchangeRedisCache(options => + { + var redisConfig = configuration.GetSection("CacheSettings:RedisConfiguration").Value; + options.Configuration = redisConfig; + }); + services.AddSingleton(); + break; + + case "file": + var filePath = configuration.GetValue("CacheSettings:FileCachePath") ?? "filecache.json"; + if (!Path.IsPathRooted(filePath)) + { + var baseDir = AppContext.BaseDirectory; + filePath = Path.GetFullPath(Path.Combine(baseDir, filePath)); + } + services.AddSingleton(new FileCacheService(filePath)); + break; + } + } +} diff --git a/jobs/Backend/Task/appsettings.dev.json b/jobs/Backend/Task/appsettings.dev.json new file mode 100644 index 000000000..b96da3510 --- /dev/null +++ b/jobs/Backend/Task/appsettings.dev.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ExchangeProviders": { + "CZE": { + "BaseUrl": "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml", + "TtlInSeconds": 60, + "UpdateHourInLocalTime": "14:30:00" + } + }, + "CacheSettings": { + "Provider": "File", // "Redis" + "FileCachePath": "../../../FileCache/filecache.json" + } +} \ 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..99c504f2c --- /dev/null +++ b/jobs/Backend/Task/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ExchangeProviders": { + "CZE": { + "BaseUrl": "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml", + "TtlInSeconds": 86400, //24 hours + "UpdateHourInLocalTime": "14:30:00" + } + }, + "CacheSettings": { + "Provider": "Redis", + "RedisConfiguration": "localhost:6379" + } +} \ No newline at end of file