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> { 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; 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>(); 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 .FirstOrDefault( e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid) ?.RouteAddress.Address; var arrivalAddress = routeAddressDetails .FirstOrDefault( e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid) ?.RouteAddress.Address; if (departureAddress == null || arrivalAddress == null) { throw new NotFoundException(); } 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, Company = _mapper .Map( tag.VehicleEnrollment.Vehicle.Company), Vehicle = _mapper .Map( tag.VehicleEnrollment.Vehicle) }); 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(); 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( tag.VehicleEnrollment.Vehicle.Company), Vehicle = _mapper.Map( 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 .ApplySort(filteredResult.AsQueryable(), request.Sort); return sortedResult; } }