From db61851d02bc8664bc3b37a40281a8b7924891d9 Mon Sep 17 00:00:00 2001 From: "jordan.davidson" Date: Sun, 26 Jan 2025 20:16:57 +0000 Subject: [PATCH 1/2] tech test submit --- jobs/Backend/Task/API/API.csproj | 18 +++ .../API/Controllers/ControllerWithMediatR.cs | 18 +++ .../Controllers/ExchangeRatesController.cs | 35 +++++ .../API/Handlers/GlobalExceptionHandler.cs | 34 +++++ jobs/Backend/Task/API/Program.cs | 37 +++++ .../Task/API/Properties/launchSettings.json | 38 +++++ .../Task/API/appsettings.Development.json | 8 + jobs/Backend/Task/API/appsettings.json | 15 ++ .../Application.Tests.csproj | 28 ++++ .../GetDailyExchangeRateQueryHandlerTests.cs | 83 +++++++++++ .../GetExchangeRateQueryHandlerTests.cs | 118 +++++++++++++++ .../Task/Application/Abstractions/IQuery.cs | 6 + .../Application/Abstractions/IQueryHandler.cs | 8 + .../Task/Application/Application.csproj | 21 +++ .../Behaviours/ValidationBehaviour.cs | 51 +++++++ .../Extensions/FluentResultsExtensions.cs | 49 ++++++ .../Application/Regstration/Registration.cs | 26 ++++ .../GetDailyExchangeRateQuery.cs | 14 ++ .../GetDailyExchangeRateQueryHandler.cs | 39 +++++ .../GetDailyExchangeRateQueryValidator.cs | 29 ++++ .../ExchangeRates/GetExchangeRateQuery.cs | 18 +++ .../GetExchangeRateQueryHandler.cs | 51 +++++++ .../GetExchangeRateQueryValidator.cs | 33 ++++ .../Abstractions/Data/IAvailableCurrencies.cs | 9 ++ .../Abstractions/Data/IAvailableLangauges.cs | 6 + .../Domain/Abstractions/Data/ICacheService.cs | 8 + .../Abstractions/Http/IHttpClientService.cs | 6 + .../Utility/IExchangeRateProvider.cs | 9 ++ .../Task/Domain/Configurations/CNBConfig.cs | 9 ++ jobs/Backend/Task/Domain/Domain.csproj | 14 ++ .../Task/Domain/Errors/Base/NotFoundError.cs | 17 +++ .../Domain/Errors/Base/ValidationError.cs | 19 +++ .../Task/Domain/Errors/CurrencyNotFound.cs | 12 ++ .../Domain/Errors/ExchangeRateNotFound.cs | 12 ++ jobs/Backend/Task/Domain/Models/Currency.cs | 27 ++++ .../Task/Domain/Models/ExchangeRate.cs | 22 +++ .../Task/Domain/Models/RawExchangeRates.cs | 14 ++ jobs/Backend/Task/ExchangeRate.cs | 21 +++ jobs/Backend/Task/ExchangeRateUpdater.csproj | 8 +- jobs/Backend/Task/ExchangeRateUpdater.sln | 59 +++++++- .../ExchangeRateProviderTests.cs | 141 ++++++++++++++++++ .../Infrastructure.Tests.csproj | 29 ++++ .../Extensions/RawExchangeRatesExtensions.cs | 30 ++++ .../Task/Infrastructure/Infrastructure.csproj | 19 +++ .../Registration/Registration.cs | 45 ++++++ .../Services/Data/AvailableCurrencies.cs | 28 ++++ .../Services/Data/AvailableLanguages.cs | 15 ++ .../Services/Data/CacheService.cs | 40 +++++ .../Services/Http/HttpClientService.cs | 59 ++++++++ .../Services/Utility/ExchangeRateProvider.cs | 88 +++++++++++ jobs/Backend/Task/Program.cs | 43 ------ 51 files changed, 1535 insertions(+), 51 deletions(-) create mode 100644 jobs/Backend/Task/API/API.csproj create mode 100644 jobs/Backend/Task/API/Controllers/ControllerWithMediatR.cs create mode 100644 jobs/Backend/Task/API/Controllers/ExchangeRatesController.cs create mode 100644 jobs/Backend/Task/API/Handlers/GlobalExceptionHandler.cs create mode 100644 jobs/Backend/Task/API/Program.cs create mode 100644 jobs/Backend/Task/API/Properties/launchSettings.json create mode 100644 jobs/Backend/Task/API/appsettings.Development.json create mode 100644 jobs/Backend/Task/API/appsettings.json create mode 100644 jobs/Backend/Task/Application.Tests/Application.Tests.csproj create mode 100644 jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs create mode 100644 jobs/Backend/Task/Application.Tests/GetExchangeRateQueryHandlerTests.cs create mode 100644 jobs/Backend/Task/Application/Abstractions/IQuery.cs create mode 100644 jobs/Backend/Task/Application/Abstractions/IQueryHandler.cs create mode 100644 jobs/Backend/Task/Application/Application.csproj create mode 100644 jobs/Backend/Task/Application/Behaviours/ValidationBehaviour.cs create mode 100644 jobs/Backend/Task/Application/Extensions/FluentResultsExtensions.cs create mode 100644 jobs/Backend/Task/Application/Regstration/Registration.cs create mode 100644 jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQuery.cs create mode 100644 jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryHandler.cs create mode 100644 jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryValidator.cs create mode 100644 jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQuery.cs create mode 100644 jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryHandler.cs create mode 100644 jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryValidator.cs create mode 100644 jobs/Backend/Task/Domain/Abstractions/Data/IAvailableCurrencies.cs create mode 100644 jobs/Backend/Task/Domain/Abstractions/Data/IAvailableLangauges.cs create mode 100644 jobs/Backend/Task/Domain/Abstractions/Data/ICacheService.cs create mode 100644 jobs/Backend/Task/Domain/Abstractions/Http/IHttpClientService.cs create mode 100644 jobs/Backend/Task/Domain/Abstractions/Utility/IExchangeRateProvider.cs create mode 100644 jobs/Backend/Task/Domain/Configurations/CNBConfig.cs create mode 100644 jobs/Backend/Task/Domain/Domain.csproj create mode 100644 jobs/Backend/Task/Domain/Errors/Base/NotFoundError.cs create mode 100644 jobs/Backend/Task/Domain/Errors/Base/ValidationError.cs create mode 100644 jobs/Backend/Task/Domain/Errors/CurrencyNotFound.cs create mode 100644 jobs/Backend/Task/Domain/Errors/ExchangeRateNotFound.cs create mode 100644 jobs/Backend/Task/Domain/Models/Currency.cs create mode 100644 jobs/Backend/Task/Domain/Models/ExchangeRate.cs create mode 100644 jobs/Backend/Task/Domain/Models/RawExchangeRates.cs create mode 100644 jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs create mode 100644 jobs/Backend/Task/Infrastructure.Tests/Infrastructure.Tests.csproj create mode 100644 jobs/Backend/Task/Infrastructure/Extensions/RawExchangeRatesExtensions.cs create mode 100644 jobs/Backend/Task/Infrastructure/Infrastructure.csproj create mode 100644 jobs/Backend/Task/Infrastructure/Registration/Registration.cs create mode 100644 jobs/Backend/Task/Infrastructure/Services/Data/AvailableCurrencies.cs create mode 100644 jobs/Backend/Task/Infrastructure/Services/Data/AvailableLanguages.cs create mode 100644 jobs/Backend/Task/Infrastructure/Services/Data/CacheService.cs create mode 100644 jobs/Backend/Task/Infrastructure/Services/Http/HttpClientService.cs create mode 100644 jobs/Backend/Task/Infrastructure/Services/Utility/ExchangeRateProvider.cs delete mode 100644 jobs/Backend/Task/Program.cs diff --git a/jobs/Backend/Task/API/API.csproj b/jobs/Backend/Task/API/API.csproj new file mode 100644 index 000000000..8702f5f36 --- /dev/null +++ b/jobs/Backend/Task/API/API.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/jobs/Backend/Task/API/Controllers/ControllerWithMediatR.cs b/jobs/Backend/Task/API/Controllers/ControllerWithMediatR.cs new file mode 100644 index 000000000..db6220d46 --- /dev/null +++ b/jobs/Backend/Task/API/Controllers/ControllerWithMediatR.cs @@ -0,0 +1,18 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public abstract class ControllerWithMediatR : ControllerBase +{ + private readonly ISender _sender; + + protected ControllerWithMediatR(ISender sender) + { + _sender = sender; + } + + protected Task Send( + IRequest request, + CancellationToken cancellationToken) => _sender.Send(request, cancellationToken); +} diff --git a/jobs/Backend/Task/API/Controllers/ExchangeRatesController.cs b/jobs/Backend/Task/API/Controllers/ExchangeRatesController.cs new file mode 100644 index 000000000..25b7abc0b --- /dev/null +++ b/jobs/Backend/Task/API/Controllers/ExchangeRatesController.cs @@ -0,0 +1,35 @@ +using Api.Controllers; +using Application.Extensions; +using Application.UseCases.ExchangeRates; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers; + +[ApiController] +[Route("api/v1/{language}/")] +public class ExchangeRatesController(ISender sender) : ControllerWithMediatR(sender) +{ + [HttpGet] + [Route("exchangeRates")] + public async Task GetDailyAsync( + [FromRoute] string language, + [FromQuery] string currencyCode, + CancellationToken token) + { + var result = await Send(new GetDailyExchangeRateRequest().GetQuery(currencyCode, language), token); + return result.IsSuccess ? Ok(result.Value) : result.GetErrorResponse(); + } + + [HttpGet] + [Route("exchangeRate")] + public async Task GetExchangeRateAsync( + [FromRoute] string language, + [FromQuery] string fromCurrency, + [FromQuery] string toCurrency, + CancellationToken token) + { + var result = await Send(new GetExchangeRateRequest().GetQuery(language, fromCurrency, toCurrency), token); + return result.IsSuccess ? Ok(result.Value) : result.GetErrorResponse(); + } +} diff --git a/jobs/Backend/Task/API/Handlers/GlobalExceptionHandler.cs b/jobs/Backend/Task/API/Handlers/GlobalExceptionHandler.cs new file mode 100644 index 000000000..df9922f87 --- /dev/null +++ b/jobs/Backend/Task/API/Handlers/GlobalExceptionHandler.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Handlers; + +public class GlobalExceptionHandler : IExceptionHandler +{ + private readonly ILogger _logger; + + public GlobalExceptionHandler(ILogger logger) + { + _logger = logger; + } + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + _logger.LogError(exception, "Exception ocurred: {Message}", exception.Message); + + var problemDetails = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Title = "Server Error", + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1" + }; + + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + + await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); + return true; + } +} diff --git a/jobs/Backend/Task/API/Program.cs b/jobs/Backend/Task/API/Program.cs new file mode 100644 index 000000000..581c75404 --- /dev/null +++ b/jobs/Backend/Task/API/Program.cs @@ -0,0 +1,37 @@ +using Api.Handlers; +using Application.Regstration; +using Infrastructure.Registration; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); + +builder.Services.AddApplication(); +builder.Services.AddInfrastructure(builder.Configuration); + +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseExceptionHandler(); + +app.MapControllers(); + +app.Run(); diff --git a/jobs/Backend/Task/API/Properties/launchSettings.json b/jobs/Backend/Task/API/Properties/launchSettings.json new file mode 100644 index 000000000..e4e126a63 --- /dev/null +++ b/jobs/Backend/Task/API/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:31464", + "sslPort": 44349 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5244", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7065;http://localhost:5244", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/jobs/Backend/Task/API/appsettings.Development.json b/jobs/Backend/Task/API/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/jobs/Backend/Task/API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/jobs/Backend/Task/API/appsettings.json b/jobs/Backend/Task/API/appsettings.json new file mode 100644 index 000000000..d03bbc065 --- /dev/null +++ b/jobs/Backend/Task/API/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CNBSettings": { + "BaseURL": "https://api.cnb.cz/", + "ExchangeRateURL": "cnbapi/exrates/daily", + "RefreshTimeHour": 2, + "RefreshTimeMinute": 30 + } +} diff --git a/jobs/Backend/Task/Application.Tests/Application.Tests.csproj b/jobs/Backend/Task/Application.Tests/Application.Tests.csproj new file mode 100644 index 000000000..acd4c3821 --- /dev/null +++ b/jobs/Backend/Task/Application.Tests/Application.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs b/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs new file mode 100644 index 000000000..6dd224ac1 --- /dev/null +++ b/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs @@ -0,0 +1,83 @@ +using Application.UseCases.ExchangeRates; +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Errors; +using Domain.Models; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Application.Tests; + +public class GetDailyExchangeRateQueryHandlerTests +{ + private readonly Mock> _mockLogger; + private readonly Mock _mockCurrencyData; + private readonly Mock _mockExchangeRateProvider; + private readonly GetDailyExchangeRateQueryHandler _handler; + + public GetDailyExchangeRateQueryHandlerTests() + { + _mockLogger = new Mock>(); + _mockCurrencyData = new Mock(); + _mockExchangeRateProvider = new Mock(); + + _handler = new GetDailyExchangeRateQueryHandler( + _mockLogger.Object, + _mockCurrencyData.Object, + _mockExchangeRateProvider.Object); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenCurrencyNotFound() + { + // Arrange + var query = new GetDailyExchangeRateQuery("XYZ", "en"); + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("XYZ")) + .Returns((Currency)null); // Simulate currency not found + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + Assert.False(result.IsSuccess); + Assert.Single(result.Errors); + Assert.IsType(result.Errors[0]); + _mockLogger.Verify( + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Handle_ShouldReturnSuccess_WhenCurrencyIsFound() + { + // Arrange + var query = new GetDailyExchangeRateQuery("USD", "en"); + var currency = new Currency("USD"); + var exchangeRates = new List + { + new ExchangeRate(currency, new Currency("EUR"), 1.2m) + }; + + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("USD")) + .Returns(currency); + + _mockExchangeRateProvider + .Setup(m => m.GetDailyExchangeRates(currency, "en")) + .ReturnsAsync(exchangeRates); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + Assert.Equal(exchangeRates, result.Value.ExchangeRates); + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Application.Tests/GetExchangeRateQueryHandlerTests.cs b/jobs/Backend/Task/Application.Tests/GetExchangeRateQueryHandlerTests.cs new file mode 100644 index 000000000..c9352a2ed --- /dev/null +++ b/jobs/Backend/Task/Application.Tests/GetExchangeRateQueryHandlerTests.cs @@ -0,0 +1,118 @@ +using Application.UseCases.ExchangeRates; +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Errors; +using Domain.Models; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Application.Tests; + +public class GetExchangeRateQueryHandlerTests +{ + private readonly Mock> _mockLogger; + private readonly Mock _mockCurrencyData; + private readonly Mock _mockExchangeRateProvider; + private readonly GetExchangeRateQueryHandler _handler; + + public GetExchangeRateQueryHandlerTests() + { + _mockLogger = new Mock>(); + _mockCurrencyData = new Mock(); + _mockExchangeRateProvider = new Mock(); + + _handler = new GetExchangeRateQueryHandler( + _mockLogger.Object, + _mockCurrencyData.Object, + _mockExchangeRateProvider.Object); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenSourceCurrencyNotFound() + { + // Arrange + var query = new GetExchangeRateQuery("en", "XYZ", "USD"); + + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("XYZ")) + .Returns((Currency)null); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + Assert.False(result.IsSuccess); + Assert.Single(result.Errors); + Assert.IsType(result.Errors[0]); + _mockLogger.Verify( + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenExchangeRateNotFound() + { + // Arrange + var query = new GetExchangeRateQuery("en", "USD", "EUR"); + + + var sourceCurrency = new Currency("USD"); + var targetCurrency = new Currency("EUR"); + + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("USD")) + .Returns(sourceCurrency); + + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("EUR")) + .Returns(targetCurrency); + + _mockExchangeRateProvider + .Setup(m => m.GetExchangeRate(sourceCurrency, targetCurrency, "en")) + .ReturnsAsync((ExchangeRate)null); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + Assert.False(result.IsSuccess); + Assert.Single(result.Errors); + Assert.IsType(result.Errors[0]); + } + + [Fact] + public async Task Handle_ShouldReturnSuccess_WhenExchangeRateIsFound() + { + // Arrange + var query = new GetExchangeRateQuery("en", "USD", "EUR"); + + var sourceCurrency = new Currency("USD"); + var targetCurrency = new Currency("EUR"); + var exchangeRate = new ExchangeRate(sourceCurrency, targetCurrency, 1.2m); + + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("USD")) + .Returns(sourceCurrency); + + _mockCurrencyData + .Setup(m => m.GetCurrencyWithCode("EUR")) + .Returns(targetCurrency); + + _mockExchangeRateProvider + .Setup(m => m.GetExchangeRate(sourceCurrency, targetCurrency, "en")) + .ReturnsAsync(exchangeRate); + + // Act + var result = await _handler.Handle(query, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + Assert.Equal(exchangeRate, result.Value.ExchangeRate); + } +} diff --git a/jobs/Backend/Task/Application/Abstractions/IQuery.cs b/jobs/Backend/Task/Application/Abstractions/IQuery.cs new file mode 100644 index 000000000..cf4075780 --- /dev/null +++ b/jobs/Backend/Task/Application/Abstractions/IQuery.cs @@ -0,0 +1,6 @@ +using FluentResults; +using MediatR; + +namespace Application.Abstractions; + +public interface IQuery : IRequest> { } \ No newline at end of file diff --git a/jobs/Backend/Task/Application/Abstractions/IQueryHandler.cs b/jobs/Backend/Task/Application/Abstractions/IQueryHandler.cs new file mode 100644 index 000000000..6a98e2cf4 --- /dev/null +++ b/jobs/Backend/Task/Application/Abstractions/IQueryHandler.cs @@ -0,0 +1,8 @@ +using FluentResults; +using MediatR; + +namespace Application.Abstractions; + +public interface IQueryHandler : IRequestHandler> + where TQuery : IQuery +{ } \ No newline at end of file diff --git a/jobs/Backend/Task/Application/Application.csproj b/jobs/Backend/Task/Application/Application.csproj new file mode 100644 index 000000000..cde0db687 --- /dev/null +++ b/jobs/Backend/Task/Application/Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/Application/Behaviours/ValidationBehaviour.cs b/jobs/Backend/Task/Application/Behaviours/ValidationBehaviour.cs new file mode 100644 index 000000000..f09641c5b --- /dev/null +++ b/jobs/Backend/Task/Application/Behaviours/ValidationBehaviour.cs @@ -0,0 +1,51 @@ +using Domain.Errors.Base; +using FluentResults; +using FluentValidation; +using MediatR; + +namespace Application.Behaviours; + +internal sealed class ValidationBehaviour : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + private readonly IEnumerable> _validators; + + public ValidationBehaviour(IEnumerable> validators) + { + _validators = validators; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var validationResult = await Validate(request); + if (validationResult.IsFailed) + { + var result = new TResponse(); + + foreach (var reason in validationResult.Reasons) + { + result.Reasons.Add(reason); + } + + return result; + } + + return await next(); + } + + private Task Validate(TRequest request) + { + var context = new ValidationContext(request); + Error[] errors = _validators + .Select(validator => validator.Validate(context)) + .SelectMany(validationResult => validationResult.Errors) + .Where(validationFailures => validationFailures is not null) + .Select(failure => new ValidationError(failure.ErrorMessage)) + .Distinct() + .ToArray(); + + return Task.FromResult(Result.Fail(errors)); + } +} diff --git a/jobs/Backend/Task/Application/Extensions/FluentResultsExtensions.cs b/jobs/Backend/Task/Application/Extensions/FluentResultsExtensions.cs new file mode 100644 index 000000000..91d2067f8 --- /dev/null +++ b/jobs/Backend/Task/Application/Extensions/FluentResultsExtensions.cs @@ -0,0 +1,49 @@ +using Domain.Errors.Base; +using FluentResults; +using Microsoft.AspNetCore.Mvc; + +namespace Application.Extensions; + +public static class FluentResultsExtensions +{ + public static ActionResult GetErrorResponse(this ResultBase result) + => BuildErrorResponse(result); + + public static ActionResult GetErrorResponse(this Result result) + => BuildErrorResponse(result); + + public static ActionResult GetErrorResponse(this Result result) + => BuildErrorResponse(result); + + private static ActionResult BuildErrorResponse(ResultBase result) + { + if (result.HasError()) + { + var errors = GetErrors(result); + return ValidationError.Problem(GetErrorMessage(errors)); + } + + if (result.HasError()) + { + var errors = GetErrors(result); + return NotFoundError.Problem(GetErrorMessage(errors)); + } + + throw new InvalidOperationException("No errors to process."); + } + + private static IEnumerable GetErrors(ResultBase result) + { + List errors = new List(); + + result.Errors.ForEach(error => + { + errors.Add(error); + }); + + return errors; + } + + private static string GetErrorMessage(IEnumerable errors) + => string.Join(Environment.NewLine, errors.Select(e => e.Message)); +} diff --git a/jobs/Backend/Task/Application/Regstration/Registration.cs b/jobs/Backend/Task/Application/Regstration/Registration.cs new file mode 100644 index 000000000..884c68437 --- /dev/null +++ b/jobs/Backend/Task/Application/Regstration/Registration.cs @@ -0,0 +1,26 @@ +using Application.Behaviours; +using FluentValidation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace Application.Regstration; + +public static class Registration +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + + services.AddMediatR(configuration => + configuration.RegisterServicesFromAssemblies(assembly) + ); + + //add pipeline behaviours in order of activation + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); + + services.AddValidatorsFromAssembly(assembly); + + return services; + } +} diff --git a/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQuery.cs b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQuery.cs new file mode 100644 index 000000000..0cee600ea --- /dev/null +++ b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQuery.cs @@ -0,0 +1,14 @@ +using Application.Abstractions; +using Domain.Models; + +namespace Application.UseCases.ExchangeRates; + +public sealed record GetDailyExchangeRateRequest() +{ + public GetDailyExchangeRateQuery GetQuery(string currencyCode, string language) + => new GetDailyExchangeRateQuery(currencyCode.ToUpper(), language.ToUpper()); +} + +public sealed record GetDailyExchangeRateQuery(string CurrencyCode, string Language) : IQuery { } + +public sealed record GetDailyExchangeRateResponse(List ExchangeRates); diff --git a/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryHandler.cs b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryHandler.cs new file mode 100644 index 000000000..6f0cb62a7 --- /dev/null +++ b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryHandler.cs @@ -0,0 +1,39 @@ +using Application.Abstractions; +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Errors; +using FluentResults; +using Microsoft.Extensions.Logging; + +namespace Application.UseCases.ExchangeRates; + +public sealed class GetDailyExchangeRateQueryHandler : IQueryHandler +{ + private readonly ILogger _logger; + private readonly IAvailableCurrencies _currencyData; + private readonly IExchangeRateProvider _exchangeRateProvider; + + public GetDailyExchangeRateQueryHandler( + ILogger logger, + IAvailableCurrencies currencyData, + IExchangeRateProvider exchangeRateProvider) + { + _logger = logger; + _currencyData = currencyData; + _exchangeRateProvider = exchangeRateProvider; + } + + public async Task> Handle(GetDailyExchangeRateQuery request, CancellationToken cancellationToken) + { + var sourceCurrency = _currencyData.GetCurrencyWithCode(request.CurrencyCode); + + if (sourceCurrency is null) + { + return Result.Fail(new CurrencyNotFound(_logger, request.CurrencyCode)); + } + + var data = await _exchangeRateProvider.GetDailyExchangeRates(sourceCurrency, request.Language); + + return Result.Ok(new GetDailyExchangeRateResponse(data)); + } +} diff --git a/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryValidator.cs b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryValidator.cs new file mode 100644 index 000000000..48b117584 --- /dev/null +++ b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetDailyExchangeRateQueryValidator.cs @@ -0,0 +1,29 @@ +using Domain.Abstractions.Data; +using FluentValidation; + +namespace Application.UseCases.ExchangeRates; + +public class GetDailyExchangeRateQueryValidator : AbstractValidator +{ + public GetDailyExchangeRateQueryValidator( + IAvailableLangauges availableLangauges, + IAvailableCurrencies availableCurrencies) + { + var listOfLanguages = availableLangauges.GetLanguages(); + var listOfCurrencies = availableCurrencies.GetCurrencies().Select(x => x.Code); + + RuleFor(x => x.Language) + .NotEmpty() + .NotNull() + .MaximumLength(2) + .Must(language => listOfLanguages.Contains(language.ToUpper())) + .WithMessage("The language is not valid"); + + RuleFor(x => x.CurrencyCode) + .NotEmpty() + .NotNull() + .MaximumLength(3) + .Must(currency => listOfCurrencies.Contains(currency.ToUpper())) + .WithMessage("The currency is not valid"); + } +} diff --git a/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQuery.cs b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQuery.cs new file mode 100644 index 000000000..09fafee87 --- /dev/null +++ b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQuery.cs @@ -0,0 +1,18 @@ +using Application.Abstractions; +using Domain.Models; + +namespace Application.UseCases.ExchangeRates; + +public sealed record GetExchangeRateRequest() +{ + public GetExchangeRateQuery GetQuery(string language, string sourceCurrency, string targetCurrency) + => new GetExchangeRateQuery(language.ToUpper(), sourceCurrency.ToUpper(), targetCurrency.ToUpper()); +} + +public sealed record GetExchangeRateQuery( + string Language, + string SourceCurrency, + string TargetCurrency + ) : IQuery { } + +public sealed record GetExchangeRateResponse(ExchangeRate ExchangeRate); diff --git a/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryHandler.cs b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryHandler.cs new file mode 100644 index 000000000..fedc01656 --- /dev/null +++ b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryHandler.cs @@ -0,0 +1,51 @@ +using Application.Abstractions; +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Errors; +using FluentResults; +using Microsoft.Extensions.Logging; + +namespace Application.UseCases.ExchangeRates; + +public sealed class GetExchangeRateQueryHandler : IQueryHandler +{ + private readonly ILogger _logger; + private readonly IAvailableCurrencies _currencyData; + private readonly IExchangeRateProvider _exchangeRateProvider; + + public GetExchangeRateQueryHandler( + ILogger logger, + IAvailableCurrencies currencyData, + IExchangeRateProvider exchangeRateProvider) + { + _logger = logger; + _currencyData = currencyData; + _exchangeRateProvider = exchangeRateProvider; + } + + public async Task> Handle(GetExchangeRateQuery request, CancellationToken cancellationToken) + { + var sourceCurrency = _currencyData.GetCurrencyWithCode(request.SourceCurrency); + + if (sourceCurrency is null) + { + return Result.Fail(new CurrencyNotFound(_logger, request.SourceCurrency)); + } + + var targetCurrency = _currencyData.GetCurrencyWithCode(request.TargetCurrency); + + if (sourceCurrency is null) + { + return Result.Fail(new CurrencyNotFound(_logger, request.TargetCurrency)); + } + + var exchangeRate = await _exchangeRateProvider.GetExchangeRate(sourceCurrency, targetCurrency, request.Language); + + if (exchangeRate is null) + { + return Result.Fail(new ExchangeRateNotFound(_logger, request.SourceCurrency, request.TargetCurrency)); + } + + return Result.Ok(new GetExchangeRateResponse(exchangeRate)); + } +} diff --git a/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryValidator.cs b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryValidator.cs new file mode 100644 index 000000000..f05774803 --- /dev/null +++ b/jobs/Backend/Task/Application/UseCases/ExchangeRates/GetExchangeRateQueryValidator.cs @@ -0,0 +1,33 @@ +using Domain.Abstractions.Data; +using FluentValidation; + +namespace Application.UseCases.ExchangeRates +{ + public class GetExchangeRateQueryValidator : AbstractValidator + { + public GetExchangeRateQueryValidator( + IAvailableLangauges availableLangauges, + IAvailableCurrencies availableCurrencies) + { + var listOfLanguages = availableLangauges.GetLanguages(); + var listOfCurrencies = availableCurrencies.GetCurrencies().Select(x => x.Code); + + RuleFor(x => x.Language) + .NotEmpty() + .NotNull() + .MaximumLength(2) + .Must(language => listOfLanguages.Contains(language.ToUpper())) + .WithMessage("The language is not valid"); + + RuleFor(x => x.SourceCurrency) + .NotEmpty() + .NotNull() + .MaximumLength(3); + + RuleFor(x => x.TargetCurrency) + .NotEmpty() + .NotNull() + .MaximumLength(3); + } + } +} diff --git a/jobs/Backend/Task/Domain/Abstractions/Data/IAvailableCurrencies.cs b/jobs/Backend/Task/Domain/Abstractions/Data/IAvailableCurrencies.cs new file mode 100644 index 000000000..e80966920 --- /dev/null +++ b/jobs/Backend/Task/Domain/Abstractions/Data/IAvailableCurrencies.cs @@ -0,0 +1,9 @@ +using Domain.Models; + +namespace Domain.Abstractions.Data; + +public interface IAvailableCurrencies +{ + IEnumerable GetCurrencies(); + Currency GetCurrencyWithCode(string code); +} diff --git a/jobs/Backend/Task/Domain/Abstractions/Data/IAvailableLangauges.cs b/jobs/Backend/Task/Domain/Abstractions/Data/IAvailableLangauges.cs new file mode 100644 index 000000000..61dff9486 --- /dev/null +++ b/jobs/Backend/Task/Domain/Abstractions/Data/IAvailableLangauges.cs @@ -0,0 +1,6 @@ +namespace Domain.Abstractions.Data; + +public interface IAvailableLangauges +{ + IEnumerable GetLanguages(); +} diff --git a/jobs/Backend/Task/Domain/Abstractions/Data/ICacheService.cs b/jobs/Backend/Task/Domain/Abstractions/Data/ICacheService.cs new file mode 100644 index 000000000..d4c31a2b6 --- /dev/null +++ b/jobs/Backend/Task/Domain/Abstractions/Data/ICacheService.cs @@ -0,0 +1,8 @@ +namespace Domain.Abstractions.Data; + +public interface ICacheService +{ + T Get(string key); + void Set(string key, T value, TimeSpan expiration); + void Remove(string key); +} diff --git a/jobs/Backend/Task/Domain/Abstractions/Http/IHttpClientService.cs b/jobs/Backend/Task/Domain/Abstractions/Http/IHttpClientService.cs new file mode 100644 index 000000000..2b86b9e7c --- /dev/null +++ b/jobs/Backend/Task/Domain/Abstractions/Http/IHttpClientService.cs @@ -0,0 +1,6 @@ +namespace Domain.Abstractions; + +public interface IHttpClientService +{ + Task GetJsonAsync(string uri); +} diff --git a/jobs/Backend/Task/Domain/Abstractions/Utility/IExchangeRateProvider.cs b/jobs/Backend/Task/Domain/Abstractions/Utility/IExchangeRateProvider.cs new file mode 100644 index 000000000..f28d77f86 --- /dev/null +++ b/jobs/Backend/Task/Domain/Abstractions/Utility/IExchangeRateProvider.cs @@ -0,0 +1,9 @@ +using Domain.Models; + +namespace Domain.Abstractions; + +public interface IExchangeRateProvider +{ + Task> GetDailyExchangeRates(Currency sourceCurrency, string language); + Task GetExchangeRate(Currency source, Currency target, string language = "EN"); +} diff --git a/jobs/Backend/Task/Domain/Configurations/CNBConfig.cs b/jobs/Backend/Task/Domain/Configurations/CNBConfig.cs new file mode 100644 index 000000000..4b8588652 --- /dev/null +++ b/jobs/Backend/Task/Domain/Configurations/CNBConfig.cs @@ -0,0 +1,9 @@ +namespace Domain.Configurations; + +public class CNBConfig +{ + public string BaseURL { get; set; } + public string ExchangeRateURL { get; set; } + public int RefreshTimeHour { get; set; } + public int RefreshTimeMinute { get; set; } +} diff --git a/jobs/Backend/Task/Domain/Domain.csproj b/jobs/Backend/Task/Domain/Domain.csproj new file mode 100644 index 000000000..f7c46d32c --- /dev/null +++ b/jobs/Backend/Task/Domain/Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/jobs/Backend/Task/Domain/Errors/Base/NotFoundError.cs b/jobs/Backend/Task/Domain/Errors/Base/NotFoundError.cs new file mode 100644 index 000000000..ed266ed79 --- /dev/null +++ b/jobs/Backend/Task/Domain/Errors/Base/NotFoundError.cs @@ -0,0 +1,17 @@ +using FluentResults; +using Microsoft.AspNetCore.Mvc; + +namespace Domain.Errors.Base; + +public class NotFoundError(string error) : Error(error), IError +{ + public static string Title = "No Data Found"; + + public static ObjectResult Problem(string error) => + new ObjectResult(new ProblemDetails() + { + Title = Title, + Status = 404, + Detail = error + }); +} \ No newline at end of file diff --git a/jobs/Backend/Task/Domain/Errors/Base/ValidationError.cs b/jobs/Backend/Task/Domain/Errors/Base/ValidationError.cs new file mode 100644 index 000000000..68d46b4e3 --- /dev/null +++ b/jobs/Backend/Task/Domain/Errors/Base/ValidationError.cs @@ -0,0 +1,19 @@ +using FluentResults; +using Microsoft.AspNetCore.Mvc; + +namespace Domain.Errors.Base; + +public sealed class ValidationError : Error, IError +{ + public ValidationError(string error) : base(error) { } + + public static string Title = "Validation Error"; + + public static ObjectResult Problem(string error) => + new ObjectResult(new ProblemDetails() + { + Title = Title, + Status = 422, + Detail = error + }); +} \ No newline at end of file diff --git a/jobs/Backend/Task/Domain/Errors/CurrencyNotFound.cs b/jobs/Backend/Task/Domain/Errors/CurrencyNotFound.cs new file mode 100644 index 000000000..9fb28cf74 --- /dev/null +++ b/jobs/Backend/Task/Domain/Errors/CurrencyNotFound.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Logging; + +namespace Domain.Errors; + +public class CurrencyNotFound : Base.NotFoundError +{ + private const string Error = "No data could be found for Currency : {0}"; + public CurrencyNotFound(ILogger logger, string id) : base(string.Format(Error, id)) + { + logger.LogError(string.Format(Error, id)); + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Domain/Errors/ExchangeRateNotFound.cs b/jobs/Backend/Task/Domain/Errors/ExchangeRateNotFound.cs new file mode 100644 index 000000000..033616196 --- /dev/null +++ b/jobs/Backend/Task/Domain/Errors/ExchangeRateNotFound.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Logging; + +namespace Domain.Errors; + +public sealed class ExchangeRateNotFound : Base.NotFoundError +{ + private const string Error = "No data could be found for ExchangeRate : {0} => {1}"; + public ExchangeRateNotFound(ILogger logger, string source, string target) : base(string.Format(Error, source, target)) + { + logger.LogError(string.Format(Error, source, target)); + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Domain/Models/Currency.cs b/jobs/Backend/Task/Domain/Models/Currency.cs new file mode 100644 index 000000000..744595aaa --- /dev/null +++ b/jobs/Backend/Task/Domain/Models/Currency.cs @@ -0,0 +1,27 @@ +namespace Domain.Models; +public sealed class Currency +{ + /// + /// Three-letter ISO 4217 code of the currency. + /// + public string Code { get; } + public string Country { get; set; } + public string CurrencyName { get; set; } + + public Currency(string code) + { + Code = code; + } + + public Currency(string code, string country, string currencyName) + { + Code = code; + Country = country; + CurrencyName = currencyName; + } + + public override string ToString() + { + return Code; + } +} diff --git a/jobs/Backend/Task/Domain/Models/ExchangeRate.cs b/jobs/Backend/Task/Domain/Models/ExchangeRate.cs new file mode 100644 index 000000000..e0c996312 --- /dev/null +++ b/jobs/Backend/Task/Domain/Models/ExchangeRate.cs @@ -0,0 +1,22 @@ +namespace Domain.Models; +public sealed 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/Domain/Models/RawExchangeRates.cs b/jobs/Backend/Task/Domain/Models/RawExchangeRates.cs new file mode 100644 index 000000000..aa40b12eb --- /dev/null +++ b/jobs/Backend/Task/Domain/Models/RawExchangeRates.cs @@ -0,0 +1,14 @@ +namespace Domain.Models; + +public sealed class RawExchangeRates +{ + public List Rates { get; set; } +} + +public sealed record RawExchangeRate +{ + public string Country { get; set; } + public string Currency { get; set; } + public string CurrencyCode { get; set; } + public decimal Rate { get; set; } +} \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRate.cs b/jobs/Backend/Task/ExchangeRate.cs index 58c5bb10e..0a0ab12e6 100644 --- a/jobs/Backend/Task/ExchangeRate.cs +++ b/jobs/Backend/Task/ExchangeRate.cs @@ -21,3 +21,24 @@ public override string ToString() } } } + + +System.AggregateException + HResult=0x80131500 + Message=Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: MediatR.IRequestHandler`2[Application.UseCases.ExchangeRates.GetDailyExchangeRateQuery,FluentResults.Result`1[Application.UseCases.ExchangeRates.GetDailyExchangeRateResponse]] Lifetime: Transient ImplementationType: Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler': Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler'.) + Source=Microsoft.Extensions.DependencyInjection + StackTrace: + at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options) + at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options) + at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build() + at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build() + at Program.
$(String[] args) in D:\technicalTests\TechTest-Jordan\jobs\Backend\Task\API\Program.cs:line 24 + + This exception was originally thrown at this call stack: + [External Code] + +Inner Exception 1: +InvalidOperationException: Error while validating the service descriptor 'ServiceType: MediatR.IRequestHandler`2[Application.UseCases.ExchangeRates.GetDailyExchangeRateQuery,FluentResults.Result`1[Application.UseCases.ExchangeRates.GetDailyExchangeRateResponse]] Lifetime: Transient ImplementationType: Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler': Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler'. + +Inner Exception 2: +InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler'. diff --git a/jobs/Backend/Task/ExchangeRateUpdater.csproj b/jobs/Backend/Task/ExchangeRateUpdater.csproj index 2fc654a12..4fcdb7030 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ b/jobs/Backend/Task/ExchangeRateUpdater.csproj @@ -2,7 +2,13 @@ Exe - net6.0 + net8.0 + + + + + + \ No newline at end of file diff --git a/jobs/Backend/Task/ExchangeRateUpdater.sln b/jobs/Backend/Task/ExchangeRateUpdater.sln index 89be84daf..84f5ea2e6 100644 --- a/jobs/Backend/Task/ExchangeRateUpdater.sln +++ b/jobs/Backend/Task/ExchangeRateUpdater.sln @@ -1,9 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExchangeRateUpdater", "ExchangeRateUpdater.csproj", "{7B2695D6-D24C-4460-A58E-A10F08550CE0}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{82E56CEE-5FF8-4D1C-ACC5-D9D1056408CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\Application.csproj", "{E8107966-7C0F-49EE-A6EF-2FC665671F06}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "Domain\Domain.csproj", "{B73DB32F-2747-4437-AAE9-1907D13171A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{162FA347-0C63-4A97-B8FF-C3018480E7FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API", "API\API.csproj", "{520C7363-B04E-4162-A21C-C8E7CFEF8CC2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8C1575EF-A3D8-44BB-ABFE-D52F6719E7B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.Tests", "Infrastructure.Tests\Infrastructure.Tests.csproj", "{16BDC70E-91EB-4E30-964D-10963DBE3BEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.Tests", "Application.Tests\Application.Tests.csproj", "{68D6D6A4-22CF-4183-A702-DF147A73E317}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,12 +25,43 @@ Global 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 + {E8107966-7C0F-49EE-A6EF-2FC665671F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8107966-7C0F-49EE-A6EF-2FC665671F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8107966-7C0F-49EE-A6EF-2FC665671F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8107966-7C0F-49EE-A6EF-2FC665671F06}.Release|Any CPU.Build.0 = Release|Any CPU + {B73DB32F-2747-4437-AAE9-1907D13171A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B73DB32F-2747-4437-AAE9-1907D13171A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B73DB32F-2747-4437-AAE9-1907D13171A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B73DB32F-2747-4437-AAE9-1907D13171A6}.Release|Any CPU.Build.0 = Release|Any CPU + {162FA347-0C63-4A97-B8FF-C3018480E7FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {162FA347-0C63-4A97-B8FF-C3018480E7FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {162FA347-0C63-4A97-B8FF-C3018480E7FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {162FA347-0C63-4A97-B8FF-C3018480E7FB}.Release|Any CPU.Build.0 = Release|Any CPU + {520C7363-B04E-4162-A21C-C8E7CFEF8CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {520C7363-B04E-4162-A21C-C8E7CFEF8CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {520C7363-B04E-4162-A21C-C8E7CFEF8CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {520C7363-B04E-4162-A21C-C8E7CFEF8CC2}.Release|Any CPU.Build.0 = Release|Any CPU + {16BDC70E-91EB-4E30-964D-10963DBE3BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16BDC70E-91EB-4E30-964D-10963DBE3BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16BDC70E-91EB-4E30-964D-10963DBE3BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16BDC70E-91EB-4E30-964D-10963DBE3BEC}.Release|Any CPU.Build.0 = Release|Any CPU + {68D6D6A4-22CF-4183-A702-DF147A73E317}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68D6D6A4-22CF-4183-A702-DF147A73E317}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68D6D6A4-22CF-4183-A702-DF147A73E317}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68D6D6A4-22CF-4183-A702-DF147A73E317}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E8107966-7C0F-49EE-A6EF-2FC665671F06} = {82E56CEE-5FF8-4D1C-ACC5-D9D1056408CC} + {B73DB32F-2747-4437-AAE9-1907D13171A6} = {82E56CEE-5FF8-4D1C-ACC5-D9D1056408CC} + {162FA347-0C63-4A97-B8FF-C3018480E7FB} = {82E56CEE-5FF8-4D1C-ACC5-D9D1056408CC} + {520C7363-B04E-4162-A21C-C8E7CFEF8CC2} = {82E56CEE-5FF8-4D1C-ACC5-D9D1056408CC} + {16BDC70E-91EB-4E30-964D-10963DBE3BEC} = {8C1575EF-A3D8-44BB-ABFE-D52F6719E7B2} + {68D6D6A4-22CF-4183-A702-DF147A73E317} = {8C1575EF-A3D8-44BB-ABFE-D52F6719E7B2} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {431A154B-F443-44EA-B8BF-6FBDCAC71625} + EndGlobalSection EndGlobal diff --git a/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs new file mode 100644 index 000000000..d487a59eb --- /dev/null +++ b/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs @@ -0,0 +1,141 @@ +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Configurations; +using Domain.Models; +using Infrastructure.Services; +using Microsoft.Extensions.Options; +using Moq; + +namespace Infrastructure.Tests +{ + public class ExchangeRateProviderTests + { + private readonly Mock _mockCacheService; + private readonly Mock _mockHttpClientService; + private readonly Mock> _mockConfig; + private readonly ExchangeRateProvider _provider; + + public ExchangeRateProviderTests() + { + _mockCacheService = new Mock(); + _mockHttpClientService = new Mock(); + _mockConfig = new Mock>(); + + // Set up the configuration mock + _mockConfig.Setup(x => x.Value).Returns(new CNBConfig + { + BaseURL = "https://google.com", + ExchangeRateURL = "", + RefreshTimeHour = 23, + RefreshTimeMinute = 30 + }); + + // Instantiate the ExchangeRateProvider + _provider = new ExchangeRateProvider( + _mockCacheService.Object, + _mockHttpClientService.Object, + _mockConfig.Object + ); + } + + [Fact] + public async Task GetDailyExchangeRates_ShouldReturn() + { + // Arrange + var exchangeRates = new List + { + new ExchangeRate( + new Currency("USD"), + new Currency("EUR"), + 0.85m + ) + }; + + _mockCacheService.Setup(x => x.Get>(It.IsAny())).Returns(exchangeRates); + + // Act + var result = await _provider.GetDailyExchangeRates(new Currency("CZK")); + + // Assert + Assert.NotNull(result); + Assert.Equal(1, result.Count); + } + + [Fact] + public async Task GetDailyExchangeRates_ShouldCallHttpClient() + { + var exchangeRates = new RawExchangeRates + { + Rates = new List + { + new RawExchangeRate + { + Country = "UK", + Currency = "Pounds", + CurrencyCode = "GBP", + Rate = 18.77m + } + } + }; + + _mockHttpClientService.Setup(x => x.GetJsonAsync(It.IsAny())) + .ReturnsAsync(exchangeRates); + + // Act + var result = await _provider.GetDailyExchangeRates(new Currency("CZK")); + + // Assert + Assert.NotNull(result); + _mockHttpClientService.Verify(x => x.GetJsonAsync(It.IsAny()), Times.Once); // Ensure HTTP request was made + } + + [Fact] + public async Task GetExchangeRate_ShouldReturnCorrectRate_WhenValidCurrenciesAreProvided() + { + // Arrange + var sourceCurrency = new Currency("USD"); + var targetCurrency = new Currency("EUR"); + var exchangeRates = new List + { + new ExchangeRate( + sourceCurrency, + targetCurrency, + (decimal)0.85 + ) + }; + + _mockCacheService.Setup(x => x.Get>(It.IsAny())).Returns(exchangeRates); + + // Act + var result = await _provider.GetExchangeRate(sourceCurrency, targetCurrency); + + // Assert + Assert.NotNull(result); + Assert.Equal((decimal)0.85, result?.Value); // Check if the correct exchange rate was returned + } + + [Fact] + public async Task GetExchangeRate_ShouldReturnNull_WhenRateNotFound() + { + // Arrange + var sourceCurrency = new Currency("USD"); + var targetCurrency = new Currency("GBP"); + var exchangeRates = new List + { + new ExchangeRate( + sourceCurrency, + targetCurrency, + (decimal)0.85 + ) + }; + + _mockCacheService.Setup(x => x.Get>(It.IsAny())).Returns(exchangeRates); + + // Act + var result = await _provider.GetExchangeRate(sourceCurrency, new Currency("EUR")); + + // Assert + Assert.Null(result); // Ensure no exchange rate was found for the requested currencies + } + } +} \ No newline at end of file diff --git a/jobs/Backend/Task/Infrastructure.Tests/Infrastructure.Tests.csproj b/jobs/Backend/Task/Infrastructure.Tests/Infrastructure.Tests.csproj new file mode 100644 index 000000000..73911e088 --- /dev/null +++ b/jobs/Backend/Task/Infrastructure.Tests/Infrastructure.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/Infrastructure/Extensions/RawExchangeRatesExtensions.cs b/jobs/Backend/Task/Infrastructure/Extensions/RawExchangeRatesExtensions.cs new file mode 100644 index 000000000..808e87a06 --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Extensions/RawExchangeRatesExtensions.cs @@ -0,0 +1,30 @@ +using Domain.Models; + +namespace Infrastructure.Extensions; + +internal static class RawExchangeRatesExtensions +{ + internal static List ConvertToExchangeRates(this RawExchangeRates rawExchangeRates, Currency sourceCurrency) + { + var rates = new List(); + + foreach(var rate in rawExchangeRates.Rates) + { + var newRate = new ExchangeRate + ( + sourceCurrency, + rate.ConvertToCurrency(), + rate.Rate + ); + rates.Add(newRate); + } + + return rates; + } + + private static Currency ConvertToCurrency(this RawExchangeRate rawExchangeRate) + => new Currency( + rawExchangeRate.CurrencyCode, + rawExchangeRate.Country, + rawExchangeRate.Currency); +} diff --git a/jobs/Backend/Task/Infrastructure/Infrastructure.csproj b/jobs/Backend/Task/Infrastructure/Infrastructure.csproj new file mode 100644 index 000000000..d2ea08bae --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Infrastructure.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/jobs/Backend/Task/Infrastructure/Registration/Registration.cs b/jobs/Backend/Task/Infrastructure/Registration/Registration.cs new file mode 100644 index 000000000..0d91ce87d --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Registration/Registration.cs @@ -0,0 +1,45 @@ +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Configurations; +using Infrastructure.Services; +using Infrastructure.Services.Data; +using Infrastructure.Services.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure.Registration; + +public static class Registration +{ + public static void AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + services.AddConfigurations(configuration); + services.AddCache(); + services.AddServices(); + services.AddData(); + } + + private static void AddConfigurations(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("CNBSettings")); + } + + private static void AddCache(this IServiceCollection services) + { + services.AddMemoryCache(); + } + + private static void AddServices(this IServiceCollection services) + { + services.AddHttpClient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + private static void AddData(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } +} diff --git a/jobs/Backend/Task/Infrastructure/Services/Data/AvailableCurrencies.cs b/jobs/Backend/Task/Infrastructure/Services/Data/AvailableCurrencies.cs new file mode 100644 index 000000000..21b99248a --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Services/Data/AvailableCurrencies.cs @@ -0,0 +1,28 @@ +using Domain.Abstractions.Data; +using Domain.Models; + +namespace Infrastructure.Services.Data; + +public class AvailableCurrencies : IAvailableCurrencies +{ + private IEnumerable Currencies = + [ + 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 IEnumerable GetCurrencies() + => Currencies; + + public Currency GetCurrencyWithCode(string code) + { + return Currencies.FirstOrDefault(c => c.Code == code); + } +} diff --git a/jobs/Backend/Task/Infrastructure/Services/Data/AvailableLanguages.cs b/jobs/Backend/Task/Infrastructure/Services/Data/AvailableLanguages.cs new file mode 100644 index 000000000..045a2270a --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Services/Data/AvailableLanguages.cs @@ -0,0 +1,15 @@ +using Domain.Abstractions.Data; + +namespace Infrastructure.Services.Data; + +public class AvailableLanguages : IAvailableLangauges +{ + private IEnumerable Languages = + [ + "EN", + "CZ" + ]; + + public IEnumerable GetLanguages() + => Languages; +} diff --git a/jobs/Backend/Task/Infrastructure/Services/Data/CacheService.cs b/jobs/Backend/Task/Infrastructure/Services/Data/CacheService.cs new file mode 100644 index 000000000..5654c8156 --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Services/Data/CacheService.cs @@ -0,0 +1,40 @@ +using Domain.Abstractions.Data; +using Microsoft.Extensions.Caching.Memory; + +namespace Infrastructure.Services; + +internal sealed class CacheService : ICacheService +{ + private readonly IMemoryCache _memoryCache; + + public CacheService(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + } + + public T? Get(string key) + { + if (_memoryCache.TryGetValue(key, out var value)) + { + return (T?)value; + } + + return default; + } + + public void Set(string key, T value, TimeSpan expiration) + { + var cacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = expiration + }; + + _memoryCache.Set(key, value, cacheEntryOptions); + } + + public void Remove(string key) + { + _memoryCache.Remove(key); + } + +} diff --git a/jobs/Backend/Task/Infrastructure/Services/Http/HttpClientService.cs b/jobs/Backend/Task/Infrastructure/Services/Http/HttpClientService.cs new file mode 100644 index 000000000..75e6d0977 --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Services/Http/HttpClientService.cs @@ -0,0 +1,59 @@ +using Domain.Abstractions; +using System.Text.Json; + +namespace Infrastructure.Services.Http; + +internal class HttpClientService : IHttpClientService +{ + private readonly IHttpClientFactory _httpClientFactory; + + public HttpClientService(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public async Task GetJsonAsync(string uri) + { + try + { + var client = _httpClientFactory.CreateClient(); + + var response = await client.GetAsync(uri); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Request failed with status code {response.StatusCode}"); + } + + var responseString = await response.Content.ReadAsStringAsync(); + + try + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + return JsonSerializer.Deserialize(responseString, options); + } + catch (JsonException ex) + { + throw new ApplicationException("Error parsing the response JSON.", ex); + } + } + catch (HttpRequestException ex) + { + // Handle HTTP request-specific errors + throw new ApplicationException("Error during HTTP request.", ex); + } + catch (TaskCanceledException ex) + { + // Handle timeout errors + throw new ApplicationException("Request timed out.", ex); + } + catch (Exception ex) + { + // Catch all other exceptions + throw new ApplicationException("An unexpected error occurred.", ex); + } + } +} diff --git a/jobs/Backend/Task/Infrastructure/Services/Utility/ExchangeRateProvider.cs b/jobs/Backend/Task/Infrastructure/Services/Utility/ExchangeRateProvider.cs new file mode 100644 index 000000000..406689bf2 --- /dev/null +++ b/jobs/Backend/Task/Infrastructure/Services/Utility/ExchangeRateProvider.cs @@ -0,0 +1,88 @@ +using Domain.Abstractions; +using Domain.Abstractions.Data; +using Domain.Configurations; +using Domain.Models; +using Infrastructure.Extensions; +using Microsoft.Extensions.Options; +using System.Web; + +namespace Infrastructure.Services; + +public class ExchangeRateProvider : IExchangeRateProvider +{ + protected readonly ICacheService _cacheService; + protected readonly IHttpClientService _httpClientService; + protected readonly CNBConfig _config; + + public ExchangeRateProvider( + ICacheService cacheService, + IHttpClientService httpClientService, + IOptions config) + { + _cacheService = cacheService; + _httpClientService = httpClientService; + _config = config.Value; + } + + const string QUERYDATE = "date"; + const string QUERYLANGUAGE = "lang"; + + const string EXCHANGEDAILYRATESCACHEKEY = "ExchangeRates:Daily"; + + public async Task> GetDailyExchangeRates(Currency sourceCurrency, string language = "EN") + { + var result = _cacheService.Get>(EXCHANGEDAILYRATESCACHEKEY); + + if (result is not null) + { + return result; + } + + return await GetExchangeDataAsync(sourceCurrency, language); + + } + + public async Task GetExchangeRate(Currency source, Currency target, string language = "EN") + { + var exchangeRates = await GetDailyExchangeRates(source, language); + return exchangeRates + .Where(x => x.SourceCurrency.Code == source.Code && x.TargetCurrency.Code == target.Code) + .FirstOrDefault(); + } + + private async Task> GetExchangeDataAsync(Currency sourceCurrency, string language = "EN") + { + var url = BuildURL(language); + + var rawData = await _httpClientService.GetJsonAsync(url); + + var convertedData = rawData.ConvertToExchangeRates(sourceCurrency); + var tomorrowAt230pm = + DateTime.Today + .AddDays(1) + .AddHours(_config.RefreshTimeHour) + .AddMinutes(_config.RefreshTimeMinute); + var expiryTime = tomorrowAt230pm - DateTime.UtcNow; + + _cacheService.Set(EXCHANGEDAILYRATESCACHEKEY, convertedData, expiryTime); + + return convertedData; + } + + private string BuildURL(string language) + { + var shortDateInISOFormat = DateTime.Now.ToString("yyyy-MM-dd"); + + var baseUri = new Uri($"{_config.BaseURL}{_config.ExchangeRateURL}"); + var uriBuilder = new UriBuilder(baseUri) + { + Port = -1 + }; + + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + query[QUERYDATE] = shortDateInISOFormat; + query[QUERYLANGUAGE] = language; + uriBuilder.Query = query.ToString(); + return uriBuilder.ToString(); + } +} 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(); - } - } -} From d0dd710c10fa11189b9c014ea49750a84a52382a Mon Sep 17 00:00:00 2001 From: "jordan.davidson" Date: Sun, 26 Jan 2025 20:32:20 +0000 Subject: [PATCH 2/2] remove old files --- .../GetDailyExchangeRateQueryHandlerTests.cs | 2 +- jobs/Backend/Task/Currency.cs | 20 --------- jobs/Backend/Task/ExchangeRate.cs | 44 ------------------- jobs/Backend/Task/ExchangeRateProvider.cs | 19 -------- jobs/Backend/Task/ExchangeRateUpdater.csproj | 14 ------ .../ExchangeRateProviderTests.cs | 2 +- 6 files changed, 2 insertions(+), 99 deletions(-) delete mode 100644 jobs/Backend/Task/Currency.cs delete mode 100644 jobs/Backend/Task/ExchangeRate.cs delete mode 100644 jobs/Backend/Task/ExchangeRateProvider.cs delete mode 100644 jobs/Backend/Task/ExchangeRateUpdater.csproj diff --git a/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs b/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs index 6dd224ac1..832709f68 100644 --- a/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs +++ b/jobs/Backend/Task/Application.Tests/GetDailyExchangeRateQueryHandlerTests.cs @@ -34,7 +34,7 @@ public async Task Handle_ShouldReturnFailure_WhenCurrencyNotFound() var query = new GetDailyExchangeRateQuery("XYZ", "en"); _mockCurrencyData .Setup(m => m.GetCurrencyWithCode("XYZ")) - .Returns((Currency)null); // Simulate currency not found + .Returns((Currency)null); // Act var result = await _handler.Handle(query, CancellationToken.None); 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 0a0ab12e6..000000000 --- a/jobs/Backend/Task/ExchangeRate.cs +++ /dev/null @@ -1,44 +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}"; - } - } -} - - -System.AggregateException - HResult=0x80131500 - Message=Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: MediatR.IRequestHandler`2[Application.UseCases.ExchangeRates.GetDailyExchangeRateQuery,FluentResults.Result`1[Application.UseCases.ExchangeRates.GetDailyExchangeRateResponse]] Lifetime: Transient ImplementationType: Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler': Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler'.) - Source=Microsoft.Extensions.DependencyInjection - StackTrace: - at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options) - at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options) - at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build() - at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build() - at Program.
$(String[] args) in D:\technicalTests\TechTest-Jordan\jobs\Backend\Task\API\Program.cs:line 24 - - This exception was originally thrown at this call stack: - [External Code] - -Inner Exception 1: -InvalidOperationException: Error while validating the service descriptor 'ServiceType: MediatR.IRequestHandler`2[Application.UseCases.ExchangeRates.GetDailyExchangeRateQuery,FluentResults.Result`1[Application.UseCases.ExchangeRates.GetDailyExchangeRateResponse]] Lifetime: Transient ImplementationType: Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler': Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler'. - -Inner Exception 2: -InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Application.UseCases.ExchangeRates.GetDailyExchangeRateQueryHandler'. 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 4fcdb7030..000000000 --- a/jobs/Backend/Task/ExchangeRateUpdater.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net8.0 - - - - - - - - - \ No newline at end of file diff --git a/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs b/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs index d487a59eb..6db1a4fc5 100644 --- a/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs +++ b/jobs/Backend/Task/Infrastructure.Tests/ExchangeRateProviderTests.cs @@ -24,7 +24,7 @@ public ExchangeRateProviderTests() // Set up the configuration mock _mockConfig.Setup(x => x.Value).Returns(new CNBConfig { - BaseURL = "https://google.com", + BaseURL = "https://example.com", ExchangeRateURL = "", RefreshTimeHour = 23, RefreshTimeMinute = 30