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 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/ExchangeRateUpdater/.gitignore b/jobs/Backend/Task/ExchangeRateUpdater/.gitignore new file mode 100644 index 000000000..440219560 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/.gitignore @@ -0,0 +1,49 @@ +# Archivos de sistema operativo +.DS_Store +Thumbs.db + +# Configuración de Visual Studio +.vs/ +*.vcxproj.user +*.suo +*.user +*.sln.docstates + +# Carpetas de compilación y temporales +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Dd]ebug*/ +[Rr]elease*/ +x64/ +x86/ +[Aa]rm/ +[Aa]rm64/ +*.log +*.tmp + +# Archivos de paquetes y dependencias +packages/ +*.nupkg +project.lock.json +project.fragment.lock.json + +# Archivos de publicación +*.publishsettings +*.azurePubxml +*.azurePubxml.user + +# Configuración de IDEs +.idea/ +.vscode/ + +# Archivos de pruebas +TestResults/ +*.trx +*.coverage + +# Archivos sensibles +*.config +*.env +*.key +*.pem diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/ExchangeRateUpdater.Business.csproj b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/ExchangeRateUpdater.Business.csproj new file mode 100644 index 000000000..ee753b73e --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/ExchangeRateUpdater.Business.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/Interfaces/IExchangeRateService.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/Interfaces/IExchangeRateService.cs new file mode 100644 index 000000000..3e9c502b9 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/Interfaces/IExchangeRateService.cs @@ -0,0 +1,8 @@ +using ExchangeRateUpdater.Data.Responses; +using ExchangeRateUpdater.Models.Requests; + +namespace ExchangeRateUpdater.Business.Interfaces; +public interface IExchangeRateService +{ + Task> GetExchangeRates(IEnumerable currencies, DateTime date, CancellationToken cancellationToken); +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/Services/ExchangeRateService.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/Services/ExchangeRateService.cs new file mode 100644 index 000000000..f223571e0 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Business/Services/ExchangeRateService.cs @@ -0,0 +1,43 @@ +using ExchangeRateUpdater.Models.Requests; +using ExchangeRateUpdater.Data.Interfaces; +using ExchangeRateUpdater.Data.Responses; +using ExchangeRateUpdater.Business.Interfaces; +using ExchangeRateUpdater.Models.Models; +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater.Business.Services; +public class ExchangeRateService : IExchangeRateService +{ + private readonly IExchangeRateRepository _exchangeRatesRepository; + private readonly ILogger _logger; + public ExchangeRateService(IExchangeRateRepository exchangeRatesRepository, ILogger logger) + { + _exchangeRatesRepository = exchangeRatesRepository; + _logger = logger; + } + public async Task> GetExchangeRates(IEnumerable currencies, DateTime date, CancellationToken cancellationToken) + { + var exchangeRatesByCurrency = new List(); + + //Get all the rates for a specified date + var exchangeRates = await _exchangeRatesRepository.GetExchangeRatesByDateAsync(date, cancellationToken); + + //Get only the requested ones + var requestedExchangeRates = exchangeRates.Where(p => currencies + .Any(e => e.ToString() == p.ToString())) + .ToList(); + + _logger.LogInformation($"{requestedExchangeRates.Count}/{currencies.ToList().Count} exchange rate(s) have been obtained."); + //Map to a new object that simplifies reading the response + return requestedExchangeRates.Select(rate => MapToResult(rate)).ToList(); + } + + private ExchangeRateResultDto MapToResult(ExchangeRate rate) + { + return new ExchangeRateResultDto( + new Currency(rate.SourceCurrency.Code), + new Currency(rate.TargetCurrency.Code), + rate.Value, + rate.Date); + } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/ExchangeRateUpdater.Data.csproj b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/ExchangeRateUpdater.Data.csproj new file mode 100644 index 000000000..49bfa6c4d --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/ExchangeRateUpdater.Data.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Interfaces/IExchangeRateCacheRepository.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Interfaces/IExchangeRateCacheRepository.cs new file mode 100644 index 000000000..2c85cdc18 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Interfaces/IExchangeRateCacheRepository.cs @@ -0,0 +1,9 @@ + +using ExchangeRateUpdater.Data.Responses; + +namespace ExchangeRateUpdater.Data.Interfaces; +public interface IExchangeRateCacheRepository +{ + ExchangeRatesResponseDto GetExchangeRates(DateTime date); + void SetExchangeRates(ExchangeRatesResponseDto rates); +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Interfaces/IExchangeRateRepository.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Interfaces/IExchangeRateRepository.cs new file mode 100644 index 000000000..b0a6f4eb1 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Interfaces/IExchangeRateRepository.cs @@ -0,0 +1,8 @@ + +using ExchangeRateUpdater.Data.Responses; + +namespace ExchangeRateUpdater.Data.Interfaces; +public interface IExchangeRateRepository +{ + Task> GetExchangeRatesByDateAsync(DateTime date, CancellationToken cancellationToken); +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Repositories/ExchangeRateCacheRepository.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Repositories/ExchangeRateCacheRepository.cs new file mode 100644 index 000000000..a3daf9210 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Repositories/ExchangeRateCacheRepository.cs @@ -0,0 +1,60 @@ +using ExchangeRateUpdater.Data.Interfaces; +using ExchangeRateUpdater.Data.Responses; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + + +namespace ExchangeRateUpdater.Data.Repositories; +public class ExchangeRateCacheRepository : IExchangeRateCacheRepository +{ + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + + public ExchangeRateCacheRepository(IMemoryCache cache, ILogger logger) + { + _cache = cache; + _logger = logger; + } + + public ExchangeRatesResponseDto GetExchangeRates(DateTime date) + { + var result = new ExchangeRatesResponseDto() { Rates = new List()}; + var key = date.ToString("yyyy-MM-dd"); + + if (_cache.TryGetValue(key, out ExchangeRatesResponseDto ratesCached)) + { + _logger.LogInformation($"{ratesCached.Rates.Count} retrieved from cache"); + result.Rates = ratesCached.Rates.Where(t => t.ValidFor == date).ToList(); + } + + return result; + } + + public void SetExchangeRates(ExchangeRatesResponseDto rates) + { + try + { + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(TimeSpan.FromDays(1)); //Keep cache for 1 day max + + //Since weekends there's no rates and the api always returns the las data avaiable, + //the rates in cache have the actual date and not the requested + var key = rates.Rates[0].ValidFor.ToString("yyyy-MM-dd"); + + if (!_cache.TryGetValue(key, out _)) + { + _logger.LogInformation($"{rates.Rates.Count} have been saved on cache"); + _cache.Set(key, rates, cacheEntryOptions); + } + + + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + throw new Exception($"There was an error while saving in cache. Error: {ex.Message}"); + } + } +} + diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Repositories/ExchangeRateRepository.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Repositories/ExchangeRateRepository.cs new file mode 100644 index 000000000..078ddfc33 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Repositories/ExchangeRateRepository.cs @@ -0,0 +1,86 @@ +using ExchangeRateUpdater.Data.Interfaces; +using ExchangeRateUpdater.Data.Responses; +using ExchangeRateUpdater.Models.Models; +using Flurl; +using Flurl.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater.Data.Repositories; +public class ExchangeRateRepository : IExchangeRateRepository +{ + private readonly IConfiguration _configuration; + private readonly IExchangeRateCacheRepository _cacheRepository; // Instancia de IMemoryCache + private readonly ILogger _logger; + + public ExchangeRateRepository(IConfiguration configuration, IExchangeRateCacheRepository cacheRepository, ILogger logger) + { + _configuration = configuration; + _cacheRepository = cacheRepository; + _logger = logger; + } + public async Task> GetExchangeRatesByDateAsync(DateTime date, CancellationToken cancellationToken) + { + ExchangeRatesResponseDto? exchangeRates; + + // Try get exchange rates from caché first + exchangeRates = _cacheRepository.GetExchangeRates(date); + + //If no results from cache, get the data from the client + if (!exchangeRates.Rates.Any()) + { + exchangeRates = await GetExchangeRates(date, cancellationToken); + + //Set the data in cache + _cacheRepository.SetExchangeRates(exchangeRates); + } + + var result = exchangeRates.Rates.Select(rate => MapToExchangeRate(rate)).ToList(); + + return result; + } + + private async Task GetExchangeRates(DateTime date, CancellationToken cancellationToken) + { + var resul = new ExchangeRatesResponseDto(); + + try + { + using (var httpClient = new HttpClient()) + { + if (string.IsNullOrEmpty(_configuration["ExchangeRateUrl"])) + { + _logger.LogError("Czech National Bank Url not defined."); + throw new ApplicationException("Czech National Bank Url not defined."); + } + + string baseUrl = _configuration["ExchangeRateUrl"] ; + + resul = await baseUrl + .SetQueryParams(new + { + date = date.ToString("yyyy-MM-dd"), + lang = "EN" + }) + .GetJsonAsync(); + } + } + catch (FlurlHttpException ex) + { + _logger.LogError(ex.Message); + throw new ApplicationException(ex.Message); + } + + _logger.LogInformation($"{resul.Rates.Count} exchange rates have been obtained."); + return resul; + } + + private ExchangeRate MapToExchangeRate(ExchangeRateDto exchangeRateDto) + { + return new ExchangeRate( + new Currency("CZK"), //CNB only retrieve rates for CZK + new Currency(exchangeRateDto.CurrencyCode), + exchangeRateDto.Rate, + exchangeRateDto.ValidFor); + } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRate.cs new file mode 100644 index 000000000..470749dbe --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRate.cs @@ -0,0 +1,31 @@ +using ExchangeRateUpdater.Models.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Data.Responses; +public class ExchangeRate +{ + public ExchangeRate(Currency sourceCurrency, Currency targetCurrency, decimal value, DateTime date) + { + SourceCurrency = sourceCurrency; + TargetCurrency = targetCurrency; + Value = value; + Date = date; + } + + public Currency SourceCurrency { get; } + + public Currency TargetCurrency { get; } + + public decimal Value { get; } + + public DateTime Date { get; } + + public override string ToString() + { + return $"{SourceCurrency}/{TargetCurrency}"; + } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRateDto.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRateDto.cs new file mode 100644 index 000000000..7a16f534e --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRateDto.cs @@ -0,0 +1,12 @@ + +namespace ExchangeRateUpdater.Data.Responses; +public class ExchangeRateDto +{ + public DateTime ValidFor { get; set; } + public int Order { get; set; } + public string Country { get; set; } + public string Currency { get; set; } + public int Amount { get; set; } + public string CurrencyCode { get; set; } + public decimal Rate { get; set; } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRateResponseDto.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRateResponseDto.cs new file mode 100644 index 000000000..6d3c2dcad --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/ExchangeRateResponseDto.cs @@ -0,0 +1,8 @@ +using Newtonsoft.Json; + +namespace ExchangeRateUpdater.Data.Responses; +public class ExchangeRatesResponseDto +{ + [JsonProperty("rates")] + public List Rates { get; set; } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/RateResultDto.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/RateResultDto.cs new file mode 100644 index 000000000..b9ea91e12 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Data/Responses/RateResultDto.cs @@ -0,0 +1,18 @@ +using ExchangeRateUpdater.Models.Models; + +namespace ExchangeRateUpdater.Data.Responses; +public class ExchangeRateResultDto +{ + public ExchangeRateResultDto(Currency sourceCurrency, Currency targetCurrency, decimal value, DateTime date) + { + Currencies = $"{sourceCurrency} / {targetCurrency}"; + Value = value; + Date = date; + } + + public string Currencies { get; } + + public decimal Value { get; } + + public DateTime Date { get; } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/ExchangeRateUpdater.Models.csproj b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/ExchangeRateUpdater.Models.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/ExchangeRateUpdater.Models.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Models/Currency.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Models/Currency.cs new file mode 100644 index 000000000..2518d5e7a --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Models/Currency.cs @@ -0,0 +1,18 @@ +namespace ExchangeRateUpdater.Models.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/ExchangeRateUpdater/ExchangeRateUpdater.Models/Requests/ExchangeRateRequest.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Requests/ExchangeRateRequest.cs new file mode 100644 index 000000000..f44f7579c --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Requests/ExchangeRateRequest.cs @@ -0,0 +1,14 @@ +using ExchangeRateUpdater.Models.Models; + +namespace ExchangeRateUpdater.Models.Requests; +public class ExchangeRateRequest +{ + public Currency SourceCurrency { get; set; } + public Currency TargetCurrency { get; set; } + + public override string ToString() + { + return $"{SourceCurrency}/{TargetCurrency}"; + } + +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Requests/ExchangeRateRequestDto.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Requests/ExchangeRateRequestDto.cs new file mode 100644 index 000000000..f784d3b86 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Models/Requests/ExchangeRateRequestDto.cs @@ -0,0 +1,7 @@ + +namespace ExchangeRateUpdater.Models.Requests; +public class ExchangeRateRequestDto +{ + public DateTime Date { get; set; } + public List ExchangeRatesDetails { get; set; } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeControllerTests.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeControllerTests.cs new file mode 100644 index 000000000..3f02edb94 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeControllerTests.cs @@ -0,0 +1,125 @@ +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater.Tests; + +[TestFixture] +public class ExchangeControllerTests +{ + private ExchangeRatesController _controller; + private Mock _mockService; + private Mock> _mockLogger; + + [SetUp] + public void SetUp() + { + _mockService = new Mock(); + _mockLogger = new Mock>(); + _controller = new ExchangeRatesController(_mockService.Object, _mockLogger.Object); + } + + [Test] + public async Task GivenNullExchangeRate_ShouldReturnBadRequest() + { + // Arrange + var exchangeRatesToRequest = new ExchangeRateRequestDto() { Date = new DateTime(2025, 01, 20) }; + + // Act + var result = await _controller.GetDailyExchangeRatesAsync(exchangeRatesToRequest, new CancellationToken()); + + // Assert + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); // Verifica que sea un BadRequestObjectResult + Assert.AreEqual(400, badRequestResult.StatusCode); // Verifica que el código de estado sea 400 + Assert.AreEqual("Exchange rate(s) cannot be empty or null", badRequestResult.Value); // Verifica el mensaje + } + + [Test] + public async Task GivenEmptyExchangeRate_ShouldReturnBadRequest() + { + // Arrange + var exchangeRatesToRequest = new ExchangeRateRequestDto() + { + Date = new DateTime(2025, 01, 20), + ExchangeRatesDetails = new List() + }; + + // Act + var result = await _controller.GetDailyExchangeRatesAsync(exchangeRatesToRequest, new CancellationToken()); + + // Assert + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); // Verifica que sea un BadRequestObjectResult + Assert.AreEqual(400, badRequestResult.StatusCode); // Verifica que el código de estado sea 400 + Assert.AreEqual("Exchange rate(s) cannot be empty or null", badRequestResult.Value); // Verifica el mensaje + } + + [Test] + public async Task GivenValidExchangeRate_ShouldReturnExchangeRate() + { + // Arrange + var request = new ExchangeRateRequestDto() + { + Date = new DateTime(2025, 01, 20), + ExchangeRatesDetails = new List() + { + new ExchangeRateRequest() + { + SourceCurrency = new Currency("CZK"), + TargetCurrency = new Currency("USD") + } + } + }; + var resultado = new List() { + new ExchangeRateResultDto(targetCurrency: new Currency("USD"), sourceCurrency: new Currency("CZK"), value:10, date: new DateTime(2025, 01, 20)) + }; + + _mockService.Setup(s => s.GetExchangeRates(request.ExchangeRatesDetails, request.Date, new CancellationToken())).ReturnsAsync(resultado); + + //// Act + var result = await _controller.GetDailyExchangeRatesAsync(request, new CancellationToken()); + + // Assert + var OkResult = result as OkObjectResult; + Assert.IsNotNull(OkResult); + Assert.AreEqual(200, OkResult.StatusCode); + + var rates = OkResult.Value as List; + Assert.IsNotNull(rates); + Assert.IsNotEmpty(rates); + Assert.IsTrue(rates.Any()); + } + + [Test] + public async Task GivenInalidExchangeRate_ShouldReturnEmptyList() + { + // Arrange + var request = new ExchangeRateRequestDto() + { + Date = new DateTime(2025, 01, 20), + ExchangeRatesDetails = new List() + { + new ExchangeRateRequest() + { + SourceCurrency = new Currency("CZK"), + TargetCurrency = new Currency("ABC") + } + } + }; + var resultado = new List(); + + _mockService.Setup(s => s.GetExchangeRates(request.ExchangeRatesDetails, request.Date, new CancellationToken())).ReturnsAsync(resultado); + + //// Act + var result = await _controller.GetDailyExchangeRatesAsync(request, new CancellationToken()); + + // Assert + var OkResult = result as OkObjectResult; + Assert.IsNotNull(OkResult); + Assert.AreEqual(200, OkResult.StatusCode); + + var rates = OkResult.Value as List; + Assert.IsNotNull(rates); + Assert.IsEmpty(rates); + Assert.IsFalse(rates.Any()); + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeRateServicesTests.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeRateServicesTests.cs new file mode 100644 index 000000000..28d1f2f7f --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeRateServicesTests.cs @@ -0,0 +1,80 @@ + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace ExchangeRateUpdater.Tests; + +[TestFixture] +public class ExchangeRateServicesTests +{ + private Mock _mockRepository; + private Mock> _mockLogger; + private ExchangeRateService _service; + + [SetUp] + public void SetUp() + { + _mockRepository = new Mock(); + _mockLogger = new Mock >(); + _service = new ExchangeRateService(_mockRepository.Object, _mockLogger.Object); + } + + + [Test] + public async Task GivenInalidExchangeRate_ShouldReturnEmptyList() + { + // Arrange + var currencies = new List() + { + new ExchangeRateRequest() + { + SourceCurrency = new Currency("CZK"), + TargetCurrency = new Currency("ABC") + } + }; + + var resultado = new List(); + _mockRepository.Setup(s => s.GetExchangeRatesByDateAsync(new DateTime(2025, 01, 01), new CancellationToken())).ReturnsAsync(resultado); + + //// Act + var result = await _service.GetExchangeRates(currencies, new DateTime(2025, 01, 01), new CancellationToken()); + + // Assert + Assert.IsNotNull(result); + Assert.IsEmpty(result); + Assert.IsFalse(result.Any()); + } + + [Test] + public async Task GivenValidExchangeRate_ShouldReturnExchangeRate() + { + // Arrange + var currencies = new List() + { + new ExchangeRateRequest() + { + SourceCurrency = new Currency("CZK"), + TargetCurrency = new Currency("USD") + } + }; + + var resultado = new List() + { + new ExchangeRate(sourceCurrency: new Currency("CZK"), + targetCurrency: new Currency("USD"), + value: 24, + date: new DateTime(2025, 01, 20)) + }; + + _mockRepository.Setup(s => s.GetExchangeRatesByDateAsync(new DateTime(2025, 01, 01), new CancellationToken())).ReturnsAsync(resultado); + + //// Act + var result = await _service.GetExchangeRates(currencies, new DateTime(2025, 01, 01), new CancellationToken()); + + // Assert + Assert.IsNotNull(result); + Assert.IsNotEmpty(result); + Assert.IsTrue(result.Any()); + Assert.AreEqual("CZK / USD", result[0].Currencies); + } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj new file mode 100644 index 000000000..c49837f1f --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/ExchangeRateUpdater.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/Usings.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/Usings.cs new file mode 100644 index 000000000..7c746b1b3 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.Tests/Usings.cs @@ -0,0 +1,9 @@ +global using Moq; +global using ExchangeRateUpdater.Business.Interfaces; +global using ExchangeRateUpdater.Controllers; +global using ExchangeRateUpdater.Models.Models; +global using ExchangeRateUpdater.Data.Responses; +global using ExchangeRateUpdater.Models.Requests; +global using Microsoft.AspNetCore.Mvc; +global using ExchangeRateUpdater.Data.Interfaces; +global using ExchangeRateUpdater.Business.Services; \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Controllers/ExchangeRatesController.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Controllers/ExchangeRatesController.cs new file mode 100644 index 000000000..43feb155e --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Controllers/ExchangeRatesController.cs @@ -0,0 +1,42 @@ +using ExchangeRateUpdater.Business.Interfaces; +using ExchangeRateUpdater.Models.Requests; +using Microsoft.AspNetCore.Mvc; + +namespace ExchangeRateUpdater.Controllers; + +[ApiController] +[Route("/api/exchangerates")] +public class ExchangeRatesController : ControllerBase +{ + private readonly IExchangeRateService _exchangeRateService; + private readonly ILogger _logger; + + + public ExchangeRatesController(IExchangeRateService exchangeRateService, ILogger logger) + { + _exchangeRateService = exchangeRateService; + _logger = logger; + } + /// + /// Gets the exchange rates for the currencies requested and the specified date + /// Uses POST to get an objetct (containing a list) as parameter + /// + /// + /// + /// + [HttpPost("getExchangeRates")] + public async Task GetDailyExchangeRatesAsync([FromBody] ExchangeRateRequestDto exchangeRatesRequest, CancellationToken cancellationToken) + { + if (exchangeRatesRequest.ExchangeRatesDetails is null || !exchangeRatesRequest.ExchangeRatesDetails.Any()) + { + _logger.LogError("Exchange rate(s) cannot be empty or null"); + return BadRequest("Exchange rate(s) cannot be empty or null"); + } + + var result = await _exchangeRateService.GetExchangeRates(exchangeRatesRequest.ExchangeRatesDetails, exchangeRatesRequest.Date, cancellationToken); + + _logger.LogInformation("Exchange rates successfully obtained."); + return Ok(result); + } + +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/ExchangeRateUpdater.Api.csproj b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/ExchangeRateUpdater.Api.csproj new file mode 100644 index 000000000..7b792eaee --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/ExchangeRateUpdater.Api.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Program.cs b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Program.cs new file mode 100644 index 000000000..ea8a0f7be --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Program.cs @@ -0,0 +1,49 @@ +using ExchangeRateUpdater.Business.Interfaces; +using ExchangeRateUpdater.Business.Services; +using ExchangeRateUpdater.Data.Interfaces; +using ExchangeRateUpdater.Data.Repositories; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMemoryCache(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Exchange Rate Updater", + Version = "v1", + Description = "This API can be used to get the conversion rates between CZK currency and many others" + }); +}); + +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +builder.Logging.AddDebug(); +builder.Logging.SetMinimumLevel(LogLevel.Information); + +var serviceProvider = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build() + ); + + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Properties/launchSettings.json b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Properties/launchSettings.json new file mode 100644 index 000000000..7d3e34914 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5276" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ExchangeRateUpdater.Api": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7276;http://localhost:5276" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52301", + "sslPort": 44368 + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/appsettings.Development.json b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/appsettings.json b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/appsettings.json new file mode 100644 index 000000000..5cb2a407c --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.api/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ExchangeRateUrl": "https://api.cnb.cz/cnbapi/exrates/daily" +} diff --git a/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.sln new file mode 100644 index 000000000..073406b68 --- /dev/null +++ b/jobs/Backend/Task/ExchangeRateUpdater/ExchangeRateUpdater.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34902.65 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater.Api", "ExchangeRateUpdater.api\ExchangeRateUpdater.Api.csproj", "{9CAC3044-FCBC-41D8-93A6-294F916292EE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater.Data", "ExchangeRateUpdater.Data\ExchangeRateUpdater.Data.csproj", "{98DEBA7B-E32C-44EF-BF6D-966FD66C58F3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater.Business", "ExchangeRateUpdater.Business\ExchangeRateUpdater.Business.csproj", "{73CD85B9-7C24-424D-90C7-4702706AD48A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExchangeRateUpdater.Models", "ExchangeRateUpdater.Models\ExchangeRateUpdater.Models.csproj", "{625CC8EA-2E4A-4308-B041-CA77AEC2186A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater.Tests", "ExchangeRateUpdater.Tests\ExchangeRateUpdater.Tests.csproj", "{1060BFDF-C994-47E0-9FC5-79E9813BEE5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9CAC3044-FCBC-41D8-93A6-294F916292EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CAC3044-FCBC-41D8-93A6-294F916292EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CAC3044-FCBC-41D8-93A6-294F916292EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CAC3044-FCBC-41D8-93A6-294F916292EE}.Release|Any CPU.Build.0 = Release|Any CPU + {98DEBA7B-E32C-44EF-BF6D-966FD66C58F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98DEBA7B-E32C-44EF-BF6D-966FD66C58F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98DEBA7B-E32C-44EF-BF6D-966FD66C58F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98DEBA7B-E32C-44EF-BF6D-966FD66C58F3}.Release|Any CPU.Build.0 = Release|Any CPU + {73CD85B9-7C24-424D-90C7-4702706AD48A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73CD85B9-7C24-424D-90C7-4702706AD48A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73CD85B9-7C24-424D-90C7-4702706AD48A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73CD85B9-7C24-424D-90C7-4702706AD48A}.Release|Any CPU.Build.0 = Release|Any CPU + {625CC8EA-2E4A-4308-B041-CA77AEC2186A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {625CC8EA-2E4A-4308-B041-CA77AEC2186A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {625CC8EA-2E4A-4308-B041-CA77AEC2186A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {625CC8EA-2E4A-4308-B041-CA77AEC2186A}.Release|Any CPU.Build.0 = Release|Any CPU + {1060BFDF-C994-47E0-9FC5-79E9813BEE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1060BFDF-C994-47E0-9FC5-79E9813BEE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1060BFDF-C994-47E0-9FC5-79E9813BEE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1060BFDF-C994-47E0-9FC5-79E9813BEE5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B38D1E9E-34E8-462E-B003-4E2921260A09} + 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(); - } - } -}