diff --git a/.gitignore b/.gitignore index fd3586545..e3f1a1f9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,44 @@ -!.gitkeep -!.gitignore -!*.dll -[Oo]bj -[Bb]in -*.user +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode/ + +# Rider +.idea/ + +# Visual Studio +.vs/ + +# Fleet +.fleet/ + +# Code Rush +.cr/ + +# User-specific files *.suo -*.[Cc]ache -*.bak -*.ncb -*.DS_Store -*.userprefs -*.iml -*.ncrunch* -.*crunch*.local.xml -.idea -[Tt]humbs.db -*.tgz -*.sublime-* +*.user +*.userosscache +*.sln.docstates -node_modules -bower_components -npm-debug.log +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn +appsettings.Development.json \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateProvider.cs b/jobs/Backend/Task/ExchangeRateProvider.cs deleted file mode 100644 index 6f82a97fb..000000000 --- a/jobs/Backend/Task/ExchangeRateProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace ExchangeRateUpdater -{ - public class ExchangeRateProvider - { - /// - /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined - /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", - /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide - /// some of the currencies, ignore them. - /// - public IEnumerable GetExchangeRates(IEnumerable currencies) - { - return Enumerable.Empty(); - } - } -} diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj deleted file mode 100644 index 2fc654a12..000000000 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - Exe - net6.0 - - - \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln deleted file mode 100644 index 89be84daf..000000000 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B2695D6-D24C-4460-A58E-A10F08550CE0}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs deleted file mode 100644 index 379a69b1f..000000000 --- a/jobs/Backend/Task/Program.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace ExchangeRateUpdater -{ - public static class Program - { - private static IEnumerable currencies = new[] - { - new Currency("USD"), - new Currency("EUR"), - new Currency("CZK"), - new Currency("JPY"), - new Currency("KES"), - new Currency("RUB"), - new Currency("THB"), - new Currency("TRY"), - new Currency("XYZ") - }; - - public static void Main(string[] args) - { - try - { - var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); - - Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); - foreach (var rate in rates) - { - Console.WriteLine(rate.ToString()); - } - } - catch (Exception e) - { - Console.WriteLine($"Could not retrieve exchange rates: '{e.Message}'."); - } - - Console.ReadLine(); - } - } -} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/.gitignore b/jobs/Backend/Task/src/ExchangeRateUpdater.App/.gitignore new file mode 100644 index 000000000..005d62573 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/.gitignore @@ -0,0 +1,44 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode/ + +# Rider +.idea/ + +# Visual Studio +.vs/ + +# Fleet +.fleet/ + +# Code Rush +.cr/ + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +*.sln +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn +appsettings.Development.json \ No newline at end of file diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/ExchangeRateUpdater.App.csproj b/jobs/Backend/Task/src/ExchangeRateUpdater.App/ExchangeRateUpdater.App.csproj new file mode 100644 index 000000000..73906c041 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/ExchangeRateUpdater.App.csproj @@ -0,0 +1,32 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + Always + + + diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/Extensions/ConverterExtension.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Extensions/ConverterExtension.cs new file mode 100644 index 000000000..ccf9ddc3f --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Extensions/ConverterExtension.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ExchangeRateUpdater.Domain.DTO; +using ExchangeRateUpdater.Domain.Models; + +namespace ExchangeRateUpdater.Extensions +{ + public static class ConverterExtension + { + public static IEnumerable ToExchangeRates(this ExchangeRatesDTO exchangeRates) + { + return exchangeRates.Rates.Select(r => new ExchangeRate(new Currency(r.CurrencyCode), new Currency("CZK"), r.Rate / r.Amount)); + } + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/Program.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Program.cs new file mode 100644 index 000000000..e93b228bb --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Program.cs @@ -0,0 +1,60 @@ +using ExchangeRateUpdater; +using ExchangeRateUpdater.Domain.Configurations; +using ExchangeRateUpdater.Providers; +using ExchangeRateUpdater.Services; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + + + +var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(config) + .CreateLogger(); +var host = new HostBuilder() + .ConfigureServices((services) => + { + + var settings = new ExchangeRateProviderSettings(); + config.GetSection("ExchangeRateProviderSettings").Bind(settings); + services.AddHttpClient("exchange", + (serviceProvider, client) => + { + client.BaseAddress = new Uri($"{settings.UrlBaseAPI}/{settings.UrlExchangeRate}"); + }) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new SocketsHttpHandler() + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15) + }; + }) + .SetHandlerLifetime(Timeout.InfiniteTimeSpan); + + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + }) + .UseSerilog() + .Build(); + +var exchangeRateProvider = host.Services.GetRequiredService(); + +for (int i = 0; i < 3; i++) +{ + var rates = await exchangeRateProvider.GetExchangeRatesAsync(TestingData.currencies); + foreach (var rate in rates) + { + Console.WriteLine(rate.ToString()); + } + Console.WriteLine("-----------------"); + +} + + + diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/Providers/ExchangeRateProvider.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Providers/ExchangeRateProvider.cs new file mode 100644 index 000000000..5b78dbd2f --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Providers/ExchangeRateProvider.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using System.Net.Http; +using ExchangeRateUpdater.Domain.Models; +using Microsoft.Extensions.Logging; +using ExchangeRateUpdater.Services; +using System.Threading.Tasks; +using ExchangeRateUpdater.Domain.DTO; +namespace ExchangeRateUpdater.Providers +{ + public class ExchangeRateProvider(ILogger _logger, IExchangeRateService exchangeRateService) : IExchangeRateProvider + { + /// + /// Should return exchange rates among the specified currencies that are defined by the source. But only those defined + /// by the source, do not return calculated exchange rates. E.g. if the source contains "CZK/USD" but not "USD/CZK", + /// do not return exchange rate "USD/CZK" with value calculated as 1 / "CZK/USD". If the source does not provide + /// some of the currencies, ignore them. + /// + public async Task> GetExchangeRatesAsync(IEnumerable currencies) + { + var result = new List(); + try + { + _logger.LogInformation("Started fetching exchange rates"); + var exchangeRates = await exchangeRateService.GetExchangeRateAsync(); + foreach (var currency in currencies) + { + _logger.LogInformation($"Adding exchange rate for {currency.Code}"); + result.AddRange(exchangeRates.Where(e => e.TargetCurrency.Code == currency.Code)); + _logger.LogInformation($"Added exchange rate for {currency.Code}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return null; + } + return result; + } + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/Providers/IExchangeRateProvider.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Providers/IExchangeRateProvider.cs new file mode 100644 index 000000000..7807667e0 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Providers/IExchangeRateProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ExchangeRateUpdater.Domain.DTO; +using ExchangeRateUpdater.Domain.Models; +using ExchangeRateUpdater.Services; + +namespace ExchangeRateUpdater.Providers +{ + public interface IExchangeRateProvider + { + public Task> GetExchangeRatesAsync(IEnumerable currencies); + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/Services/ExchangeRateService.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Services/ExchangeRateService.cs new file mode 100644 index 000000000..d64bdd414 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Services/ExchangeRateService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; +using ExchangeRateUpdater.Domain.Const; +using ExchangeRateUpdater.Domain.DTO; +using ExchangeRateUpdater.Domain.Models; +using ExchangeRateUpdater.Extensions; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater.Services +{ + public class ExchangeRateService(IHttpClientFactory clientFactory, ILogger _logger, IMemoryCache _memoryCache): IExchangeRateService + { + public async Task> GetExchangeRateAsync() + { + var cacheConstant = MemoryCacheConstants.ExchangeRateKey(nameof(ExchangeRate) ,DateTime.Now.Date); + if (_memoryCache.TryGetValue(cacheConstant, out IEnumerable result)) + { + _logger.LogInformation("Exchange rates retrieved from cache"); + return result; + } + using var client = clientFactory.CreateClient("exchange"); + _logger.LogInformation($"Sending GET request for getting exchange rates for {DateTime.Now:yyyy-MM-dd}"); + var response = await client.GetAsync(client.BaseAddress + $"?date={DateTime.Now:yyyy-MM-dd}"); + if (!response.IsSuccessStatusCode) + { + _logger.LogError($"Status code: {response.StatusCode}; Failed to fetch rates on {DateTime.Now:yyyy-MM-dd}"); + throw new Exception($"Status code: {response.StatusCode}; Failed to fetch rates on{DateTime.Now:yyyy-MM-dd}"); + } + result = (await response.Content.ReadFromJsonAsync()).ToExchangeRates(); + if (result.Any()) + { + _memoryCache.Set(cacheConstant, result, TimeSpan.FromSeconds(60)); + } + return result; + } + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/Services/IExchangeRateService.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Services/IExchangeRateService.cs new file mode 100644 index 000000000..4bfe297e1 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/Services/IExchangeRateService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ExchangeRateUpdater.Domain.DTO; +using ExchangeRateUpdater.Domain.Models; + +namespace ExchangeRateUpdater.Services +{ + public interface IExchangeRateService + { + public Task> GetExchangeRateAsync(); + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/TestingData.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.App/TestingData.cs new file mode 100644 index 000000000..66983c952 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/TestingData.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ExchangeRateUpdater.Domain.Models; + +namespace ExchangeRateUpdater +{ + public class TestingData + { + public 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") + }; + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.App/appsettings.json b/jobs/Backend/Task/src/ExchangeRateUpdater.App/appsettings.json new file mode 100644 index 000000000..4db5ccc0e --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.App/appsettings.json @@ -0,0 +1,32 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "ExchangeRateProviderSettings": { + "UrlBaseAPI": "https://api.cnb.cz", + "UrlExchangeRate": "cnbapi/exrates/daily" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "MinimumLevel": { + "Default": "Information" + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "C:\\Temp\\logs\\log-.txt", + "rollingInterval": "Day" + } + } + ] + } + } \ No newline at end of file diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Configurations/ExchangeRateProviderSettings.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Configurations/ExchangeRateProviderSettings.cs new file mode 100644 index 000000000..546180279 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Configurations/ExchangeRateProviderSettings.cs @@ -0,0 +1,8 @@ +namespace ExchangeRateUpdater.Domain.Configurations +{ + public class ExchangeRateProviderSettings + { + public string? UrlBaseAPI { get; set; } + public string? UrlExchangeRate { get; set; } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Const/MemoryCacheConstants.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Const/MemoryCacheConstants.cs new file mode 100644 index 000000000..dbaffe3ac --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Const/MemoryCacheConstants.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Domain.Const +{ + + public class MemoryCacheConstants + { + public static string ExchangeRateKey(string className, DateTime? date) + { + return $"{className}_DATE_{date.GetValueOrDefault().ToString("yyyy-MM-dd")}_KEY"; + } + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/DTO/ExchangeRateDTO.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/DTO/ExchangeRateDTO.cs new file mode 100644 index 000000000..53108a0e7 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/DTO/ExchangeRateDTO.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Domain.DTO +{ + public class ExchangeRateDTO + { + [JsonPropertyName("amount")] + public int Amount { get; init; } + + [JsonPropertyName("currencyCode")] + public string CurrencyCode { get; init; } + + [JsonPropertyName("currency")] + public string Currency { get; init; } + + [JsonPropertyName("country")] + public string Country { get; init; } + + [JsonPropertyName("rate")] + public decimal Rate { get; init; } + + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/DTO/ExchangeRatesDTO.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/DTO/ExchangeRatesDTO.cs new file mode 100644 index 000000000..dc23019a4 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/DTO/ExchangeRatesDTO.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Domain.DTO +{ + public class ExchangeRatesDTO + { + [JsonPropertyName("rates")] + public IEnumerable Rates { get; set; } = new List(); + } +} diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj new file mode 100644 index 000000000..0a8343153 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/ExchangeRateUpdater.Domain.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + + + + + diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Models/Currency.cs similarity index 88% rename from jobs/Backend/Task/Currency.cs rename to jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Models/Currency.cs index f375776f2..a46a1e902 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Models/Currency.cs @@ -1,4 +1,4 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater.Domain.Models { public class Currency { diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Models/ExchangeRate.cs similarity index 91% rename from jobs/Backend/Task/ExchangeRate.cs rename to jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Models/ExchangeRate.cs index 58c5bb10e..969a319c5 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Domain/Models/ExchangeRate.cs @@ -1,5 +1,6 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater.Domain.Models { + public class ExchangeRate { public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value) diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs new file mode 100644 index 000000000..161d71ead --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateProviderTests.cs @@ -0,0 +1,45 @@ +using ExchangeRateUpdater.Domain.Models; +using ExchangeRateUpdater.Providers; +using ExchangeRateUpdater.Services; +using Microsoft.Extensions.Logging; +using Moq; + + +namespace ExchangeRateUpdater.Tests +{ + public class ExchangeRateProviderTests + { + + private Mock> _loggerMock; + public readonly Mock exchangeRateServiceMock; + public ExchangeRateProvider exchangeRateProvider; + + public IEnumerable testExchangeRatesData { get; set; } + public IEnumerable testCurrenciesData { get; set; } + + public ExchangeRateProviderTests() + { + testExchangeRatesData = new List() + { + new ExchangeRate(new Currency("CZK"), new Currency("USD"), 0.89m), + new ExchangeRate(new Currency("CZK"), new Currency("EUR"), 0.86m), + new ExchangeRate(new Currency("CZK"), new Currency("AUD"), 1.37m), + new ExchangeRate(new Currency("CZK"), new Currency("GBP"), 0.0075m), + }; + testCurrenciesData = new List(TestingData.currencies); + + _loggerMock = new Mock>(); + exchangeRateServiceMock = new Mock(); + exchangeRateServiceMock.Setup(s => s.GetExchangeRateAsync()).ReturnsAsync(testExchangeRatesData); + } + + [Fact] + public async Task GetExchangeRatesProvider_NotNullAsync() + { + exchangeRateProvider = new ExchangeRateProvider(_loggerMock.Object, exchangeRateServiceMock.Object); + var result = await exchangeRateProvider.GetExchangeRatesAsync(testCurrenciesData); + Assert.NotNull(result); + Assert.True(result.Count() > 0); + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateServiceTests.cs b/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateServiceTests.cs new file mode 100644 index 000000000..03eb2154a --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateServiceTests.cs @@ -0,0 +1,65 @@ +using Castle.Core.Logging; +using ExchangeRateUpdater.Domain.DTO; +using ExchangeRateUpdater.Domain.Models; +using ExchangeRateUpdater.Extensions; +using ExchangeRateUpdater.Providers; +using ExchangeRateUpdater.Services; +using Microsoft.Extensions.Logging; +using Moq; + + +namespace ExchangeRateUpdater.Tests +{ + public class ExchangeRateServiceTests + { + + public readonly Mock exchangeRateServiceMock; + public ExchangeRatesDTO ExchangeRateDTOTestData { get; set; } = new ExchangeRatesDTO(); + public IEnumerable testExchangeRatesData { get; set; } + public IEnumerable exchangeRatesDTO { get; set; } + public IEnumerable testCurrenciesData { get; set; } + + public ExchangeRateServiceTests() + { + testExchangeRatesData = new List() + { + new ExchangeRate(new Currency("CZK"), new Currency("USD"), 0.89m), + new ExchangeRate(new Currency("CZK"), new Currency("EUR"), 0.86m), + new ExchangeRate(new Currency("CZK"), new Currency("AUD"), 1.37m), + new ExchangeRate(new Currency("CZK"), new Currency("GBP"), 0.0075m), + }; + + exchangeRatesDTO = new List() + { + new ExchangeRateDTO { Amount = 100, CurrencyCode = "USD", Currency = "US Dollar", Country = "United States", Rate = 1.0m}, + new ExchangeRateDTO { Amount = 200, CurrencyCode = "EUR", Currency = "Euro", Country = "European Union", Rate = 0.89m }, + new ExchangeRateDTO { Amount = 300, CurrencyCode = "GBP", Currency = "British Pound", Country = "United Kingdom", Rate = 0.75m}, + new ExchangeRateDTO { Amount = 1000, CurrencyCode = "JPY", Currency = "Japanese Yen", Country = "Japan", Rate = 109.65m}, + }; + ExchangeRateDTOTestData.Rates = exchangeRatesDTO; + testCurrenciesData = new List(TestingData.currencies); + + exchangeRateServiceMock = new Mock(); + exchangeRateServiceMock.Setup(s => s.GetExchangeRateAsync()).ReturnsAsync(testExchangeRatesData); + } + + + [Fact] + public async Task GetExchangeRates_NotNullAsync() + { + var result = await exchangeRateServiceMock.Object.GetExchangeRateAsync(); + + Assert.NotNull(result); + Assert.True(result.Count() > 0); + } + [Fact] + + public async Task ConvertsExchangeRates_OkAsync() + { + var result = ConverterExtension.ToExchangeRates(ExchangeRateDTOTestData); + + Assert.NotNull(result); + Assert.True(result.Count() > 0); + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj new file mode 100644 index 000000000..6dcbc0047 --- /dev/null +++ b/jobs/Backend/Task/src/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/src/Readme.md b/jobs/Backend/Task/src/Readme.md new file mode 100644 index 000000000..98a3a8d39 --- /dev/null +++ b/jobs/Backend/Task/src/Readme.md @@ -0,0 +1,49 @@ +# Mews backend task - Exchange Rate Updater solution +This is a repository with solution for Mews backend task. Said task is supposed to fetch exchange rates from Czech National banks for their national currency. +## Requirements +For sucessfull running of the solution certain packages are required: +* Serilog - consists of several packages such as Serilog sinks for different kinds of outputs (console and file in this solution), Serilog hosting and configuration for configuring logging via appsettings json file +* Microsoft.Extensions - extensions package that consists of different package needed for setting up of the program.cs class: + * Microsoft.Extensions.Caching.Memory - package for in memory caching of returned exchange rates + * Microsoft.Extensions.Configuration - package for binding appsettings json to a custom settings class + * Microsoft.Extensions.Logging - package for logger used in Serilog + * Microsoft.Extensions.Http - package for http client for sending get requests to the bank API +## Logging +Logging in this solution is done via Serilog which allows the user to setup a logger with just few lines of code. It allows the user to have logging done directly to different outpouts such as: +* Message broker +* Console +* File + +``` +var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(config) + .CreateLogger(); +var host = new HostBuilder().UseSerilog(); +``` +## How To Run +Use dotnet run to run the solution +## Projects +### [App](ExchangeRateUpdater.App) +Main part of the project. Inside of it are [service](ExchangeRateUpdater.App/Services/ExchangeRateService.cs) and [provider](ExchangeRateUpdater.App/Providers/ExchangeRateProvider.cs) alongside their interfaces for the fetching of exchange rates. + +### [Domain](ExchangeRateUpdater.Domain) +Consists of all supporting classes ExchangeRateUpdater.App requires such as configuration, DTO, model, etc classes. +### [Tests](ExchangeRateUpdater.Tests) +Consits of tests for ExchangeRateUpdater.App service and provider. + +## Configuration +Configuration is stored in [appsettings.json](ExchangeRateUpdater.App/appsettings.json) file which is parsed inside [settings](ExchangeRateUpdater.Domain/Configurations/ExchangeRateProviderSettings.cs) file. It allows the user to enter the API endpoint where exchange rates can be found. + +## Testing + +Running dotnet test inside [Tests](ExchangeRateUpdater.Tests/) directory will execute the application tests. + +## Possible improvements +* Caching - Caching could be implemented in Redis instead of in memory caching +* Docker, CI/CD - Was not required in the task but it is a must have thing in every modern production environment +* Performance tracing and metrics - Enterprise solutions require tracing of how solution is performing, Grafana could be used for creating easy to read dashboards for tracking. + +