add all vehicle enrollment with transfers search
All checks were successful
/ build (push) Successful in 9m40s
/ tests (push) Successful in 1m29s
/ build-docker (push) Successful in 10m30s

This commit is contained in:
cuqmbr 2025-05-24 11:17:43 +03:00
parent d5ffedbdb9
commit e3dd2dd582
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
8 changed files with 815 additions and 55 deletions

View File

@ -0,0 +1,39 @@
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application
.VehicleEnrollmentSearch.Queries.SearchAll;
public record SearchAllQuery :
IRequest<IEnumerable<VehicleEnrollmentSearchDto>>
{
public Guid DepartureAddressGuid { get; set; }
public Guid ArrivalAddressGuid { get; set; }
public DateOnly DepartureDate { get; set; }
public HashSet<VehicleType> 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; }
}

View File

@ -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<SearchAllQuery>
{
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
});
}
}

View File

@ -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<SearchAllQuery, IEnumerable<VehicleEnrollmentSearchDto>>
{
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<IEnumerable<VehicleEnrollmentSearchDto>> 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<Address, RouteAddressDetail>>();
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<Address, RouteAddressDetail>(
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<List<TaggedEdge<Address, RouteAddressDetail>>>();
var queue = new Queue<(TaggedEdge<Address, RouteAddressDetail> edge, List<TaggedEdge<Address, RouteAddressDetail>> path)>();
foreach (var edge in graph.OutEdges(departureAddress))
{
queue.Enqueue((edge, new List<TaggedEdge<Address, RouteAddressDetail>>() { 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<TaggedEdge<Address, RouteAddressDetail>>(current.path) { neighbor };
queue.Enqueue((neighbor, newPath));
}
}
}
// Create DTO object
var result = new List<VehicleEnrollmentSearchDto>();
foreach (var path in paths)
{
var vehicleEnrollmentDtos =
new List<VehicleEnrollmentSearchVehicleEnrollmentDto>();
var addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
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<VehicleEnrollmentSearchAddressDto>();
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<VehicleEnrollmentSearchDto>
.ApplySort(filteredResult.AsQueryable(), request.Sort);
return sortedResult;
}
}

View File

@ -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<SearchAllQuery>
{
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))));
}
}

View File

@ -6,11 +6,11 @@ using AutoMapper;
using QuikGraph; using QuikGraph;
using QuikGraph.Algorithms; using QuikGraph.Algorithms;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application namespace cuqmbr.TravelGuide.Application
.VehicleEnrollmentSearch.Queries.SearchShortest; .VehicleEnrollmentSearch.Queries.SearchShortest;
// TODO: Refactor.
// TODO: Add configurable time between transfers. // TODO: Add configurable time between transfers.
public class SearchShortestQueryHandler : public class SearchShortestQueryHandler :
IRequestHandler<SearchShortestQuery, VehicleEnrollmentSearchDto> IRequestHandler<SearchShortestQuery, VehicleEnrollmentSearchDto>
@ -21,16 +21,20 @@ public class SearchShortestQueryHandler :
private readonly SessionCurrencyService _sessionCurrencyService; private readonly SessionCurrencyService _sessionCurrencyService;
private readonly CurrencyConverterService _currencyConverterService; private readonly CurrencyConverterService _currencyConverterService;
private readonly SessionTimeZoneService _sessionTimeZoneService;
public SearchShortestQueryHandler( public SearchShortestQueryHandler(
UnitOfWork unitOfWork, UnitOfWork unitOfWork,
IMapper mapper, IMapper mapper,
SessionCurrencyService sessionCurrencyService, SessionCurrencyService sessionCurrencyService,
CurrencyConverterService currencyConverterService) CurrencyConverterService currencyConverterService,
SessionTimeZoneService sessionTimeZoneService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_mapper = mapper; _mapper = mapper;
_sessionCurrencyService = sessionCurrencyService; _sessionCurrencyService = sessionCurrencyService;
_currencyConverterService = currencyConverterService; _currencyConverterService = currencyConverterService;
_sessionTimeZoneService = sessionTimeZoneService;
} }
public async Task<VehicleEnrollmentSearchDto> Handle( public async Task<VehicleEnrollmentSearchDto> Handle(
@ -163,10 +167,14 @@ public class SearchShortestQueryHandler :
long lastRouteAddressId; long lastRouteAddressId;
Guid lastRouteAddressGuid; Guid lastRouteAddressGuid;
decimal vehicleEnrollmentCost;
Currency vehicleEnrollmentCurrency;
decimal costToNextAddress;
var addressDtos = new List<VehicleEnrollmentSearchAddressDto>(); var addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
var addressOrder = (short)1; var addressOrder = (short)1;
var enrollmentTravelTime = TimeSpan.Zero;
var enrollmentCost = (decimal)0;
var enrollmentOrder = (short)1; var enrollmentOrder = (short)1;
Address source; Address source;
@ -181,13 +189,10 @@ public class SearchShortestQueryHandler :
nextTag = path.Select(e => e.Tag).ElementAt(i+1); nextTag = path.Select(e => e.Tag).ElementAt(i+1);
totalTravelTime += costToNextAddress = await _currencyConverterService
tag.TimeToNextAddress + tag.CurrentAddressStopTime; .ConvertAsync(tag.CostToNextAddress,
enrollmentTravelTime += tag.VehicleEnrollment.Currency,
tag.TimeToNextAddress + tag.CurrentAddressStopTime; _sessionCurrencyService.Currency, cancellationToken);
totalCost += tag.CostToNextAddress;
enrollmentCost += tag.CostToNextAddress;
addressDtos.Add(new VehicleEnrollmentSearchAddressDto() addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
{ {
@ -202,7 +207,7 @@ public class SearchShortestQueryHandler :
CityUuid = source.City.Guid, CityUuid = source.City.Guid,
CityName = source.City.Name, CityName = source.City.Name,
TimeToNextAddress = tag.TimeToNextAddress, TimeToNextAddress = tag.TimeToNextAddress,
CostToNextAddress = tag.CostToNextAddress, CostToNextAddress = costToNextAddress,
CurrentAddressStopTime = tag.CurrentAddressStopTime, CurrentAddressStopTime = tag.CurrentAddressStopTime,
Order = addressOrder, Order = addressOrder,
RouteAddressUuid = tag.RouteAddress.Guid RouteAddressUuid = tag.RouteAddress.Guid
@ -255,13 +260,29 @@ public class SearchShortestQueryHandler :
.ElementAt(1) .ElementAt(1)
.Id; .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( vehicleEnrollmentDtos.Add(
new VehicleEnrollmentSearchVehicleEnrollmentDto() new VehicleEnrollmentSearchVehicleEnrollmentDto()
{ {
DepartureTime = tag.VehicleEnrollment DepartureTime = tag.VehicleEnrollment
.GetDepartureTime(firstRouteAddressId), .GetDepartureTime(firstRouteAddressId)
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
ArrivalTime = tag.VehicleEnrollment ArrivalTime = tag.VehicleEnrollment
.GetArrivalTime(lastRouteAddressId), .GetArrivalTime(lastRouteAddressId)
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
TravelTime = tag.VehicleEnrollment TravelTime = tag.VehicleEnrollment
.GetTravelTime(firstRouteAddressId, .GetTravelTime(firstRouteAddressId,
lastRouteAddressId), lastRouteAddressId),
@ -274,10 +295,8 @@ public class SearchShortestQueryHandler :
NumberOfStops = tag.VehicleEnrollment NumberOfStops = tag.VehicleEnrollment
.GetNumberOfStops(firstRouteAddressId, .GetNumberOfStops(firstRouteAddressId,
lastRouteAddressId), lastRouteAddressId),
Currency = tag.VehicleEnrollment.Currency.Name, Currency = vehicleEnrollmentCurrency.Name,
Cost = tag.VehicleEnrollment Cost = vehicleEnrollmentCost,
.GetCost(firstRouteAddressId,
lastRouteAddressId),
VehicleType = source.VehicleType.Name, VehicleType = source.VehicleType.Name,
Uuid = tag.VehicleEnrollment.Guid, Uuid = tag.VehicleEnrollment.Guid,
Order = enrollmentOrder, Order = enrollmentOrder,
@ -288,8 +307,6 @@ public class SearchShortestQueryHandler :
addressDtos = new List<VehicleEnrollmentSearchAddressDto>(); addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
addressOrder = (short)1; addressOrder = (short)1;
enrollmentTravelTime = TimeSpan.Zero;
enrollmentCost = (decimal)0;
enrollmentOrder++; enrollmentOrder++;
} }
} }
@ -299,6 +316,12 @@ public class SearchShortestQueryHandler :
tag = path.Select(e => e.Tag).Last(); tag = path.Select(e => e.Tag).Last();
nextTag = 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() addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
{ {
Uuid = source.Guid, Uuid = source.Guid,
@ -312,12 +335,14 @@ public class SearchShortestQueryHandler :
CityUuid = source.City.Guid, CityUuid = source.City.Guid,
CityName = source.City.Name, CityName = source.City.Name,
TimeToNextAddress = tag.TimeToNextAddress, TimeToNextAddress = tag.TimeToNextAddress,
CostToNextAddress = tag.CostToNextAddress, CostToNextAddress = costToNextAddress,
CurrentAddressStopTime = tag.CurrentAddressStopTime, CurrentAddressStopTime = tag.CurrentAddressStopTime,
Order = addressOrder, Order = addressOrder,
RouteAddressUuid = tag.RouteAddress.Guid RouteAddressUuid = tag.RouteAddress.Guid
}); });
addressOrder++;
lastRouteAddressGuid = vehicleEnrollments lastRouteAddressGuid = vehicleEnrollments
.Single(e => e.Id == tag.VehicleEnrollmentId) .Single(e => e.Id == tag.VehicleEnrollmentId)
.RouteAddressDetails .RouteAddressDetails
@ -328,7 +353,6 @@ public class SearchShortestQueryHandler :
.ElementAt(1) .ElementAt(1)
.Guid; .Guid;
addressOrder++;
addressDtos.Add(new VehicleEnrollmentSearchAddressDto() addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
{ {
Uuid = target.Guid, Uuid = target.Guid,
@ -358,13 +382,29 @@ public class SearchShortestQueryHandler :
.ElementAt(1) .ElementAt(1)
.Id; .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( vehicleEnrollmentDtos.Add(
new VehicleEnrollmentSearchVehicleEnrollmentDto() new VehicleEnrollmentSearchVehicleEnrollmentDto()
{ {
DepartureTime = tag.VehicleEnrollment DepartureTime = tag.VehicleEnrollment
.GetDepartureTime(firstRouteAddressId), .GetDepartureTime(firstRouteAddressId)
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
ArrivalTime = tag.VehicleEnrollment ArrivalTime = tag.VehicleEnrollment
.GetArrivalTime(lastRouteAddressId), .GetArrivalTime(lastRouteAddressId)
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
TravelTime = tag.VehicleEnrollment TravelTime = tag.VehicleEnrollment
.GetTravelTime(firstRouteAddressId, .GetTravelTime(firstRouteAddressId,
lastRouteAddressId), lastRouteAddressId),
@ -377,10 +417,8 @@ public class SearchShortestQueryHandler :
NumberOfStops = tag.VehicleEnrollment NumberOfStops = tag.VehicleEnrollment
.GetNumberOfStops(firstRouteAddressId, .GetNumberOfStops(firstRouteAddressId,
lastRouteAddressId), lastRouteAddressId),
Currency = tag.VehicleEnrollment.Currency.Name, Currency = vehicleEnrollmentCurrency.Name,
Cost = tag.VehicleEnrollment Cost = vehicleEnrollmentCost,
.GetCost(firstRouteAddressId,
lastRouteAddressId),
VehicleType = source.VehicleType.Name, VehicleType = source.VehicleType.Name,
Uuid = tag.VehicleEnrollment.Guid, Uuid = tag.VehicleEnrollment.Guid,
Order = enrollmentOrder, 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 var departureTime = vehicleEnrollmentDtos
.OrderBy(e => e.Order).First().DepartureTime; .OrderBy(e => e.Order).First().DepartureTime;
var arrivalTime = vehicleEnrollmentDtos var arrivalTime = vehicleEnrollmentDtos
@ -422,14 +438,20 @@ public class SearchShortestQueryHandler :
var timeInStops = vehicleEnrollmentDtos var timeInStops = vehicleEnrollmentDtos
.Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops); .Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops);
var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1; var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1;
var cost = vehicleEnrollmentDtos
.Aggregate((decimal)0, (sum, next) => sum += next.Cost);
return new VehicleEnrollmentSearchDto() return new VehicleEnrollmentSearchDto()
{ {
DepartureTime = departureTime, DepartureTime = departureTime
ArrivalTime = arrivalTime, .ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
ArrivalTime = arrivalTime
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
TravelTime = arrivalTime - departureTime, TravelTime = arrivalTime - departureTime,
TimeInStops = timeInStops, TimeInStops = timeInStops,
NumberOfTransfers = numberOfTransfers, NumberOfTransfers = numberOfTransfers,
Currency = _sessionCurrencyService.Currency.Name,
Cost = cost,
Enrollments = vehicleEnrollmentDtos Enrollments = vehicleEnrollmentDtos
}; };
} }

View File

@ -12,6 +12,10 @@ public sealed class VehicleEnrollmentSearchDto
public int NumberOfTransfers { get; set; } public int NumberOfTransfers { get; set; }
public string Currency { get; set; }
public decimal Cost { get; set; }
public ICollection<VehicleEnrollmentSearchVehicleEnrollmentDto> public ICollection<VehicleEnrollmentSearchVehicleEnrollmentDto>
Enrollments { get; set; } Enrollments { get; set; }
} }

View File

@ -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<string> 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; }
}

View File

@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Application.Common.ViewModels;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch
.Queries.SearchShortest; .Queries.SearchShortest;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch
.Queries.SearchAll;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels; using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers; namespace cuqmbr.TravelGuide.HttpApi.Controllers;
@ -11,8 +14,8 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("vehicleEnrollmentSearch")] [Route("vehicleEnrollmentSearch")]
public class VehicleEnrollmentSearchController : ControllerBase public class VehicleEnrollmentSearchController : ControllerBase
{ {
[HttpGet] [HttpGet("shortest")]
[SwaggerOperation("Search vehicle enrollments with transfers")] [SwaggerOperation("Search shortest vehicle enrollments with transfers")]
[SwaggerResponse( [SwaggerResponse(
StatusCodes.Status200OK, "Search successful", StatusCodes.Status200OK, "Search successful",
typeof(VehicleEnrollmentSearchDto))] typeof(VehicleEnrollmentSearchDto))]
@ -32,7 +35,7 @@ public class VehicleEnrollmentSearchController : ControllerBase
[SwaggerResponse( [SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error", StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))] typeof(ProblemDetails))]
public async Task<ActionResult<VehicleEnrollmentSearchDto>> Add( public async Task<ActionResult<VehicleEnrollmentSearchDto>> SearchShortest(
[FromQuery] SearchShortestViewModel viewModel, [FromQuery] SearchShortestViewModel viewModel,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -51,4 +54,66 @@ public class VehicleEnrollmentSearchController : ControllerBase
}, },
cancellationToken)); cancellationToken));
} }
[HttpGet("all")]
[SwaggerOperation("Search all vehicle enrollments with transfers")]
[SwaggerResponse(
StatusCodes.Status200OK, "Search successful",
typeof(IEnumerable<VehicleEnrollmentSearchDto>))]
[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<ActionResult<IEnumerable<VehicleEnrollmentSearchDto>>>
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));
}
} }