http-api/src/Application/VehicleEnrollmentSearch/Queries/SearchAll/SearchAllQueryHandler.cs

546 lines
22 KiB
C#

using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using QuikGraph;
using cuqmbr.TravelGuide.Application.Common.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.
// TODO: Refactor DTO creation code to use mapper as much as possible.
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;
var vehicles = (await _unitOfWork.VehicleRepository
.GetPageAsync(
e => e.Enrollments.All(e => vehicleEnrollmentIds.Contains(e.Id)),
1, int.MaxValue, cancellationToken))
.Items;
var companyIds = vehicles.Select(e => e.CompanyId);
var companies = (await _unitOfWork.CompanyRepository
.GetPageAsync(
e => companyIds.Contains(e.Id),
1, int.MaxValue, cancellationToken))
.Items;
// Hydrate vehicle enrollments with:
// - route address details;
// - vehicle info;
// - comapny info.
foreach (var vehicleEnrollment in vehicleEnrollments)
{
vehicleEnrollment.RouteAddressDetails = routeAddressDetails
.Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id)
.OrderBy(e => e.RouteAddress.Order)
.ToArray();
vehicleEnrollment.Vehicle = vehicles
.Single(e => e.Id == vehicleEnrollment.VehicleId);
vehicleEnrollment.Vehicle.Company = companies
.Single(e => e.Id == vehicleEnrollment.Vehicle.CompanyId);
}
// 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,
Company = _mapper
.Map<VehicleEnrollmentSearchCompanyDto>(
tag.VehicleEnrollment.Vehicle.Company),
Vehicle = _mapper
.Map<VehicleEnrollmentSearchVehicleDto>(
tag.VehicleEnrollment.Vehicle)
});
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();
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 = tag.RouteAddress.Guid
});
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,
Company = _mapper.Map<VehicleEnrollmentSearchCompanyDto>(
tag.VehicleEnrollment.Vehicle.Company),
Vehicle = _mapper.Map<VehicleEnrollmentSearchVehicleDto>(
tag.VehicleEnrollment.Vehicle)
});
// ---------------
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;
}
}