From e3dd2dd58233720a65b30c09ed6c319c662412a8 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Sat, 24 May 2025 11:17:43 +0300 Subject: [PATCH] add all vehicle enrollment with transfers search --- .../Queries/SearchAll/SearchAllQuery.cs | 39 ++ .../SearchAll/SearchAllQueryAuthorizer.cs | 33 ++ .../SearchAll/SearchAllQueryHandler.cs | 522 ++++++++++++++++++ .../SearchAll/SearchAllQueryValidator.cs | 43 ++ .../SearchShortestQueryHandler.cs | 126 +++-- .../VehicleEnrollmentSearchDto.cs | 4 + .../ViewModels/SearchAllViewModel.cs | 32 ++ .../VehicleEnrollmentSearchController.cs | 71 ++- 8 files changed, 815 insertions(+), 55 deletions(-) create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs create mode 100644 src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs create mode 100644 src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs new file mode 100644 index 0000000..5f71e49 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQuery.cs @@ -0,0 +1,39 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public record SearchAllQuery : + IRequest> +{ + public Guid DepartureAddressGuid { get; set; } + + public Guid ArrivalAddressGuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public string Sort { get; set; } = String.Empty; + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public decimal? CostGreaterThanOrEqualTo { get; set; } + + public decimal? CostLessThanOrEqualTo { get; set; } + + public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + + public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs new file mode 100644 index 0000000..19ba695 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryAuthorizer.cs @@ -0,0 +1,33 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public class SearchAllQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public SearchAllQueryAuthorizer( + SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(SearchAllQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs new file mode 100644 index 0000000..4cd7fa8 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs @@ -0,0 +1,522 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using QuikGraph; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +// TODO: Add configurable time between transfers. +public class SearchAllQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + private readonly SessionCurrencyService _sessionCurrencyService; + private readonly CurrencyConverterService _currencyConverterService; + + private readonly SessionTimeZoneService _sessionTimeZoneService; + + public SearchAllQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper, + SessionCurrencyService sessionCurrencyService, + CurrencyConverterService currencyConverterService, + SessionTimeZoneService sessionTimeZoneService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _sessionCurrencyService = sessionCurrencyService; + _currencyConverterService = currencyConverterService; + _sessionTimeZoneService = sessionTimeZoneService; + } + + public async Task> Handle( + SearchAllQuery request, + CancellationToken cancellationToken) + { + // Get related data + + var zeroTime = TimeOnly.FromTimeSpan(TimeSpan.Zero); + var departureDate = + new DateTimeOffset(request.DepartureDate, zeroTime, TimeSpan.Zero); + + var range = TimeSpan.FromDays(3); + var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository + .GetPageAsync( + e => + e.DepartureTime >= departureDate.Subtract(range) && + e.DepartureTime <= departureDate.Add(range) && + request.VehicleTypes.Contains(e.Vehicle.VehicleType), + e => e.Route, + 1, int.MaxValue, cancellationToken)) + .Items; + + if (vehicleEnrollments.Count == 0) + { + throw new NotFoundException(); + } + + var vehicleEnrollmentIds = vehicleEnrollments.Select(e => e.Id); + var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository + .GetPageAsync( + e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId), + e => e.RouteAddress.Address.City.Region.Country, + 1, int.MaxValue, cancellationToken)) + .Items; + + + // Hydrate vehicle enrollments with route address details + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + vehicleEnrollment.RouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order) + .ToArray(); + } + + + // Creat and fill graph data structure + + var graph = new AdjacencyGraph< + Address, TaggedEdge>(); + + foreach (var vehicleEnrollment in vehicleEnrollments) + { + var vehicleEnrollmentRouteAddressDetails = routeAddressDetails + .Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id) + .OrderBy(e => e.RouteAddress.Order); + + for (int i = 1; i < vehicleEnrollmentRouteAddressDetails.Count(); i++) + { + var sourceRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i - 1); + var targetRouteAddressDetail = + vehicleEnrollmentRouteAddressDetails.ElementAt(i); + + var sourceAddress = + sourceRouteAddressDetail.RouteAddress.Address; + var targetAddress = + targetRouteAddressDetail.RouteAddress.Address; + + var weight = sourceRouteAddressDetail.CostToNextAddress; + + graph.AddVerticesAndEdge( + new TaggedEdge( + sourceAddress, targetAddress, sourceRouteAddressDetail)); + } + } + + + // Find paths + + var departureAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid) + .RouteAddress.Address; + var arrivalAddress = routeAddressDetails + .First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid) + .RouteAddress.Address; + + + var paths = new List>>(); + var queue = new Queue<(TaggedEdge edge, List> path)>(); + + foreach (var edge in graph.OutEdges(departureAddress)) + { + queue.Enqueue((edge, new List>() { edge })); + } + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + if (current.edge.Target.Equals(arrivalAddress)) + { + paths.Add(current.path); + continue; + } + + foreach (var edge in graph.OutEdges(current.edge.Target)) + { + var neighbor = edge; + if (!current.path.Contains(neighbor)) + { + var newPath = new List>(current.path) { neighbor }; + queue.Enqueue((neighbor, newPath)); + } + } + } + + + // Create DTO object + + var result = new List(); + + foreach (var path in paths) + { + var vehicleEnrollmentDtos = + new List(); + + var addressDtos = new List(); + + var firstRouteAddressId = path.First().Tag.RouteAddressId; + + Guid lastRouteAddressGuid; + long lastRouteAddressId; + + decimal vehicleEnrollmentCost; + Currency vehicleEnrollmentCurrency; + + decimal costToNextAddress; + + short addressOrder = 1; + short enrollmentOrder = 1; + + Address source; + Address nextSource; + + Address target; + Address nextTarget; + + RouteAddressDetail tag; + RouteAddressDetail nextTag; + + for (int i = 0; i < path.Count - 1; i++) + { + var edge = path[i]; + var nextEdge = path[i+1]; + + source = edge.Source; + nextSource = nextEdge.Source; + + tag = edge.Tag; + nextTag = nextEdge.Tag; + + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = costToNextAddress, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = tag.RouteAddress.Guid + }); + + addressOrder++; + + + if (tag.VehicleEnrollmentId != nextTag.VehicleEnrollmentId) + { + target = edge.Target; + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos + }); + + + firstRouteAddressId = nextTag.RouteAddressId; + addressDtos = new List(); + addressOrder = 1; + enrollmentOrder++; + } + } + + // --------------- + + source = path.Select(e => e.Source).Last(); + target = path.Select(e => e.Target).Last(); + tag = path.Select(e => e.Tag).Last(); + + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = source.Guid, + Name = source.Name, + Longitude = source.Latitude, + Latitude = source.Longitude, + CountryUuid = source.City.Region.Country.Guid, + CountryName = source.City.Region.Country.Name, + RegionUuid = source.City.Region.Guid, + RegionName = source.City.Region.Name, + CityUuid = source.City.Guid, + CityName = source.City.Name, + TimeToNextAddress = tag.TimeToNextAddress, + CostToNextAddress = 0, + CurrentAddressStopTime = tag.CurrentAddressStopTime, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressGuid = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Guid; + + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() + { + Uuid = target.Guid, + Name = target.Name, + Longitude = target.Latitude, + Latitude = target.Longitude, + CountryUuid = target.City.Region.Country.Guid, + CountryName = target.City.Region.Country.Name, + RegionUuid = target.City.Region.Guid, + RegionName = target.City.Region.Name, + CityUuid = target.City.Guid, + CityName = target.City.Name, + TimeToNextAddress = TimeSpan.Zero, + CostToNextAddress = 0, + CurrentAddressStopTime = TimeSpan.Zero, + Order = addressOrder, + RouteAddressUuid = lastRouteAddressGuid + }); + + + lastRouteAddressId = vehicleEnrollments + .Single(e => e.Id == tag.VehicleEnrollmentId) + .RouteAddressDetails + .Select(e => e.RouteAddress) + .OrderBy(e => e.Order) + .SkipWhile(e => e.Order != tag.RouteAddress.Order) + .Take(2) + .ElementAt(1) + .Id; + + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + + vehicleEnrollmentDtos.Add( + new VehicleEnrollmentSearchVehicleEnrollmentDto() + { + DepartureTime = tag.VehicleEnrollment + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = tag.VehicleEnrollment + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = tag.VehicleEnrollment + .GetTravelTime(firstRouteAddressId, + lastRouteAddressId), + TimeMoving = tag.VehicleEnrollment + .GetTimeMoving(firstRouteAddressId, + lastRouteAddressId), + TimeInStops = tag.VehicleEnrollment + .GetTimeInStops(firstRouteAddressId, + lastRouteAddressId), + NumberOfStops = tag.VehicleEnrollment + .GetNumberOfStops(firstRouteAddressId, + lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, + VehicleType = source.VehicleType.Name, + Uuid = tag.VehicleEnrollment.Guid, + Order = enrollmentOrder, + Addresses = addressDtos + }); + + // --------------- + + + var departureTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).First().DepartureTime; + var arrivalTime = vehicleEnrollmentDtos + .OrderBy(e => e.Order).Last().ArrivalTime; + var timeInStops = vehicleEnrollmentDtos + .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); + var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, (sum, next) => sum += next.Cost); + + result.Add(new VehicleEnrollmentSearchDto() + { + DepartureTime = departureTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = arrivalTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + TravelTime = arrivalTime - departureTime, + TimeInStops = timeInStops, + NumberOfTransfers = numberOfTransfers, + Currency = _sessionCurrencyService.Currency.Name, + Cost = cost, + Enrollments = vehicleEnrollmentDtos + }); + } + + + if (result.Count == 0) + { + throw new NotFoundException(); + } + + + var filteredResult = result.Where(e => + (request.TravelTimeGreaterThanOrEqualTo != null + ? e.TravelTime >= request.TravelTimeGreaterThanOrEqualTo + : true) && + (request.TravelTimeLessThanOrEqualTo != null + ? e.TravelTime <= request.TravelTimeLessThanOrEqualTo + : true) && + (request.CostGreaterThanOrEqualTo != null + ? e.Cost >= request.CostGreaterThanOrEqualTo + : true) && + (request.CostLessThanOrEqualTo != null + ? e.Cost <= request.CostLessThanOrEqualTo + : true) && + (request.NumberOfTransfersGreaterThanOrEqualTo != null + ? e.NumberOfTransfers >= request.NumberOfTransfersGreaterThanOrEqualTo + : true) && + (request.NumberOfTransfersLessThanOrEqualTo != null + ? e.NumberOfTransfers <= request.NumberOfTransfersLessThanOrEqualTo + : true) && + (request.DepartureTimeGreaterThanOrEqualTo != null + ? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqualTo + : true) && + (request.DepartureTimeLessThanOrEqualTo != null + ? e.DepartureTime <= request.DepartureTimeLessThanOrEqualTo + : true) && + (request.ArrivalTimeGreaterThanOrEqualTo != null + ? e.ArrivalTime >= request.ArrivalTimeGreaterThanOrEqualTo + : true) && + (request.ArrivalTimeLessThanOrEqualTo != null + ? e.ArrivalTime <= request.ArrivalTimeLessThanOrEqualTo + : true)); + + var sortedResult = QueryableExtension + .ApplySort(filteredResult.AsQueryable(), request.Sort); + + + return sortedResult; + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs new file mode 100644 index 0000000..e12f958 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application + .VehicleEnrollmentSearch.Queries.SearchAll; + +public class SearchAllQueryValidator : + AbstractValidator +{ + public SearchAllQueryValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.DepartureAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.ArrivalAddressGuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.DepartureDate) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow)) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + DateOnly.FromDateTime(DateTime.UtcNow))); + + RuleForEach(v => v.VehicleTypes) + .Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + VehicleType.Enumerations.Values.Select(e => e.Name)))); + } +} diff --git a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs index 5c5f0b3..c1acabb 100644 --- a/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs +++ b/src/Application/VehicleEnrollmentSearch/Queries/SearchShortest/SearchShortestQueryHandler.cs @@ -6,11 +6,11 @@ using AutoMapper; using QuikGraph; using QuikGraph.Algorithms; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; namespace cuqmbr.TravelGuide.Application .VehicleEnrollmentSearch.Queries.SearchShortest; -// TODO: Refactor. // TODO: Add configurable time between transfers. public class SearchShortestQueryHandler : IRequestHandler @@ -21,16 +21,20 @@ public class SearchShortestQueryHandler : private readonly SessionCurrencyService _sessionCurrencyService; private readonly CurrencyConverterService _currencyConverterService; + private readonly SessionTimeZoneService _sessionTimeZoneService; + public SearchShortestQueryHandler( UnitOfWork unitOfWork, IMapper mapper, SessionCurrencyService sessionCurrencyService, - CurrencyConverterService currencyConverterService) + CurrencyConverterService currencyConverterService, + SessionTimeZoneService sessionTimeZoneService) { _unitOfWork = unitOfWork; _mapper = mapper; _sessionCurrencyService = sessionCurrencyService; _currencyConverterService = currencyConverterService; + _sessionTimeZoneService = sessionTimeZoneService; } public async Task Handle( @@ -163,10 +167,14 @@ public class SearchShortestQueryHandler : long lastRouteAddressId; Guid lastRouteAddressGuid; + decimal vehicleEnrollmentCost; + Currency vehicleEnrollmentCurrency; + + decimal costToNextAddress; + var addressDtos = new List(); + var addressOrder = (short)1; - var enrollmentTravelTime = TimeSpan.Zero; - var enrollmentCost = (decimal)0; var enrollmentOrder = (short)1; Address source; @@ -181,13 +189,10 @@ public class SearchShortestQueryHandler : nextTag = path.Select(e => e.Tag).ElementAt(i+1); - totalTravelTime += - tag.TimeToNextAddress + tag.CurrentAddressStopTime; - enrollmentTravelTime += - tag.TimeToNextAddress + tag.CurrentAddressStopTime; - - totalCost += tag.CostToNextAddress; - enrollmentCost += tag.CostToNextAddress; + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); addressDtos.Add(new VehicleEnrollmentSearchAddressDto() { @@ -202,7 +207,7 @@ public class SearchShortestQueryHandler : CityUuid = source.City.Guid, CityName = source.City.Name, TimeToNextAddress = tag.TimeToNextAddress, - CostToNextAddress = tag.CostToNextAddress, + CostToNextAddress = costToNextAddress, CurrentAddressStopTime = tag.CurrentAddressStopTime, Order = addressOrder, RouteAddressUuid = tag.RouteAddress.Guid @@ -255,13 +260,29 @@ public class SearchShortestQueryHandler : .ElementAt(1) .Id; + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + vehicleEnrollmentDtos.Add( new VehicleEnrollmentSearchVehicleEnrollmentDto() { DepartureTime = tag.VehicleEnrollment - .GetDepartureTime(firstRouteAddressId), + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), ArrivalTime = tag.VehicleEnrollment - .GetArrivalTime(lastRouteAddressId), + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), TravelTime = tag.VehicleEnrollment .GetTravelTime(firstRouteAddressId, lastRouteAddressId), @@ -274,10 +295,8 @@ public class SearchShortestQueryHandler : NumberOfStops = tag.VehicleEnrollment .GetNumberOfStops(firstRouteAddressId, lastRouteAddressId), - Currency = tag.VehicleEnrollment.Currency.Name, - Cost = tag.VehicleEnrollment - .GetCost(firstRouteAddressId, - lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, @@ -288,8 +307,6 @@ public class SearchShortestQueryHandler : addressDtos = new List(); addressOrder = (short)1; - enrollmentTravelTime = TimeSpan.Zero; - enrollmentCost = (decimal)0; enrollmentOrder++; } } @@ -299,6 +316,12 @@ public class SearchShortestQueryHandler : tag = path.Select(e => e.Tag).Last(); nextTag = path.Select(e => e.Tag).Last(); + + costToNextAddress = await _currencyConverterService + .ConvertAsync(tag.CostToNextAddress, + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, cancellationToken); + addressDtos.Add(new VehicleEnrollmentSearchAddressDto() { Uuid = source.Guid, @@ -312,12 +335,14 @@ public class SearchShortestQueryHandler : CityUuid = source.City.Guid, CityName = source.City.Name, TimeToNextAddress = tag.TimeToNextAddress, - CostToNextAddress = tag.CostToNextAddress, + CostToNextAddress = costToNextAddress, CurrentAddressStopTime = tag.CurrentAddressStopTime, Order = addressOrder, RouteAddressUuid = tag.RouteAddress.Guid }); + addressOrder++; + lastRouteAddressGuid = vehicleEnrollments .Single(e => e.Id == tag.VehicleEnrollmentId) .RouteAddressDetails @@ -328,7 +353,6 @@ public class SearchShortestQueryHandler : .ElementAt(1) .Guid; - addressOrder++; addressDtos.Add(new VehicleEnrollmentSearchAddressDto() { Uuid = target.Guid, @@ -358,13 +382,29 @@ public class SearchShortestQueryHandler : .ElementAt(1) .Id; + vehicleEnrollmentCost = await _currencyConverterService + .ConvertAsync( + tag.VehicleEnrollment + .GetCost(firstRouteAddressId, + lastRouteAddressId), + tag.VehicleEnrollment.Currency, + _sessionCurrencyService.Currency, + cancellationToken); + + vehicleEnrollmentCurrency = + _sessionCurrencyService.Currency.Equals(Currency.Default) ? + tag.VehicleEnrollment.Currency : + _sessionCurrencyService.Currency; + vehicleEnrollmentDtos.Add( new VehicleEnrollmentSearchVehicleEnrollmentDto() { DepartureTime = tag.VehicleEnrollment - .GetDepartureTime(firstRouteAddressId), + .GetDepartureTime(firstRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), ArrivalTime = tag.VehicleEnrollment - .GetArrivalTime(lastRouteAddressId), + .GetArrivalTime(lastRouteAddressId) + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), TravelTime = tag.VehicleEnrollment .GetTravelTime(firstRouteAddressId, lastRouteAddressId), @@ -377,10 +417,8 @@ public class SearchShortestQueryHandler : NumberOfStops = tag.VehicleEnrollment .GetNumberOfStops(firstRouteAddressId, lastRouteAddressId), - Currency = tag.VehicleEnrollment.Currency.Name, - Cost = tag.VehicleEnrollment - .GetCost(firstRouteAddressId, - lastRouteAddressId), + Currency = vehicleEnrollmentCurrency.Name, + Cost = vehicleEnrollmentCost, VehicleType = source.VehicleType.Name, Uuid = tag.VehicleEnrollment.Guid, Order = enrollmentOrder, @@ -393,28 +431,6 @@ public class SearchShortestQueryHandler : } - foreach (var vehicleEnrollmentDto in vehicleEnrollmentDtos) - { - foreach (var addressDto in vehicleEnrollmentDto.Addresses) - { - addressDto.CostToNextAddress = await _currencyConverterService - .ConvertAsync(addressDto.CostToNextAddress, - vehicleEnrollments - .First(e => e.Guid == vehicleEnrollmentDto.Uuid) - .Currency, - _sessionCurrencyService.Currency, cancellationToken); - } - - vehicleEnrollmentDto.Currency = vehicleEnrollmentDto.Currency; - vehicleEnrollmentDto.Cost = vehicleEnrollmentDto.Addresses - .Aggregate((decimal)0, - (sum, next) => sum += next.CostToNextAddress); - } - - var cost = vehicleEnrollmentDtos - .Aggregate((decimal)0, - (sum, next) => sum += next.Cost); - var departureTime = vehicleEnrollmentDtos .OrderBy(e => e.Order).First().DepartureTime; var arrivalTime = vehicleEnrollmentDtos @@ -422,14 +438,20 @@ public class SearchShortestQueryHandler : var timeInStops = vehicleEnrollmentDtos .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; + var cost = vehicleEnrollmentDtos + .Aggregate((decimal)0, (sum, next) => sum += next.Cost); return new VehicleEnrollmentSearchDto() { - DepartureTime = departureTime, - ArrivalTime = arrivalTime, + DepartureTime = departureTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), + ArrivalTime = arrivalTime + .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset), TravelTime = arrivalTime - departureTime, TimeInStops = timeInStops, NumberOfTransfers = numberOfTransfers, + Currency = _sessionCurrencyService.Currency.Name, + Cost = cost, Enrollments = vehicleEnrollmentDtos }; } diff --git a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs index 2409797..e8fc53b 100644 --- a/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs +++ b/src/Application/VehicleEnrollmentSearch/VehicleEnrollmentSearchDto.cs @@ -12,6 +12,10 @@ public sealed class VehicleEnrollmentSearchDto public int NumberOfTransfers { get; set; } + public string Currency { get; set; } + + public decimal Cost { get; set; } + public ICollection Enrollments { get; set; } } diff --git a/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs new file mode 100644 index 0000000..d072655 --- /dev/null +++ b/src/Application/VehicleEnrollmentSearch/ViewModels/SearchAllViewModel.cs @@ -0,0 +1,32 @@ +namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; + +public sealed class SearchAllViewModel +{ + public Guid DepartureAddressUuid { get; set; } + + public Guid ArrivalAddressUuid { get; set; } + + public DateOnly DepartureDate { get; set; } + + public HashSet VehicleTypes { get; set; } + + public TimeSpan? TravelTimeGreaterThanOrEqualTo { get; set; } + + public TimeSpan? TravelTimeLessThanOrEqualTo { get; set; } + + public decimal? CostGreaterThanOrEqualTo { get; set; } + + public decimal? CostLessThanOrEqualTo { get; set; } + + public short? NumberOfTransfersGreaterThanOrEqualTo { get; set; } + + public short? NumberOfTransfersLessThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? DepartureTimeLessThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeGreaterThanOrEqualTo { get; set; } + + public DateTimeOffset? ArrivalTimeLessThanOrEqualTo { get; set; } +} diff --git a/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs index e10020e..5ed284a 100644 --- a/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs +++ b/src/HttpApi/Controllers/VehicleEnrollmentSearchController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Common.ViewModels; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch .Queries.SearchShortest; +using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch + .Queries.SearchAll; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -11,8 +14,8 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; [Route("vehicleEnrollmentSearch")] public class VehicleEnrollmentSearchController : ControllerBase { - [HttpGet] - [SwaggerOperation("Search vehicle enrollments with transfers")] + [HttpGet("shortest")] + [SwaggerOperation("Search shortest vehicle enrollments with transfers")] [SwaggerResponse( StatusCodes.Status200OK, "Search successful", typeof(VehicleEnrollmentSearchDto))] @@ -32,7 +35,7 @@ public class VehicleEnrollmentSearchController : ControllerBase [SwaggerResponse( StatusCodes.Status500InternalServerError, "Internal server error", typeof(ProblemDetails))] - public async Task> Add( + public async Task> SearchShortest( [FromQuery] SearchShortestViewModel viewModel, CancellationToken cancellationToken) { @@ -51,4 +54,66 @@ public class VehicleEnrollmentSearchController : ControllerBase }, cancellationToken)); } + + [HttpGet("all")] + [SwaggerOperation("Search all vehicle enrollments with transfers")] + [SwaggerResponse( + StatusCodes.Status200OK, "Search successful", + typeof(IEnumerable))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "No enrollments found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task>> + SearchAll( + [FromQuery] SearchAllViewModel viewModel, + [FromQuery] SortQuery sortQuery, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new SearchAllQuery() + { + DepartureAddressGuid = viewModel.DepartureAddressUuid, + ArrivalAddressGuid = viewModel.ArrivalAddressUuid, + DepartureDate = viewModel.DepartureDate, + VehicleTypes = viewModel.VehicleTypes + .Select(e => VehicleType.FromName(e)).ToHashSet(), + Sort = sortQuery.Sort, + TravelTimeGreaterThanOrEqualTo = + viewModel.TravelTimeGreaterThanOrEqualTo, + TravelTimeLessThanOrEqualTo = + viewModel.TravelTimeLessThanOrEqualTo, + CostGreaterThanOrEqualTo = + viewModel.CostGreaterThanOrEqualTo, + CostLessThanOrEqualTo = + viewModel.CostLessThanOrEqualTo, + NumberOfTransfersGreaterThanOrEqualTo = + viewModel.NumberOfTransfersGreaterThanOrEqualTo, + NumberOfTransfersLessThanOrEqualTo = + viewModel.NumberOfTransfersLessThanOrEqualTo, + DepartureTimeGreaterThanOrEqualTo = + viewModel.DepartureTimeGreaterThanOrEqualTo, + DepartureTimeLessThanOrEqualTo = + viewModel.DepartureTimeLessThanOrEqualTo, + ArrivalTimeGreaterThanOrEqualTo = + viewModel.ArrivalTimeGreaterThanOrEqualTo, + ArrivalTimeLessThanOrEqualTo = + viewModel.ArrivalTimeLessThanOrEqualTo + }, + cancellationToken)); + } }