add currency converter service and integrated it with vehicle enrollment management

This commit is contained in:
cuqmbr 2025-05-14 17:43:10 +03:00
parent 5ee8c9c5df
commit b1aceac750
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
18 changed files with 493 additions and 68 deletions

View File

@ -0,0 +1,12 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface CurrencyConverterService
{
Task<decimal> ConvertAsync(decimal amount, Currency from, Currency to,
CancellationToken cancellationToken);
Task<decimal> ConvertAsync(decimal amount, Currency from, Currency to,
DateTimeOffset time, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,8 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface SessionCurrencyService
{
public Currency Currency { get; }
}

View File

@ -2,6 +2,8 @@ using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper; using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.VehicleEnrollments namespace cuqmbr.TravelGuide.Application.VehicleEnrollments
.Queries.GetVehicleEnrollment; .Queries.GetVehicleEnrollment;
@ -12,12 +14,19 @@ public class GetVehicleEnrollmentQueryHandler :
private readonly UnitOfWork _unitOfWork; private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly SessionCurrencyService _sessionCurrencyService;
private readonly CurrencyConverterService _currencyConverterService;
public GetVehicleEnrollmentQueryHandler( public GetVehicleEnrollmentQueryHandler(
UnitOfWork unitOfWork, UnitOfWork unitOfWork,
IMapper mapper) IMapper mapper,
SessionCurrencyService sessionCurrencyService,
CurrencyConverterService currencyConverterService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_mapper = mapper; _mapper = mapper;
_sessionCurrencyService = sessionCurrencyService;
_currencyConverterService = currencyConverterService;
} }
public async Task<VehicleEnrollmentDto> Handle( public async Task<VehicleEnrollmentDto> Handle(
@ -51,6 +60,23 @@ public class GetVehicleEnrollmentQueryHandler :
.First(ra => ra.Id == rad.RouteAddressId); .First(ra => ra.Id == rad.RouteAddressId);
} }
// Convert currency
// TODO: Replace with AutoMapper Resolver
if (!_sessionCurrencyService.Currency.Equals(Currency.Default))
{
foreach (var rad in entity.RouteAddressDetails)
{
rad.CostToNextAddress = await _currencyConverterService
.ConvertAsync(rad.CostToNextAddress, entity.Currency,
_sessionCurrencyService.Currency, cancellationToken);
}
entity.Currency = _sessionCurrencyService.Currency;
}
_unitOfWork.Dispose(); _unitOfWork.Dispose();
return _mapper.Map<VehicleEnrollmentDto>(entity); return _mapper.Map<VehicleEnrollmentDto>(entity);

View File

@ -3,6 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper; using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models; using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions; using cuqmbr.TravelGuide.Application.Common.Extensions;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.VehicleEnrollments namespace cuqmbr.TravelGuide.Application.VehicleEnrollments
.Queries.GetVehicleEnrollmentsPage; .Queries.GetVehicleEnrollmentsPage;
@ -13,12 +15,19 @@ public class GetVehicleEnrollmentsPageQueryHandler :
private readonly UnitOfWork _unitOfWork; private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly SessionCurrencyService _sessionCurrencyService;
private readonly CurrencyConverterService _currencyConverterService;
public GetVehicleEnrollmentsPageQueryHandler( public GetVehicleEnrollmentsPageQueryHandler(
UnitOfWork unitOfWork, UnitOfWork unitOfWork,
IMapper mapper) IMapper mapper,
SessionCurrencyService sessionCurrencyService,
CurrencyConverterService currencyConverterService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_mapper = mapper; _mapper = mapper;
_sessionCurrencyService = sessionCurrencyService;
_currencyConverterService = currencyConverterService;
} }
public async Task<PaginatedList<VehicleEnrollmentDto>> Handle( public async Task<PaginatedList<VehicleEnrollmentDto>> Handle(
@ -55,13 +64,15 @@ public class GetVehicleEnrollmentsPageQueryHandler :
(request.ArrivalTimeGreaterThanOrEqual != null (request.ArrivalTimeGreaterThanOrEqual != null
? ?
e.DepartureTime.AddSeconds(e.RouteAddressDetails e.DepartureTime.AddSeconds(e.RouteAddressDetails
.Sum(rad => rad.TimeToNextAddress.TotalSeconds + rad.CurrentAddressStopTime.TotalSeconds)) >= .Sum(rad => rad.TimeToNextAddress.TotalSeconds +
rad.CurrentAddressStopTime.TotalSeconds)) >=
request.ArrivalTimeGreaterThanOrEqual request.ArrivalTimeGreaterThanOrEqual
: true) && : true) &&
(request.ArrivalTimeLessThanOrEqual != null (request.ArrivalTimeLessThanOrEqual != null
? ?
e.DepartureTime.AddSeconds(e.RouteAddressDetails e.DepartureTime.AddSeconds(e.RouteAddressDetails
.Sum(rad => rad.TimeToNextAddress.TotalSeconds + rad.CurrentAddressStopTime.TotalSeconds)) <= .Sum(rad => rad.TimeToNextAddress.TotalSeconds +
rad.CurrentAddressStopTime.TotalSeconds)) <=
request.ArrivalTimeLessThanOrEqual request.ArrivalTimeLessThanOrEqual
: true) && : true) &&
(request.TravelTimeGreaterThanOrEqual != null (request.TravelTimeGreaterThanOrEqual != null
@ -141,6 +152,25 @@ public class GetVehicleEnrollmentsPageQueryHandler :
} }
// Convert currency
// TODO: Replace with AutoMapper Resolver
if (!_sessionCurrencyService.Currency.Equals(Currency.Default))
{
foreach (var ve in paginatedList.Items)
{
foreach (var rad in ve.RouteAddressDetails)
{
rad.CostToNextAddress = await _currencyConverterService
.ConvertAsync(rad.CostToNextAddress, ve.Currency,
_sessionCurrencyService.Currency, cancellationToken);
}
ve.Currency = _sessionCurrencyService.Currency;
}
}
var mappedItems = _mapper var mappedItems = _mapper
.ProjectTo<VehicleEnrollmentDto>(paginatedList.Items.AsQueryable()); .ProjectTo<VehicleEnrollmentDto>(paginatedList.Items.AsQueryable());

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using cuqmbr.TravelGuide.Identity.Exceptions; using cuqmbr.TravelGuide.Identity.Exceptions;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace cuqmbr.TravelGuide.Configuration.Identity; namespace cuqmbr.TravelGuide.Configuration.Identity;
@ -38,6 +39,8 @@ public static class Configuration
"ef_migrations_history", "ef_migrations_history",
configuration.Datastore.PartitionName); configuration.Datastore.PartitionName);
}); });
options.ConfigureWarnings(w => w.Ignore(
RelationalEventId.PendingModelChangesWarning));
}); });
services services

View File

@ -1,19 +1,21 @@
// using Microsoft.Extensions.Configuration; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
// using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
//
// namespace cuqmbr.TravelGuide.Configuration.Infrastructure; namespace cuqmbr.TravelGuide.Configuration.Infrastructure;
//
// public static class Configuration public static class Configuration
// { {
// public static IServiceCollection ConfigureInfrastructure( public static IServiceCollection ConfigureInfrastructure(
// this IServiceCollection services, this IServiceCollection services)
// IConfiguration configuration) {
// { services
// services .AddHttpClient();
// .AddIdentity(configuration)
// .AddAuthenticationWithJwt(configuration) services
// .AddServices(); .AddScoped<
// CurrencyConverterService,
// return services; ExchangeApiCurrencyConverterService>();
// }
// } return services;
}
}

View File

@ -6,6 +6,7 @@ using cuqmbr.TravelGuide.Persistence.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using cuqmbr.TravelGuide.Persistence.PostgreSql; using cuqmbr.TravelGuide.Persistence.PostgreSql;
using cuqmbr.TravelGuide.Persistence.InMemory; using cuqmbr.TravelGuide.Persistence.InMemory;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace cuqmbr.TravelGuide.Configuration.Persistence; namespace cuqmbr.TravelGuide.Configuration.Persistence;
@ -34,6 +35,8 @@ public static class Configuration
"ef_migrations_history", "ef_migrations_history",
configuration.PartitionName); configuration.PartitionName);
}); });
options.ConfigureWarnings(w => w.Ignore(
RelationalEventId.PendingModelChangesWarning));
}); });
services services

View File

@ -495,13 +495,23 @@
"resolved": "9.0.4", "resolved": "9.0.4",
"contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA=="
}, },
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "9.0.4",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": { "Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.1", "resolved": "9.0.4",
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "8.0.2" "Microsoft.Extensions.Options": "9.0.4"
} }
}, },
"Microsoft.Extensions.FileProviders.Abstractions": { "Microsoft.Extensions.FileProviders.Abstractions": {
@ -539,6 +549,19 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.2" "Microsoft.Extensions.Logging.Abstractions": "8.0.2"
} }
}, },
"Microsoft.Extensions.Http": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Diagnostics": "9.0.4",
"Microsoft.Extensions.Logging": "9.0.4",
"Microsoft.Extensions.Logging.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "9.0.4"
}
},
"Microsoft.Extensions.Identity.Core": { "Microsoft.Extensions.Identity.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.4", "resolved": "9.0.4",
@ -696,6 +719,11 @@
"System.Security.Principal.Windows": "4.5.0" "System.Security.Principal.Windows": "4.5.0"
} }
}, },
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Npgsql": { "Npgsql": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.3",
@ -832,7 +860,9 @@
"infrastructure": { "infrastructure": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Application": "[1.0.0, )" "Application": "[1.0.0, )",
"Microsoft.Extensions.Http": "[9.0.4, )",
"Newtonsoft.Json": "[13.0.3, )"
} }
}, },
"persistence": { "persistence": {

View File

@ -3,16 +3,23 @@ namespace cuqmbr.TravelGuide.Domain.Enums;
// Do not forget to update the schema of your database when changing // Do not forget to update the schema of your database when changing
// this class (if you use it with a database) // this class (if you use it with a database)
// ISO-4217 Country Codes dated 2025-03-31 // ISO-4217 Currency Codes dated 2025-03-31
public abstract class Currency : Enumeration<Currency> public abstract class Currency : Enumeration<Currency>
{ {
public static readonly Currency Default = new DefaultCurrency();
public static readonly Currency USD = new USDCurrency(); public static readonly Currency USD = new USDCurrency();
public static readonly Currency EUR = new EURCurrency(); public static readonly Currency EUR = new EURCurrency();
public static readonly Currency UAH = new UAHCurrency(); public static readonly Currency UAH = new UAHCurrency();
protected Currency(int value, string name) : base(value, name) { } protected Currency(int value, string name) : base(value, name) { }
// When no currency is specified
private sealed class DefaultCurrency : Currency
{
public DefaultCurrency() : base(Int32.MaxValue, "DEFAULT") { }
}
private sealed class USDCurrency : Currency private sealed class USDCurrency : Currency
{ {
public USDCurrency() : base(840, "USD") { } public USDCurrency() : base(840, "USD") { }

View File

@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.Localizer.Json" Version="1.0.1" /> <PackageReference Include="AspNetCore.Localizer.Json" Version="1.0.1" />
<PackageReference Include="MicroElements.Swashbuckle.FluentValidation" Version="6.1.0" /> <!-- <PackageReference Include="MicroElements.Swashbuckle.FluentValidation" Version="6.1.0" /> -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -1,5 +1,6 @@
using cuqmbr.TravelGuide.Configuration.Persistence; using cuqmbr.TravelGuide.Configuration.Persistence;
using cuqmbr.TravelGuide.Configuration.Application; using cuqmbr.TravelGuide.Configuration.Application;
using cuqmbr.TravelGuide.Configuration.Infrastructure;
using cuqmbr.TravelGuide.Configuration.Identity; using cuqmbr.TravelGuide.Configuration.Identity;
using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Configuration;
using cuqmbr.TravelGuide.Configuration.Logging; using cuqmbr.TravelGuide.Configuration.Logging;
@ -9,7 +10,7 @@ using cuqmbr.TravelGuide.HttpApi.Middlewares;
using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters;
using System.Net; using System.Net;
using Swashbuckle.AspNetCore.SwaggerUI; using Swashbuckle.AspNetCore.SwaggerUI;
using MicroElements.Swashbuckle.FluentValidation.AspNetCore; // using MicroElements.Swashbuckle.FluentValidation.AspNetCore;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using System.Reflection; using System.Reflection;
@ -25,12 +26,13 @@ services.ConfigureLogging();
services.ConfigurePersistence(); services.ConfigurePersistence();
services.ConfigureIdentity(); services.ConfigureIdentity();
// services.AddInfrastructure(); services.ConfigureInfrastructure();
services.ConfigureApplication(); services.ConfigureApplication();
services.AddScoped<SessionUserService, AspNetSessionUserService>(); services.AddScoped<SessionUserService, AspNetSessionUserService>();
services.AddScoped<TimeZoneService, AspNetTimeZoneService>();
services.AddScoped<CultureService, AspNetCultureService>(); services.AddScoped<CultureService, AspNetCultureService>();
services.AddScoped<TimeZoneService, AspNetTimeZoneService>();
services.AddScoped<SessionCurrencyService, AspNetSessionCurrencyService>();
services.AddControllers(); services.AddControllers();
@ -82,8 +84,18 @@ services.AddSwaggerGen(options =>
"from IANA tz database (https://www.iana.org/time-zones).", "from IANA tz database (https://www.iana.org/time-zones).",
Type = SecuritySchemeType.ApiKey Type = SecuritySchemeType.ApiKey
}); });
// Set Accept-Currency header in Authorize window
options.OperationFilter<AcceptCurrencyHeaderOperationFilter>();
options.AddSecurityDefinition("Accept-Currency", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Accept-Currency",
Description = "ISO-4217 Currency Code.",
Type = SecuritySchemeType.ApiKey
});
}); });
services.AddFluentValidationRulesToSwagger(); // services.AddFluentValidationRulesToSwagger();
services.AddScoped<ThreadCultureSetterMiddleware>(); services.AddScoped<ThreadCultureSetterMiddleware>();

View File

@ -0,0 +1,38 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.HttpApi.Services;
public sealed class AspNetSessionCurrencyService : SessionCurrencyService
{
private readonly HttpContext _httpContext;
public AspNetSessionCurrencyService(IHttpContextAccessor httpContextAccessor)
{
_httpContext = httpContextAccessor.HttpContext;
}
public Currency Currency
{
get
{
string? headerCurrencyCode =
_httpContext.Request.Headers["Accept-Currency"];
string currencyCode =
headerCurrencyCode?.ToUpper() ??
cuqmbr.TravelGuide.Domain.Enums.Currency.Default.Name;
var resultCurrency =
cuqmbr.TravelGuide.Domain.Enums.Currency.FromName(currencyCode);
if (resultCurrency == null)
{
resultCurrency =
cuqmbr.TravelGuide.Domain.Enums.Currency.Default;
}
return resultCurrency;
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters;
public class AcceptCurrencyHeaderOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// TODO: Remove security requirements
operation.Security ??= new List<OpenApiSecurityRequirement>();
var acceptCurrency = new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Accept-Currency"
}
};
operation.Security.Add(new OpenApiSecurityRequirement
{
[acceptCurrency] = new List<string>()
});
if (operation.Parameters == null)
operation.Parameters = new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Currency",
Description = "ISO-4217 Currency Code.",
In = ParameterLocation.Header,
Schema = new OpenApiSchema { Type = "String" },
Required = false
});
}
}

View File

@ -13,17 +13,6 @@
"Microsoft.Extensions.Localization": "9.0.0" "Microsoft.Extensions.Localization": "9.0.0"
} }
}, },
"MicroElements.Swashbuckle.FluentValidation": {
"type": "Direct",
"requested": "[6.1.0, )",
"resolved": "6.1.0",
"contentHash": "VzqApLPY8xIqXDvWqRuvoDYEoCHII42c4LgvLO3BikKoIVcECD+ZSG727I7yPZ/J07VEoa8aJddoqUtSm4E4gw==",
"dependencies": {
"FluentValidation": "[10.0.0, 12.0.0)",
"MicroElements.OpenApi.FluentValidation": "6.1.0",
"Swashbuckle.AspNetCore.SwaggerGen": "[6.3.0, 8.0.0)"
}
},
"Microsoft.AspNetCore.OpenApi": { "Microsoft.AspNetCore.OpenApi": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.4, )", "requested": "[9.0.4, )",
@ -159,17 +148,6 @@
"resolved": "2.0.1", "resolved": "2.0.1",
"contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ=="
}, },
"MicroElements.OpenApi.FluentValidation": {
"type": "Transitive",
"resolved": "6.1.0",
"contentHash": "qJPAI3bL70ND6fIi4bGqQf/lpV9wUod23R7JVhVVYRonoT/ZGmPBjMbO//IPPy3yP2F21iMX05brYjyU4/WqwQ==",
"dependencies": {
"FluentValidation": "[10.0.0, 12.0.0)",
"Microsoft.AspNetCore.Http.Abstractions": "2.2.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0"
}
},
"Microsoft.AspNetCore.Authentication": { "Microsoft.AspNetCore.Authentication": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.3.0", "resolved": "2.3.0",
@ -634,13 +612,23 @@
"resolved": "9.0.4", "resolved": "9.0.4",
"contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA=="
}, },
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "9.0.4",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": { "Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.1", "resolved": "9.0.4",
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "8.0.2" "Microsoft.Extensions.Options": "9.0.4"
} }
}, },
"Microsoft.Extensions.FileProviders.Abstractions": { "Microsoft.Extensions.FileProviders.Abstractions": {
@ -678,6 +666,19 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.2" "Microsoft.Extensions.Logging.Abstractions": "8.0.2"
} }
}, },
"Microsoft.Extensions.Http": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Diagnostics": "9.0.4",
"Microsoft.Extensions.Logging": "9.0.4",
"Microsoft.Extensions.Logging.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "9.0.4"
}
},
"Microsoft.Extensions.Identity.Core": { "Microsoft.Extensions.Identity.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.4", "resolved": "9.0.4",
@ -873,6 +874,11 @@
"System.CodeDom": "6.0.0" "System.CodeDom": "6.0.0"
} }
}, },
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Npgsql": { "Npgsql": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.3",
@ -1119,7 +1125,9 @@
"infrastructure": { "infrastructure": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Application": "[1.0.0, )" "Application": "[1.0.0, )",
"Microsoft.Extensions.Http": "[9.0.4, )",
"Newtonsoft.Json": "[13.0.3, )"
} }
}, },
"persistence": { "persistence": {

View File

@ -10,6 +10,11 @@
<ProjectReference Include="..\Application\Application.csproj" /> <ProjectReference Include="..\Application\Application.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup> </PropertyGroup>

View File

@ -0,0 +1,101 @@
using System.Globalization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using Newtonsoft.Json;
// https://github.com/fawazahmed0/exchange-api
public sealed class ExchangeApiCurrencyConverterService :
CurrencyConverterService
{
private readonly
IDictionary<
DateOnly, IDictionary<
Currency, IDictionary<
Currency, decimal>>> _cache;
private const string urlFormat = "https://cdn.jsdelivr.net/" +
"npm/@fawazahmed0/currency-api@{0}/v1/currencies/{1}.json";
private readonly IHttpClientFactory _httpClientFactory;
public ExchangeApiCurrencyConverterService(
IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_cache =
new Dictionary<
DateOnly, IDictionary<
Currency, IDictionary<
Currency, decimal>>>();
}
public async Task<decimal> ConvertAsync(decimal amount, Currency from,
Currency to, CancellationToken cancellationToken)
{
return await ConvertAsync(amount, from, to,
DateTimeOffset.UtcNow, cancellationToken);
}
public async Task<decimal> ConvertAsync(decimal amount, Currency from,
Currency to, DateTimeOffset time, CancellationToken cancellationToken)
{
if (from.Equals(to))
{
return amount;
}
var requestDate = DateOnly.FromDateTime(time.ToUniversalTime().Date);
// Return cached value if available
if (_cache.Keys.Contains(requestDate) &&
_cache[requestDate].Keys.Contains(from) &&
_cache[requestDate][from].Keys.Contains(to))
{
return amount * _cache[requestDate][from][to];
}
// Retreive new value from api
var httpClient = _httpClientFactory.CreateClient();
var requestDateString =
requestDate == DateOnly.FromDateTime(DateTime.UtcNow) ?
"latest" :
requestDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var url = String.Format(urlFormat,
requestDateString, from.Name.ToLower());
var jsonString =
await httpClient.GetStringAsync(url, cancellationToken);
var obj = JsonConvert.DeserializeObject<dynamic>(jsonString);
decimal rate = obj[from.Name.ToLower()][to.Name.ToLower()];
// Cache new value
if (!_cache.Keys.Contains(requestDate))
{
_cache.Add(requestDate,
new Dictionary<Currency, IDictionary<Currency, decimal>>());
}
if (!_cache[requestDate].Keys.Contains(from))
{
_cache[requestDate].Add(from, new Dictionary<Currency, decimal>());
}
if (!_cache[requestDate][from].Keys.Contains(to))
{
_cache[requestDate][from].Add(to, rate);
}
return amount * rate;
}
}

View File

@ -2,6 +2,26 @@
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net9.0": { "net9.0": {
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[9.0.4, )",
"resolved": "9.0.4",
"contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Diagnostics": "9.0.4",
"Microsoft.Extensions.Logging": "9.0.4",
"Microsoft.Extensions.Logging.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "9.0.4"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"AspNetCore.Localizer.Json": { "AspNetCore.Localizer.Json": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.0.1", "resolved": "1.0.1",
@ -97,6 +117,31 @@
"Microsoft.Extensions.Primitives": "9.0.0" "Microsoft.Extensions.Primitives": "9.0.0"
} }
}, },
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
"Microsoft.Extensions.Primitives": "9.0.4"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.4"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
}
},
"Microsoft.Extensions.DependencyInjection": { "Microsoft.Extensions.DependencyInjection": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.4", "resolved": "9.0.4",
@ -110,6 +155,25 @@
"resolved": "9.0.4", "resolved": "9.0.4",
"contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
}, },
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "9.0.4",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "9.0.4"
}
},
"Microsoft.Extensions.Localization": { "Microsoft.Extensions.Localization": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.0", "resolved": "9.0.0",
@ -153,6 +217,18 @@
"Microsoft.Extensions.Primitives": "9.0.4" "Microsoft.Extensions.Primitives": "9.0.4"
} }
}, },
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
"Microsoft.Extensions.Configuration.Binder": "9.0.4",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "9.0.4",
"Microsoft.Extensions.Primitives": "9.0.4"
}
},
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.4", "resolved": "9.0.4",

View File

@ -497,13 +497,23 @@
"resolved": "9.0.4", "resolved": "9.0.4",
"contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA=="
}, },
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "9.0.4",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": { "Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.1", "resolved": "9.0.4",
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "8.0.2" "Microsoft.Extensions.Options": "9.0.4"
} }
}, },
"Microsoft.Extensions.FileProviders.Abstractions": { "Microsoft.Extensions.FileProviders.Abstractions": {
@ -541,6 +551,19 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.2" "Microsoft.Extensions.Logging.Abstractions": "8.0.2"
} }
}, },
"Microsoft.Extensions.Http": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "ezelU6HJgmq4862YoWuEbHGSV+JnfnonTSbNSJVh6n6wDehyiJn4hBtcK7rGbf2KO3QeSvK5y8E7uzn1oaRH5w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
"Microsoft.Extensions.Diagnostics": "9.0.4",
"Microsoft.Extensions.Logging": "9.0.4",
"Microsoft.Extensions.Logging.Abstractions": "9.0.4",
"Microsoft.Extensions.Options": "9.0.4"
}
},
"Microsoft.Extensions.Identity.Core": { "Microsoft.Extensions.Identity.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.4", "resolved": "9.0.4",
@ -772,8 +795,8 @@
}, },
"Newtonsoft.Json": { "Newtonsoft.Json": {
"type": "Transitive", "type": "Transitive",
"resolved": "13.0.1", "resolved": "13.0.3",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
}, },
"Npgsql": { "Npgsql": {
"type": "Transitive", "type": "Transitive",
@ -1005,7 +1028,9 @@
"infrastructure": { "infrastructure": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Application": "[1.0.0, )" "Application": "[1.0.0, )",
"Microsoft.Extensions.Http": "[9.0.4, )",
"Newtonsoft.Json": "[13.0.3, )"
} }
}, },
"persistence": { "persistence": {