add shortest vehicle enrollment with transfers search
All checks were successful
/ build (push) Successful in 10m38s
/ tests (push) Successful in 1m14s
/ build-docker (push) Successful in 9m39s

This commit is contained in:
cuqmbr 2025-05-23 14:19:47 +03:00
parent 6830fea563
commit d5ffedbdb9
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
20 changed files with 930 additions and 11 deletions

View File

@ -17,6 +17,7 @@
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" /> <PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageReference Include="QuikGraph" Version="2.5.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" /> <PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
</ItemGroup> </ItemGroup>

View File

@ -10,15 +10,15 @@ public sealed class RouteAddressDto : IMapFrom<RouteAddress>
public short Order { get; set; } public short Order { get; set; }
public Guid AddressUuid { get; set; } public Guid Uuid { get; set; }
public string AddressName { get; set; } public string Name { get; set; }
public double AddressLongitude { get; set; } public double Longitude { get; set; }
public double AddressLatitude { get; set; } public double Latitude { get; set; }
public string AddressVehicleType { get; set; } public string VehicleType { get; set; }
public Guid CountryUuid { get; set; } public Guid CountryUuid { get; set; }
@ -39,19 +39,19 @@ public sealed class RouteAddressDto : IMapFrom<RouteAddress>
d => d.RouteAddressUuid, d => d.RouteAddressUuid,
opt => opt.MapFrom(s => s.Guid)) opt => opt.MapFrom(s => s.Guid))
.ForMember( .ForMember(
d => d.AddressUuid, d => d.Uuid,
opt => opt.MapFrom(s => s.Address.Guid)) opt => opt.MapFrom(s => s.Address.Guid))
.ForMember( .ForMember(
d => d.AddressName, d => d.Name,
opt => opt.MapFrom(s => s.Address.Name)) opt => opt.MapFrom(s => s.Address.Name))
.ForMember( .ForMember(
d => d.AddressLongitude, d => d.Longitude,
opt => opt.MapFrom(s => s.Address.Longitude)) opt => opt.MapFrom(s => s.Address.Longitude))
.ForMember( .ForMember(
d => d.AddressLatitude, d => d.Latitude,
opt => opt.MapFrom(s => s.Address.Latitude)) opt => opt.MapFrom(s => s.Address.Latitude))
.ForMember( .ForMember(
d => d.AddressVehicleType, d => d.VehicleType,
opt => opt.MapFrom(s => s.Address.VehicleType.Name)) opt => opt.MapFrom(s => s.Address.VehicleType.Name))
.ForMember( .ForMember(
d => d.CityUuid, d => d.CityUuid,

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application
.VehicleEnrollmentSearch.Queries.SearchShortest;
public record SearchShortestQuery :
IRequest<VehicleEnrollmentSearchDto>
{
public Guid DepartureAddressGuid { get; set; }
public Guid ArrivalAddressGuid { get; set; }
public DateOnly DepartureDate { get; set; }
public HashSet<VehicleType> VehicleTypes { get; set; }
public bool ShortestByCost { get; set; }
public bool ShortestByTime { 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.SearchShortest;
public class SearchShortestQueryAuthorizer :
AbstractRequestAuthorizer<SearchShortestQuery>
{
private readonly SessionUserService _sessionUserService;
public SearchShortestQueryAuthorizer(
SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(SearchShortestQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,436 @@
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 QuikGraph.Algorithms;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
namespace cuqmbr.TravelGuide.Application
.VehicleEnrollmentSearch.Queries.SearchShortest;
// TODO: Refactor.
// TODO: Add configurable time between transfers.
public class SearchShortestQueryHandler :
IRequestHandler<SearchShortestQuery, VehicleEnrollmentSearchDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly SessionCurrencyService _sessionCurrencyService;
private readonly CurrencyConverterService _currencyConverterService;
public SearchShortestQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper,
SessionCurrencyService sessionCurrencyService,
CurrencyConverterService currencyConverterService)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_sessionCurrencyService = sessionCurrencyService;
_currencyConverterService = currencyConverterService;
}
public async Task<VehicleEnrollmentSearchDto> Handle(
SearchShortestQuery 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;
Func<TaggedEdge<Address, RouteAddressDetail>, double> weightByCost =
edge => (double)edge.Tag.CostToNextAddress;
Func<TaggedEdge<Address, RouteAddressDetail>, double> weightByTime =
edge =>
edge.Tag.TimeToNextAddress.Ticks +
edge.Tag.CurrentAddressStopTime.Ticks;
Func<TaggedEdge<Address, RouteAddressDetail>, double> edgeWeight =
_ => 0;
if (request.ShortestByCost && request.ShortestByTime)
{
edgeWeight = edge => weightByCost(edge) + weightByTime(edge);
}
else if (request.ShortestByCost)
{
edgeWeight = edge => weightByCost(edge);
}
else if (request.ShortestByTime)
{
edgeWeight = edge => weightByTime(edge);
}
var tryGetPaths = graph.ShortestPathsDijkstra(edgeWeight, departureAddress);
// Create and hydrate a DTO object
var vehicleEnrollmentDtos =
new List<VehicleEnrollmentSearchVehicleEnrollmentDto>();
var totalTravelTime = TimeSpan.Zero;
var totalCost = (decimal)0;
if (tryGetPaths(arrivalAddress, out var path))
{
var firstRouteAddressId = path.First().Tag.RouteAddressId;
long lastRouteAddressId;
Guid lastRouteAddressGuid;
var addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
var addressOrder = (short)1;
var enrollmentTravelTime = TimeSpan.Zero;
var enrollmentCost = (decimal)0;
var enrollmentOrder = (short)1;
Address source;
Address target;
RouteAddressDetail tag;
RouteAddressDetail nextTag;
for (int i = 0; i < path.Count() - 1; i++)
{
source = path.Select(e => e.Source).ElementAt(i);
tag = path.Select(e => e.Tag).ElementAt(i);
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;
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 = tag.CostToNextAddress,
CurrentAddressStopTime = tag.CurrentAddressStopTime,
Order = addressOrder,
RouteAddressUuid = tag.RouteAddress.Guid
});
addressOrder++;
// First address after transfer
if (nextTag.VehicleEnrollmentId != tag.VehicleEnrollmentId)
{
target = path.Select(e => e.Target).ElementAt(i);
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;
vehicleEnrollmentDtos.Add(
new VehicleEnrollmentSearchVehicleEnrollmentDto()
{
DepartureTime = tag.VehicleEnrollment
.GetDepartureTime(firstRouteAddressId),
ArrivalTime = tag.VehicleEnrollment
.GetArrivalTime(lastRouteAddressId),
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 = tag.VehicleEnrollment.Currency.Name,
Cost = tag.VehicleEnrollment
.GetCost(firstRouteAddressId,
lastRouteAddressId),
VehicleType = source.VehicleType.Name,
Uuid = tag.VehicleEnrollment.Guid,
Order = enrollmentOrder,
Addresses = addressDtos
});
firstRouteAddressId = nextTag.RouteAddressId;
addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
addressOrder = (short)1;
enrollmentTravelTime = TimeSpan.Zero;
enrollmentCost = (decimal)0;
enrollmentOrder++;
}
}
source = path.Select(e => e.Source).Last();
target = path.Select(e => e.Target).Last();
tag = path.Select(e => e.Tag).Last();
nextTag = path.Select(e => e.Tag).Last();
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 = tag.CostToNextAddress,
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;
addressOrder++;
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;
vehicleEnrollmentDtos.Add(
new VehicleEnrollmentSearchVehicleEnrollmentDto()
{
DepartureTime = tag.VehicleEnrollment
.GetDepartureTime(firstRouteAddressId),
ArrivalTime = tag.VehicleEnrollment
.GetArrivalTime(lastRouteAddressId),
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 = tag.VehicleEnrollment.Currency.Name,
Cost = tag.VehicleEnrollment
.GetCost(firstRouteAddressId,
lastRouteAddressId),
VehicleType = source.VehicleType.Name,
Uuid = tag.VehicleEnrollment.Guid,
Order = enrollmentOrder,
Addresses = addressDtos
});
}
else
{
throw new NotFoundException();
}
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
.OrderBy(e => e.Order).Last().ArrivalTime;
var timeInStops = vehicleEnrollmentDtos
.Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops);
var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1;
return new VehicleEnrollmentSearchDto()
{
DepartureTime = departureTime,
ArrivalTime = arrivalTime,
TravelTime = arrivalTime - departureTime,
TimeInStops = timeInStops,
NumberOfTransfers = numberOfTransfers,
Enrollments = vehicleEnrollmentDtos
};
}
}

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.SearchShortest;
public class SearchShortestQueryValidator :
AbstractValidator<SearchShortestQuery>
{
public SearchShortestQueryValidator(
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

@ -0,0 +1,34 @@
namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch;
public sealed class VehicleEnrollmentSearchAddressDto
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public Guid CountryUuid { get; set; }
public string CountryName { get; set; }
public Guid RegionUuid { get; set; }
public string RegionName { get; set; }
public Guid CityUuid { get; set; }
public string CityName { get; set; }
public TimeSpan TimeToNextAddress { get; set; }
public decimal CostToNextAddress { get; set; }
public TimeSpan CurrentAddressStopTime { get; set; }
public short Order { get; set; }
public Guid RouteAddressUuid { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch;
public sealed class VehicleEnrollmentSearchDto
{
public DateTimeOffset DepartureTime { get; set; }
public DateTimeOffset ArrivalTime { get; set; }
public TimeSpan TravelTime { get; set; }
public TimeSpan TimeInStops { get; set; }
public int NumberOfTransfers { get; set; }
public ICollection<VehicleEnrollmentSearchVehicleEnrollmentDto>
Enrollments { get; set; }
}

View File

@ -0,0 +1,29 @@
namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch;
public sealed class VehicleEnrollmentSearchVehicleEnrollmentDto
{
public string VehicleType { get; set; }
public DateTimeOffset DepartureTime { get; set; }
public DateTimeOffset ArrivalTime { get; set; }
public TimeSpan TravelTime { get; set; }
public TimeSpan TimeMoving { get; set; }
public TimeSpan TimeInStops { get; set; }
public int NumberOfStops { get; set; }
public string Currency { get; set; }
public decimal Cost { get; set; }
public Guid Uuid { get; set; }
public short Order { get; set; }
public ICollection<VehicleEnrollmentSearchAddressDto>
Addresses { get; set; }
}

View File

@ -0,0 +1,16 @@
namespace cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels;
public sealed class SearchShortestViewModel
{
public Guid DepartureAddressUuid { get; set; }
public Guid ArrivalAddressUuid { get; set; }
public DateOnly DepartureDate { get; set; }
public HashSet<string> VehicleTypes { get; set; }
public bool ShortestByCost { get; set; }
public bool ShortestByTime { get; set; }
}

View File

@ -59,6 +59,12 @@
"Microsoft.Extensions.Options": "9.0.4" "Microsoft.Extensions.Options": "9.0.4"
} }
}, },
"QuikGraph": {
"type": "Direct",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"System.Linq.Dynamic.Core": { "System.Linq.Dynamic.Core": {
"type": "Direct", "type": "Direct",
"requested": "[1.6.2, )", "requested": "[1.6.2, )",

View File

@ -742,6 +742,11 @@
"Npgsql": "9.0.3" "Npgsql": "9.0.3"
} }
}, },
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"SQLitePCLRaw.bundle_e_sqlite3": { "SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.10", "resolved": "2.1.10",
@ -838,6 +843,7 @@
"MediatR": "[12.4.1, )", "MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )", "MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )", "Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"System.Linq.Dynamic.Core": "[1.6.2, )" "System.Linq.Dynamic.Core": "[1.6.2, )"
} }
}, },

View File

@ -23,4 +23,197 @@ public class VehicleEnrollment : EntityBase
public ICollection<Ticket> Tickets { get; set; } public ICollection<Ticket> Tickets { get; set; }
public DateTimeOffset GetDepartureTime(long DepartureRouteAddressId)
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
if (DepartureRouteAddressId == firstRouteAddressId)
{
return DepartureTime;
}
var orderedRouteAddressDetails = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order);
var timeToDeparture = TimeSpan.Zero;
foreach (var routeAddressDetail in orderedRouteAddressDetails)
{
timeToDeparture =
timeToDeparture + routeAddressDetail.CurrentAddressStopTime;
if (routeAddressDetail.Id == DepartureRouteAddressId)
{
break;
}
timeToDeparture =
timeToDeparture += routeAddressDetail.TimeToNextAddress;
}
return DepartureTime + timeToDeparture;
}
public DateTimeOffset GetDepartureTime()
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
return GetDepartureTime(firstRouteAddressId);
}
public DateTimeOffset GetArrivalTime(long ArrivalRouteAddressId)
{
var orderedRouteAddressDetails = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order);
var timeToDeparture = TimeSpan.Zero;
foreach (var routeAddressDetail in orderedRouteAddressDetails)
{
if (routeAddressDetail.Id == ArrivalRouteAddressId)
{
break;
}
timeToDeparture =
timeToDeparture +
routeAddressDetail.TimeToNextAddress +
routeAddressDetail.CurrentAddressStopTime;
}
return DepartureTime + timeToDeparture;
}
public DateTimeOffset GetArrivalTime()
{
var lastRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId;
return GetArrivalTime(lastRouteAddressId);
}
public TimeSpan GetTravelTime(
long DepartureRouteAddressId, long ArrivalRouteAddressId)
{
return
GetArrivalTime(ArrivalRouteAddressId) -
GetDepartureTime(DepartureRouteAddressId);
}
public TimeSpan GetTravelTime()
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
var lastRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId;
return GetTravelTime(firstRouteAddressId, lastRouteAddressId);
}
public TimeSpan GetTimeInStops(
long DepartureRouteAddressId, long ArrivalRouteAddressId)
{
var orderedRouteAddressDetails = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order);
var departureRouteAddressDetail = orderedRouteAddressDetails
.Single(e => e.Id == DepartureRouteAddressId);
var timeInStops = TimeSpan.Zero;
foreach (var routeAddressDetail in orderedRouteAddressDetails)
{
if (routeAddressDetail.RouteAddress.Order <=
departureRouteAddressDetail.RouteAddress.Order)
{
continue;
}
if (routeAddressDetail.Id == ArrivalRouteAddressId)
{
break;
}
timeInStops += routeAddressDetail.CurrentAddressStopTime;
}
return timeInStops;
}
public TimeSpan GetTimeInStops()
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
var lastRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId;
return GetTimeInStops(firstRouteAddressId, lastRouteAddressId);
}
public int GetNumberOfStops(
long DepartureRouteAddressId, long ArrivalRouteAddressId)
{
return
RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order)
.SkipWhile(e => e.Id != DepartureRouteAddressId)
.TakeWhile(e => e.Id != ArrivalRouteAddressId)
.Count() - 1;
}
public TimeSpan GetNumberOfStops()
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
var lastRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId;
return GetTimeInStops(firstRouteAddressId, lastRouteAddressId);
}
public TimeSpan GetTimeMoving(
long DepartureRouteAddressId, long ArrivalRouteAddressId)
{
return
RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order)
.SkipWhile(e => e.Id != DepartureRouteAddressId)
.TakeWhile(e => e.Id != ArrivalRouteAddressId)
.Aggregate(TimeSpan.Zero,
(sum, next) => sum += next.TimeToNextAddress);
}
public TimeSpan GetTimeMoving()
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
var lastRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId;
return GetTimeMoving(firstRouteAddressId, lastRouteAddressId);
}
public decimal GetCost(
long DepartureRouteAddressId, long ArrivalRouteAddressId)
{
return
RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order)
.SkipWhile(e => e.Id != DepartureRouteAddressId)
.TakeWhile(e => e.Id != ArrivalRouteAddressId)
.Aggregate((decimal)0,
(sum, next) => sum += next.CostToNextAddress);
}
public decimal GetCost()
{
var firstRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).First().RouteAddressId;
var lastRouteAddressId = RouteAddressDetails
.OrderBy(e => e.RouteAddress.Order).Last().RouteAddressId;
return GetCost(firstRouteAddressId, lastRouteAddressId);
}
} }

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch
.Queries.SearchShortest;
using cuqmbr.TravelGuide.Application.VehicleEnrollmentSearch.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("vehicleEnrollmentSearch")]
public class VehicleEnrollmentSearchController : ControllerBase
{
[HttpGet]
[SwaggerOperation("Search vehicle enrollments with transfers")]
[SwaggerResponse(
StatusCodes.Status200OK, "Search successful",
typeof(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<VehicleEnrollmentSearchDto>> Add(
[FromQuery] SearchShortestViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new SearchShortestQuery()
{
DepartureAddressGuid = viewModel.DepartureAddressUuid,
ArrivalAddressGuid = viewModel.ArrivalAddressUuid,
DepartureDate = viewModel.DepartureDate,
VehicleTypes = viewModel.VehicleTypes
.Select(e => VehicleType.FromName(e)).ToHashSet(),
ShortestByCost = viewModel.ShortestByCost,
ShortestByTime = viewModel.ShortestByTime
},
cancellationToken));
}
}

View File

@ -897,6 +897,11 @@
"Npgsql": "9.0.3" "Npgsql": "9.0.3"
} }
}, },
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"SQLitePCLRaw.bundle_e_sqlite3": { "SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.10", "resolved": "2.1.10",
@ -1079,6 +1084,7 @@
"MediatR": "[12.4.1, )", "MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )", "MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )", "Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"System.Linq.Dynamic.Core": "[1.6.2, )" "System.Linq.Dynamic.Core": "[1.6.2, )"
} }
}, },

View File

@ -528,6 +528,11 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.2" "Microsoft.Extensions.Logging.Abstractions": "8.0.2"
} }
}, },
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"System.Buffers": { "System.Buffers": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.6.0", "resolved": "4.6.0",
@ -589,6 +594,7 @@
"MediatR": "[12.4.1, )", "MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )", "MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )", "Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"System.Linq.Dynamic.Core": "[1.6.2, )" "System.Linq.Dynamic.Core": "[1.6.2, )"
} }
}, },

View File

@ -41,7 +41,7 @@ public sealed class ExchangeApiCurrencyConverterService :
public async Task<decimal> ConvertAsync(decimal amount, Currency from, public async Task<decimal> ConvertAsync(decimal amount, Currency from,
Currency to, DateTimeOffset time, CancellationToken cancellationToken) Currency to, DateTimeOffset time, CancellationToken cancellationToken)
{ {
if (from.Equals(to)) if (from.Equals(to) || to.Equals(Currency.Default))
{ {
return amount; return amount;
} }

View File

@ -234,6 +234,11 @@
"resolved": "9.0.4", "resolved": "9.0.4",
"contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
}, },
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"System.Linq.Dynamic.Core": { "System.Linq.Dynamic.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.6.2", "resolved": "1.6.2",
@ -249,6 +254,7 @@
"MediatR": "[12.4.1, )", "MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )", "MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )", "Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"System.Linq.Dynamic.Core": "[1.6.2, )" "System.Linq.Dynamic.Core": "[1.6.2, )"
} }
}, },

View File

@ -274,6 +274,11 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.2" "Microsoft.Extensions.Logging.Abstractions": "8.0.2"
} }
}, },
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"SQLitePCLRaw.bundle_e_sqlite3": { "SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.10", "resolved": "2.1.10",
@ -329,6 +334,7 @@
"MediatR": "[12.4.1, )", "MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )", "MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )", "Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"System.Linq.Dynamic.Core": "[1.6.2, )" "System.Linq.Dynamic.Core": "[1.6.2, )"
} }
}, },

View File

@ -816,6 +816,11 @@
"Npgsql": "9.0.3" "Npgsql": "9.0.3"
} }
}, },
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"SQLitePCLRaw.bundle_e_sqlite3": { "SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.10", "resolved": "2.1.10",
@ -982,6 +987,7 @@
"MediatR": "[12.4.1, )", "MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )", "MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )", "Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"System.Linq.Dynamic.Core": "[1.6.2, )" "System.Linq.Dynamic.Core": "[1.6.2, )"
} }
}, },