From af132dad82dd2277a99d322197452786c9b9d1b8 Mon Sep 17 00:00:00 2001 From: PedroAlvarenga Date: Fri, 13 Feb 2026 02:05:34 +0000 Subject: [PATCH] Exchange Rate Provider for CNB - Pedro Alvarenga --- .../ExchangeRateAPIIntegrationTest.csproj | 25 +++++++++ .../ExchangeRateControllerTest.cs | 25 +++++++++ .../Adapters/ExchangeRateAdapter.cs | 25 +++++++++ .../Controllers/ExchangeRateController.cs | 40 ++++++++++++++ .../ExchangeRateAPI/ExchangeRateAPI.csproj | 20 +++++++ .../ExchangeRateAPI/ExchangeRateAPI.http | 6 +++ .../Models/ExchangeRateResponse.cs | 16 ++++++ jobs/Backend/ExchangeRateAPI/Program.cs | 31 +++++++++++ .../Properties/launchSettings.json | 38 +++++++++++++ .../appsettings.Development.json | 8 +++ jobs/Backend/ExchangeRateAPI/appsettings.json | 9 ++++ .../ExchangeRateAPIUnitTest.csproj | 26 +++++++++ .../ExchangeRateControllerTest.cs | 38 +++++++++++++ .../ExchangeRateProviderTest.cs | 46 ++++++++++++++++ .../ExchangeRateUpdaterUnitTest.csproj | 26 +++++++++ jobs/Backend/Task/{ => Business}/Currency.cs | 2 +- .../Task/{ => Business}/ExchangeRate.cs | 2 +- .../Task/Business/ExchangeRateProvider.cs | 53 +++++++++++++++++++ .../Task/Business/IExchangeRateProvider.cs | 10 ++++ .../Task/Business/Models/ExchangeRateModel.cs | 13 +++++ .../Models/ExchangeRateResponseModel.cs | 11 ++++ .../Parameters/ExchangeRateParameters.cs | 18 +++++++ .../Parameters/IExchangeRateParameters.cs | 12 +++++ .../Business/Services/ExchangeRateService.cs | 48 +++++++++++++++++ .../Business/Services/IExchangeRateService.cs | 11 ++++ jobs/Backend/Task/ExchangeRateProvider.cs | 19 ------- jobs/Backend/Task/ExchangeRateUpdater.csproj | 5 ++ jobs/Backend/Task/ExchangeRateUpdater.sln | 33 +++++++++++- jobs/Backend/Task/Program.cs | 9 ++-- 29 files changed, 599 insertions(+), 26 deletions(-) create mode 100644 jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateAPIIntegrationTest.csproj create mode 100644 jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateControllerTest.cs create mode 100644 jobs/Backend/ExchangeRateAPI/Adapters/ExchangeRateAdapter.cs create mode 100644 jobs/Backend/ExchangeRateAPI/Controllers/ExchangeRateController.cs create mode 100644 jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.csproj create mode 100644 jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.http create mode 100644 jobs/Backend/ExchangeRateAPI/Models/ExchangeRateResponse.cs create mode 100644 jobs/Backend/ExchangeRateAPI/Program.cs create mode 100644 jobs/Backend/ExchangeRateAPI/Properties/launchSettings.json create mode 100644 jobs/Backend/ExchangeRateAPI/appsettings.Development.json create mode 100644 jobs/Backend/ExchangeRateAPI/appsettings.json create mode 100644 jobs/Backend/ExchangeRateAPITest/ExchangeRateAPIUnitTest.csproj create mode 100644 jobs/Backend/ExchangeRateAPITest/ExchangeRateControllerTest.cs create mode 100644 jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateProviderTest.cs create mode 100644 jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateUpdaterUnitTest.csproj rename jobs/Backend/Task/{ => Business}/Currency.cs (89%) rename jobs/Backend/Task/{ => Business}/ExchangeRate.cs (92%) create mode 100644 jobs/Backend/Task/Business/ExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Business/IExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Business/Models/ExchangeRateModel.cs create mode 100644 jobs/Backend/Task/Business/Models/ExchangeRateResponseModel.cs create mode 100644 jobs/Backend/Task/Business/Parameters/ExchangeRateParameters.cs create mode 100644 jobs/Backend/Task/Business/Parameters/IExchangeRateParameters.cs create mode 100644 jobs/Backend/Task/Business/Services/ExchangeRateService.cs create mode 100644 jobs/Backend/Task/Business/Services/IExchangeRateService.cs delete mode 100644 jobs/Backend/Task/ExchangeRateProvider.cs diff --git a/jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateAPIIntegrationTest.csproj b/jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateAPIIntegrationTest.csproj new file mode 100644 index 0000000000..fbbc859bc1 --- /dev/null +++ b/jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateAPIIntegrationTest.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateControllerTest.cs b/jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateControllerTest.cs new file mode 100644 index 0000000000..f636185de8 --- /dev/null +++ b/jobs/Backend/ExchangeAPIIntegrationTest/ExchangeRateControllerTest.cs @@ -0,0 +1,25 @@ +using ExchangeRateAPI.Controllers; +using ExchangeRateAPI.Models; +using ExchangeRateUpdater.Business; + +namespace ExchangeRateAPIIntegrationTest +{ + public class ExchangeRateControllerTest + { + private readonly ExchangeRateController __ExchangeRateController; + + public ExchangeRateControllerTest() + { + __ExchangeRateController = new ExchangeRateController(new ExchangeRateProvider()); + } + + [Fact] + public async Task ExchangeRateController_GetAsync_ShouldReturnResultsAsync() + { + List _Response = await __ExchangeRateController.GetAsync(); + + Assert.NotNull(_Response); + Assert.Equal(6, _Response.Count); + } + } +} diff --git a/jobs/Backend/ExchangeRateAPI/Adapters/ExchangeRateAdapter.cs b/jobs/Backend/ExchangeRateAPI/Adapters/ExchangeRateAdapter.cs new file mode 100644 index 0000000000..401c91b084 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/Adapters/ExchangeRateAdapter.cs @@ -0,0 +1,25 @@ +using ExchangeRateAPI.Models; +using ExchangeRateUpdater.Business; + +namespace ExchangeRateAPI.Adapters +{ + public static class ExchangeRateAdapter + { + public static List ConvertToCurrencyList(List currencyCodes) + => currencyCodes.Select(currencyCode => new Currency(currencyCode)).ToList(); + + private static ExchangeRateResponse ToExchangeRateResponse(this ExchangeRate exchangeRate) + { + return new ExchangeRateResponse + { + SourceCurrency = exchangeRate.SourceCurrency.Code, + TargetCurrency = exchangeRate.TargetCurrency.Code, + Value = exchangeRate.Value + }; + } + + public static List ToExchangeRateResponses(this List exchangeRates) + => exchangeRates.Select(exchangeRate => exchangeRate.ToExchangeRateResponse()).ToList(); + + } +} diff --git a/jobs/Backend/ExchangeRateAPI/Controllers/ExchangeRateController.cs b/jobs/Backend/ExchangeRateAPI/Controllers/ExchangeRateController.cs new file mode 100644 index 0000000000..1db35f562d --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/Controllers/ExchangeRateController.cs @@ -0,0 +1,40 @@ +using ExchangeRateAPI.Adapters; +using ExchangeRateAPI.Models; +using ExchangeRateUpdater.Business; +using Microsoft.AspNetCore.Mvc; + +namespace ExchangeRateAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + public class ExchangeRateController : ControllerBase + { + private readonly IExchangeRateProvider __ExchangeRateProvider; + + private readonly List __Currencies = + [ + "AUD", + "USD", + "EUR", + "CZK", + "JPY", + "KES", + "RUB", + "THB", + "TRY", + "XYZ" + ]; + + public ExchangeRateController(IExchangeRateProvider exchangeRateProvider) + { + __ExchangeRateProvider = exchangeRateProvider; + } + + [HttpGet(Name = "GetExchangeRates")] + public async Task> GetAsync() + { + List _Response = (await __ExchangeRateProvider.GetExchangeRatesAsync(ExchangeRateAdapter.ConvertToCurrencyList(__Currencies))).ToExchangeRateResponses(); + return _Response; + } + } +} diff --git a/jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.csproj b/jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.csproj new file mode 100644 index 0000000000..39205ab9b5 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + diff --git a/jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.http b/jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.http new file mode 100644 index 0000000000..1fff11c1f1 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/ExchangeRateAPI.http @@ -0,0 +1,6 @@ +@ExchangeRateAPI_HostAddress = http://localhost:5164 + +GET {{ExchangeRateAPI_HostAddress}}/exchangerate/ +Accept: application/json + +### diff --git a/jobs/Backend/ExchangeRateAPI/Models/ExchangeRateResponse.cs b/jobs/Backend/ExchangeRateAPI/Models/ExchangeRateResponse.cs new file mode 100644 index 0000000000..bad3d5e98d --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/Models/ExchangeRateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace ExchangeRateAPI.Models +{ + public class ExchangeRateResponse + { + [JsonPropertyName("sourceCurrency")] + public string SourceCurrency { get; set; } + + [JsonPropertyName("targetCurrency")] + public string TargetCurrency { get; set; } + + [JsonPropertyName("value")] + public decimal Value { get; set; } + } +} diff --git a/jobs/Backend/ExchangeRateAPI/Program.cs b/jobs/Backend/ExchangeRateAPI/Program.cs new file mode 100644 index 0000000000..2ab0f5c7f2 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/Program.cs @@ -0,0 +1,31 @@ +using ExchangeRateUpdater.Business; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +builder.Services.AddSingleton(); + +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/jobs/Backend/ExchangeRateAPI/Properties/launchSettings.json b/jobs/Backend/ExchangeRateAPI/Properties/launchSettings.json new file mode 100644 index 0000000000..368d001b56 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5164" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7099;http://localhost:5164" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62116/", + "sslPort": 44304 + } + } +} \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateAPI/appsettings.Development.json b/jobs/Backend/ExchangeRateAPI/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/jobs/Backend/ExchangeRateAPI/appsettings.json b/jobs/Backend/ExchangeRateAPI/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/jobs/Backend/ExchangeRateAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/jobs/Backend/ExchangeRateAPITest/ExchangeRateAPIUnitTest.csproj b/jobs/Backend/ExchangeRateAPITest/ExchangeRateAPIUnitTest.csproj new file mode 100644 index 0000000000..e4437beb7d --- /dev/null +++ b/jobs/Backend/ExchangeRateAPITest/ExchangeRateAPIUnitTest.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jobs/Backend/ExchangeRateAPITest/ExchangeRateControllerTest.cs b/jobs/Backend/ExchangeRateAPITest/ExchangeRateControllerTest.cs new file mode 100644 index 0000000000..04057ec421 --- /dev/null +++ b/jobs/Backend/ExchangeRateAPITest/ExchangeRateControllerTest.cs @@ -0,0 +1,38 @@ +using ExchangeRateAPI.Controllers; +using ExchangeRateAPI.Models; +using ExchangeRateUpdater.Business; +using Moq; + +namespace ExchangeRateAPIUnitTest +{ + public class ExchangeRateControllerTest + { + private readonly ExchangeRateController __ExchangeRateController; + private readonly Mock __ExchangeRateProviderMock; + + public ExchangeRateControllerTest() + { + __ExchangeRateProviderMock = new Mock(); + __ExchangeRateController = new ExchangeRateController(__ExchangeRateProviderMock.Object); + } + + [Fact] + public async Task ExchangeRateControllerTest_GetAsync_ShouldReturnResultsAsync() + { + List _ExchangeRateMock = + [ + new ExchangeRate(new Currency("USD"), new Currency("CZK"), 20.43m), + new ExchangeRate(new Currency("AUD"), new Currency("CZK"), 14.48m), + ]; + + __ExchangeRateProviderMock + .Setup(mock => mock.GetExchangeRatesAsync(It.IsAny>())) + .Returns(Task.FromResult(_ExchangeRateMock)); + + List _Response = await __ExchangeRateController.GetAsync(); + + Assert.NotNull(_Response); + Assert.Equal(2, _Response.Count); + } + } +} diff --git a/jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateProviderTest.cs b/jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateProviderTest.cs new file mode 100644 index 0000000000..dee43bc64c --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateProviderTest.cs @@ -0,0 +1,46 @@ +using ExchangeRateUpdater.Business; +using ExchangeRateUpdater.Business.Models; +using ExchangeRateUpdater.Business.Services; +using Moq; + +namespace ExchangeRateUpdaterUnitTest +{ + public class ExchangeRateProviderTest + { + private readonly IExchangeRateProvider __ExchangeRateProvider; + private readonly Mock __ExchangeRateServiceMock; + + public ExchangeRateProviderTest() + { + __ExchangeRateServiceMock = new Mock(); + __ExchangeRateProvider = new ExchangeRateProvider(__ExchangeRateServiceMock.Object); + } + + [Fact] + public async Task ExchangeRateProvider_GetExchangeRatesAsync_ShouldReturnResultsAsync() + { + List _ExchangeRateModels = + [ + new ExchangeRateModel() { CurrencyCode = "USD", Rate = 20.43m }, + new ExchangeRateModel() { CurrencyCode = "AUD", Rate = 14.48m }, + new ExchangeRateModel() { CurrencyCode = "BRL", Rate = 3.92m }, + ]; + + __ExchangeRateServiceMock + .Setup(mock => mock.GetExchangeRatesAsync()) + .Returns(Task.FromResult(_ExchangeRateModels)) + .Verifiable(); + + List _Currencies = + [ + new Currency("AUD"), + new Currency("USD") + ]; + + List _Results = await __ExchangeRateProvider.GetExchangeRatesAsync(_Currencies); + + Assert.NotNull(_Results); + Assert.Equal(2, _Results.Count); + } + } +} diff --git a/jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateUpdaterUnitTest.csproj b/jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateUpdaterUnitTest.csproj new file mode 100644 index 0000000000..7dde928f18 --- /dev/null +++ b/jobs/Backend/ExchangeRateUpdaterUnitTest/ExchangeRateUpdaterUnitTest.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/Currency.cs b/jobs/Backend/Task/Business/Currency.cs similarity index 89% rename from jobs/Backend/Task/Currency.cs rename to jobs/Backend/Task/Business/Currency.cs index f375776f25..976d8ed377 100644 --- a/jobs/Backend/Task/Currency.cs +++ b/jobs/Backend/Task/Business/Currency.cs @@ -1,4 +1,4 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater.Business { public class Currency { diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/Business/ExchangeRate.cs similarity index 92% rename from jobs/Backend/Task/ExchangeRate.cs rename to jobs/Backend/Task/Business/ExchangeRate.cs index 58c5bb10e0..3cae44f113 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/Business/ExchangeRate.cs @@ -1,4 +1,4 @@ -namespace ExchangeRateUpdater +namespace ExchangeRateUpdater.Business { public class ExchangeRate { diff --git a/jobs/Backend/Task/Business/ExchangeRateProvider.cs b/jobs/Backend/Task/Business/ExchangeRateProvider.cs new file mode 100644 index 0000000000..fdbaa9e632 --- /dev/null +++ b/jobs/Backend/Task/Business/ExchangeRateProvider.cs @@ -0,0 +1,53 @@ +using ExchangeRateUpdater.Business.Models; +using ExchangeRateUpdater.Business.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Business +{ + public class ExchangeRateProvider : IExchangeRateProvider + { + private const string DEFAULT_CURRENCY = "CZK"; + + // This list represents the full list of exchange rates retrieved daily by calling the CNB API. + // Further improvement would be to keep track of the last time the API was called to see if it's time to update the database. + private static List __ExchangeRates = new(); + + private readonly IExchangeRateService __ExchangeRateService; + /// + /// 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 ExchangeRateProvider() + : this(new ExchangeRateService()) + { } + + public ExchangeRateProvider(IExchangeRateService exchangeRateService) + { + __ExchangeRateService = exchangeRateService ?? throw new ArgumentNullException(nameof(exchangeRateService)); + } + + public async Task> GetExchangeRatesAsync(IEnumerable currencies) + { + if (__ExchangeRates.Count == 0) + { + List _Results = await __ExchangeRateService.GetExchangeRatesAsync(); + if (_Results != null && _Results.Any()) + { + __ExchangeRates = _Results.Select(result => new ExchangeRate(new Currency(DEFAULT_CURRENCY), new Currency(result.CurrencyCode), result.Rate)).ToList(); + } + } + + if (currencies != null && currencies.Any()) + { + return __ExchangeRates.Where(result => currencies.Any(currency => currency.Code == result.TargetCurrency.Code)).ToList(); + } + + return __ExchangeRates; + } + } +} diff --git a/jobs/Backend/Task/Business/IExchangeRateProvider.cs b/jobs/Backend/Task/Business/IExchangeRateProvider.cs new file mode 100644 index 0000000000..8abdede94c --- /dev/null +++ b/jobs/Backend/Task/Business/IExchangeRateProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Business +{ + public interface IExchangeRateProvider + { + Task> GetExchangeRatesAsync(IEnumerable currencies); + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Business/Models/ExchangeRateModel.cs b/jobs/Backend/Task/Business/Models/ExchangeRateModel.cs new file mode 100644 index 0000000000..c61f9c2c32 --- /dev/null +++ b/jobs/Backend/Task/Business/Models/ExchangeRateModel.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace ExchangeRateUpdater.Business.Models +{ + public class ExchangeRateModel + { + [JsonPropertyName("currencyCode")] + public string CurrencyCode { get; set; } + + [JsonPropertyName("rate")] + public decimal Rate { get; set; } + } +} diff --git a/jobs/Backend/Task/Business/Models/ExchangeRateResponseModel.cs b/jobs/Backend/Task/Business/Models/ExchangeRateResponseModel.cs new file mode 100644 index 0000000000..68ff3ebce0 --- /dev/null +++ b/jobs/Backend/Task/Business/Models/ExchangeRateResponseModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ExchangeRateUpdater.Business.Models +{ + public class ExchangeRateResponseModel + { + [JsonPropertyName("rates")] + public List ExchangeRates{ get; set; } + } +} diff --git a/jobs/Backend/Task/Business/Parameters/ExchangeRateParameters.cs b/jobs/Backend/Task/Business/Parameters/ExchangeRateParameters.cs new file mode 100644 index 0000000000..8370796ed7 --- /dev/null +++ b/jobs/Backend/Task/Business/Parameters/ExchangeRateParameters.cs @@ -0,0 +1,18 @@ +using System; + +namespace ExchangeRateUpdater.Business.Parameters +{ + /** + * This class represents a set of possible parameterised details regarding the exchange rate for the Czech National Bank. + * These could be: + * - viewed/updated from a frontend parameter page + * - retrieved from a database with an entity/repository pattern before using them in the service class that performs the API call. + */ + internal class ExchangeRateParameters : IExchangeRateParameters + { + public string BaseURL { get; set; } = "https://api.cnb.cz/cnbapi/"; + public string ExchangeRatesDailyEndpoint { get; set; } = "exrates/daily"; + public DateTime Date { get; set; } = DateTime.Now; + public string Language { get; set; } = "EN"; + } +} diff --git a/jobs/Backend/Task/Business/Parameters/IExchangeRateParameters.cs b/jobs/Backend/Task/Business/Parameters/IExchangeRateParameters.cs new file mode 100644 index 0000000000..46e408d0db --- /dev/null +++ b/jobs/Backend/Task/Business/Parameters/IExchangeRateParameters.cs @@ -0,0 +1,12 @@ +using System; + +namespace ExchangeRateUpdater.Business.Parameters +{ + internal interface IExchangeRateParameters + { + string BaseURL { get; set; } + string ExchangeRatesDailyEndpoint { get; set; } + DateTime Date { get; set; } + string Language { get; set; } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Business/Services/ExchangeRateService.cs b/jobs/Backend/Task/Business/Services/ExchangeRateService.cs new file mode 100644 index 0000000000..c7be5d3621 --- /dev/null +++ b/jobs/Backend/Task/Business/Services/ExchangeRateService.cs @@ -0,0 +1,48 @@ +using ExchangeRateUpdater.Business.Models; +using ExchangeRateUpdater.Business.Parameters; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Business.Services +{ + public class ExchangeRateService : IExchangeRateService + { + private readonly HttpClient __Client; + private readonly IExchangeRateParameters __ExchangeRateParameters; + private readonly ILogger __Logger; + + // Using dependency injection to better manage all dependencies and testing + public ExchangeRateService() + : this(new ExchangeRateParameters(), new HttpClient(), new LoggerFactory().CreateLogger()) + { } + + internal ExchangeRateService(IExchangeRateParameters exchangeRateParameters, HttpClient httpClient, ILogger logger) + { + // Business logic classes can all extend a base class that logs different types of exception, for example this ArgumentNullException + __ExchangeRateParameters = exchangeRateParameters ?? throw new ArgumentNullException(nameof(exchangeRateParameters)); + __Client = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + __Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + async Task> IExchangeRateService.GetExchangeRatesAsync() + { + List _ExchangeRates = new(); + try + { + string _DailyRatesEndpoint = $"{__ExchangeRateParameters.BaseURL}{__ExchangeRateParameters.ExchangeRatesDailyEndpoint}?date={__ExchangeRateParameters.Date:yyyy-MM-dd}&lang={__ExchangeRateParameters.Language}"; + ExchangeRateResponseModel _Response = await __Client.GetFromJsonAsync(new Uri(_DailyRatesEndpoint)); + _ExchangeRates = _Response.ExchangeRates; + } + catch (Exception _Exception) + { + __Logger.LogError(_Exception, _Exception.Message); + } + + return _ExchangeRates; + } + } +} diff --git a/jobs/Backend/Task/Business/Services/IExchangeRateService.cs b/jobs/Backend/Task/Business/Services/IExchangeRateService.cs new file mode 100644 index 0000000000..f7ed4f09c1 --- /dev/null +++ b/jobs/Backend/Task/Business/Services/IExchangeRateService.cs @@ -0,0 +1,11 @@ +using ExchangeRateUpdater.Business.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ExchangeRateUpdater.Business.Services +{ + public interface IExchangeRateService + { + Task> GetExchangeRatesAsync(); + } +} \ 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 6f82a97fbe..0000000000 --- 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 2fc654a12b..25595763f2 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -5,4 +5,9 @@ net6.0 + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 89be84daff..0916fd4948 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -1,10 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 18 +VisualStudioVersion = 18.2.11415.280 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}") = "ExchangeRateAPI", "..\ExchangeRateAPI\ExchangeRateAPI.csproj", "{75788C34-62FB-410B-8E99-99B487A742F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateAPIUnitTest", "..\ExchangeRateAPITest\ExchangeRateAPIUnitTest.csproj", "{525549F1-F0EB-4524-A9D9-0A2DCB121C69}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateAPIIntegrationTest", "..\ExchangeAPIIntegrationTest\ExchangeRateAPIIntegrationTest.csproj", "{F6E46D30-FEE4-4579-8640-F3E7C155323F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdaterUnitTest", "..\ExchangeRateUpdaterUnitTest\ExchangeRateUpdaterUnitTest.csproj", "{A5D8D2C9-46EE-428C-94CF-AEAE0E522F3F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3848E27F-625E-4712-93A9-4700B3AFF6C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,8 +25,27 @@ 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 + {75788C34-62FB-410B-8E99-99B487A742F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75788C34-62FB-410B-8E99-99B487A742F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75788C34-62FB-410B-8E99-99B487A742F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75788C34-62FB-410B-8E99-99B487A742F9}.Release|Any CPU.Build.0 = Release|Any CPU + {525549F1-F0EB-4524-A9D9-0A2DCB121C69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {525549F1-F0EB-4524-A9D9-0A2DCB121C69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {525549F1-F0EB-4524-A9D9-0A2DCB121C69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {525549F1-F0EB-4524-A9D9-0A2DCB121C69}.Release|Any CPU.Build.0 = Release|Any CPU + {F6E46D30-FEE4-4579-8640-F3E7C155323F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6E46D30-FEE4-4579-8640-F3E7C155323F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6E46D30-FEE4-4579-8640-F3E7C155323F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6E46D30-FEE4-4579-8640-F3E7C155323F}.Release|Any CPU.Build.0 = Release|Any CPU + {A5D8D2C9-46EE-428C-94CF-AEAE0E522F3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5D8D2C9-46EE-428C-94CF-AEAE0E522F3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5D8D2C9-46EE-428C-94CF-AEAE0E522F3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5D8D2C9-46EE-428C-94CF-AEAE0E522F3F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5F12761D-E3ED-4941-AA34-5A3AA4BA3FC5} + EndGlobalSection EndGlobal diff --git a/jobs/Backend/Task/Program.cs b/jobs/Backend/Task/Program.cs index 379a69b1f8..c3e23cb089 100644 --- a/jobs/Backend/Task/Program.cs +++ b/jobs/Backend/Task/Program.cs @@ -1,6 +1,8 @@ -using System; +using ExchangeRateUpdater.Business; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace ExchangeRateUpdater { @@ -8,6 +10,7 @@ public static class Program { private static IEnumerable currencies = new[] { + new Currency("AUD"), new Currency("USD"), new Currency("EUR"), new Currency("CZK"), @@ -19,12 +22,12 @@ public static class Program new Currency("XYZ") }; - public static void Main(string[] args) + public static async Task Main(string[] args) { try { var provider = new ExchangeRateProvider(); - var rates = provider.GetExchangeRates(currencies); + var rates = await provider.GetExchangeRatesAsync(currencies); Console.WriteLine($"Successfully retrieved {rates.Count()} exchange rates:"); foreach (var rate in rates)