Compare commits

...

2 Commits

Author SHA1 Message Date
d5ffedbdb9
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
2025-05-23 14:19:47 +03:00
6830fea563
add ticket group creation 2025-05-20 20:39:09 +03:00
57 changed files with 3773 additions and 32 deletions

View File

@ -17,6 +17,7 @@
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" />
<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" />
</ItemGroup>

View File

@ -21,4 +21,35 @@ public static class CustomValidators
ruleBuilder
.Matches(@"^\+[0-9]{7,15}$");
}
public static IRuleBuilderOptions<T, ICollection<TSource>>
IsUnique<T, TSource, TResult>(
this IRuleBuilder<T, ICollection<TSource>> ruleBuilder,
Func<TSource, TResult> selector)
{
if (selector == null)
{
throw new ArgumentNullException(
nameof(selector),
"Cannot pass a null selector.");
}
return
ruleBuilder
.Must(x => x.IsDistinct(selector));
}
public static bool IsDistinct<TSource, TResult>(
this IEnumerable<TSource> elements, Func<TSource, TResult> selector)
{
var hashSet = new HashSet<TResult>();
foreach (var element in elements.Select(selector))
{
if (!hashSet.Contains(element))
hashSet.Add(element);
else
return false;
}
return true;
}
}

View File

@ -0,0 +1,7 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface RouteAddressDetailRepository :
BaseRepository<RouteAddressDetail> { }

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface TicketGroupRepository : BaseRepository<TicketGroup> { }

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface TicketRepository : BaseRepository<Ticket> { }

View File

@ -30,6 +30,12 @@ public interface UnitOfWork : IDisposable
EmployeeRepository EmployeeRepository { get; }
TicketGroupRepository TicketGroupRepository { get; }
TicketRepository TicketRepository { get; }
RouteAddressDetailRepository RouteAddressDetailRepository { get; }
int Save();
Task<int> SaveAsync(CancellationToken cancellationToken);

View File

@ -6,7 +6,8 @@
"LessThanOrEqualTo": "Must be less than or equal to {0}.",
"MustBeInEnum": "Must be one of the following: {0}.",
"IsEmail": "Must be a valid email address according to RFC 5321.",
"IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters."
"IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters.",
"IsUnique": "Elements of the collection must be unique."
},
"Validation": {
"DistinctOrder": "Must have distinct order values.",

View File

@ -10,15 +10,15 @@ public sealed class RouteAddressDto : IMapFrom<RouteAddress>
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; }
@ -39,19 +39,19 @@ public sealed class RouteAddressDto : IMapFrom<RouteAddress>
d => d.RouteAddressUuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.AddressUuid,
d => d.Uuid,
opt => opt.MapFrom(s => s.Address.Guid))
.ForMember(
d => d.AddressName,
d => d.Name,
opt => opt.MapFrom(s => s.Address.Name))
.ForMember(
d => d.AddressLongitude,
d => d.Longitude,
opt => opt.MapFrom(s => s.Address.Longitude))
.ForMember(
d => d.AddressLatitude,
d => d.Latitude,
opt => opt.MapFrom(s => s.Address.Latitude))
.ForMember(
d => d.AddressVehicleType,
d => d.VehicleType,
opt => opt.MapFrom(s => s.Address.VehicleType.Name))
.ForMember(
d => d.CityUuid,

View File

@ -0,0 +1,25 @@
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
using cuqmbr.TravelGuide.Application.TicketGroups.Models;
namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup;
public record AddTicketGroupCommand : IRequest<TicketGroupDto>
{
public string PassangerFirstName { get; set; }
public string PassangerLastName { get; set; }
public string PassangerPatronymic { get; set; }
public Sex PassangerSex { get; set; }
public DateOnly PassangerBirthDate { get; set; }
public DateTimeOffset PurchaseTime { get; set; }
public bool Returned { get; set; }
public ICollection<TicketModel> Tickets { get; set; }
}

View File

@ -0,0 +1,31 @@
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.TicketGroups.Commands.AddTicketGroup;
public class AddTicketGroupCommandAuthorizer :
AbstractRequestAuthorizer<AddTicketGroupCommand>
{
private readonly SessionUserService _sessionUserService;
public AddTicketGroupCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddTicketGroupCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,477 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using Microsoft.Extensions.Localization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation.Results;
namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup;
// TODO: Add descriptions and proper property names to validation errors
public class AddTicketGroupCommandHandler :
IRequestHandler<AddTicketGroupCommand, TicketGroupDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IStringLocalizer _localizer;
private readonly SessionCurrencyService _sessionCurrencyService;
private readonly CurrencyConverterService _currencyConverterService;
public AddTicketGroupCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper,
IStringLocalizer localizer,
SessionCurrencyService sessionCurrencyService,
CurrencyConverterService currencyConverterService)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_localizer = localizer;
_sessionCurrencyService = sessionCurrencyService;
_currencyConverterService = currencyConverterService;
}
public async Task<TicketGroupDto> Handle(
AddTicketGroupCommand request,
CancellationToken cancellationToken)
{
// Check whether provided vehicle enrollments are present in datastore.
{
var vehicleEnrollmentGuids =
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
.GetPageAsync(
e => vehicleEnrollmentGuids.Contains(e.Guid),
1, vehicleEnrollmentGuids.Count(), cancellationToken))
.Items;
if (vehicleEnrollmentGuids.Count() > vehicleEnrollments.Count)
{
throw new NotFoundException();
}
}
// Check whether provided arrival and departure address guids
// are used in provided vehicle enrollment and
// and are in the correct order.
{
var vehicleEnrollmentGuids =
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
.GetPageAsync(
e => vehicleEnrollmentGuids.Contains(e.Guid),
1, vehicleEnrollmentGuids.Count(), cancellationToken))
.Items;
var routeAddressGuids =
request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat(
request.Tickets.Select(t => t.ArrivalRouteAddressGuid));
var routeAddresses = (await _unitOfWork.RouteAddressRepository
.GetPageAsync(
e =>
routeAddressGuids.Contains(e.Guid),
1, routeAddressGuids.Count(), cancellationToken))
.Items;
foreach (var t in request.Tickets)
{
var departureRouteAddress = routeAddresses.First(
ra => ra.Guid == t.DepartureRouteAddressGuid);
var arrivalRouteAddress = routeAddresses.First(
ra => ra.Guid == t.ArrivalRouteAddressGuid);
var ve = vehicleEnrollments.First(
e => e.Guid == t.VehicleEnrollmentGuid);
if (departureRouteAddress.RouteId != ve.RouteId ||
arrivalRouteAddress.RouteId != ve.RouteId)
{
throw new ValidationException(
new List<ValidationFailure>
{
new()
{
PropertyName = nameof(request.Tickets)
}
});
}
if (departureRouteAddress.Order > arrivalRouteAddress.Order)
{
throw new ValidationException(
new List<ValidationFailure>
{
new()
{
PropertyName = nameof(request.Tickets)
}
});
}
}
}
// Check availability of free places.
{
// Get all tickets for vehicle enrollments requested in ticket group.
var vehicleEnrollmentGuids =
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
var ticketGroupTickets = (await _unitOfWork.TicketRepository
.GetPageAsync(
e =>
vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) &&
e.TicketGroup.Returned == false,
1, int.MaxValue, cancellationToken))
.Items;
// Get all vehicle enrollments requested in ticket group
// together with vehicles.
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
.GetPageAsync(
e => vehicleEnrollmentGuids.Contains(e.Guid),
e => e.Vehicle,
1, vehicleEnrollmentGuids.Count(), cancellationToken))
.Items;
// Get all route addresses of vehicle enrollments
// requested in ticket group.
var routeIds = vehicleEnrollments.Select(e => e.RouteId);
var routeAddresses = (await _unitOfWork.RouteAddressRepository
.GetPageAsync(
e => routeIds.Contains(e.RouteId),
1, int.MaxValue, cancellationToken))
.Items;
// For each ticket in request.
foreach (var requestTicket in request.Tickets)
{
// Get vehicle enrollment of requested ticket.
var requestVehicleEnrollment = vehicleEnrollments.First(e =>
e.Guid == requestTicket.VehicleEnrollmentGuid);
// Get bought tickets of vehicle enrollment of requested ticket.
var tickets = ticketGroupTickets.Where(t =>
t.VehicleEnrollmentId == requestVehicleEnrollment.Id);
// Get route addresses of vehicle enrollment.
var ticketRouteAddresses = routeAddresses
.Where(e => e.RouteId == requestVehicleEnrollment.RouteId)
.OrderBy(e => e.Order);
// Count available capacity.
// Get total capacity in requested vehicle.
int totalCapacity;
var vehicle = vehicleEnrollments.First(e =>
e.Guid == requestTicket.VehicleEnrollmentGuid)
.Vehicle;
if (vehicle.VehicleType.Equals(VehicleType.Bus))
{
totalCapacity = ((Bus)vehicle).Capacity;
}
else if (vehicle.VehicleType.Equals(VehicleType.Aircraft))
{
totalCapacity = ((Aircraft)vehicle).Capacity;
}
else if (vehicle.VehicleType.Equals(VehicleType.Train))
{
totalCapacity = ((Train)vehicle).Capacity;
}
else
{
throw new NotImplementedException();
}
int takenCapacity = 0;
// For each bought ticket.
foreach (var ticket in tickets)
{
// Get departure and arrival route address
// of requested ticket.
var requestDepartureRouteAddress = ticketRouteAddresses
.Single(e =>
e.Guid == requestTicket.DepartureRouteAddressGuid);
var requestArrivalRouteAddress = ticketRouteAddresses
.Single(e =>
e.Guid == requestTicket.ArrivalRouteAddressGuid);
// Get departure and arrival route address
// of bought ticket.
var departureRouteAddress = ticketRouteAddresses
.Single(e =>
e.Id == ticket.DepartureRouteAddressId);
var arrivalRouteAddress = ticketRouteAddresses
.Single(e =>
e.Id == ticket.ArrivalRouteAddressId);
// Count taken capacity in requested vehicle
// accounting for requested ticket
// departure and arrival route addresses.
// The algorithm is the same as vehicle enrollment
// time overlap check.
if ((requestDepartureRouteAddress.Order >=
departureRouteAddress.Order &&
requestDepartureRouteAddress.Order <
arrivalRouteAddress.Order) ||
(requestArrivalRouteAddress.Order <=
arrivalRouteAddress.Order &&
requestArrivalRouteAddress.Order >
departureRouteAddress.Order) ||
(requestDepartureRouteAddress.Order <=
departureRouteAddress.Order &&
requestArrivalRouteAddress.Order >=
arrivalRouteAddress.Order))
{
takenCapacity++;
}
}
var availableCapacity = totalCapacity - takenCapacity;
if (availableCapacity <= 0)
{
throw new ValidationException(
new List<ValidationFailure>
{
new()
{
PropertyName = nameof(request.Tickets)
}
});
}
}
}
// Calculate travel time and cost.
var ticketsDetails = new List<(short order, DateTimeOffset departureTime,
DateTimeOffset arrivalTime, decimal cost, Currency currency)>();
{
var vehicleEnrollmentGuids =
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
.GetPageAsync(
e => vehicleEnrollmentGuids.Contains(e.Guid),
1, vehicleEnrollmentGuids.Count(), cancellationToken))
.Items;
var routeAddressGuids =
request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat(
request.Tickets.Select(t => t.ArrivalRouteAddressGuid));
var routeAddresses = (await _unitOfWork.RouteAddressRepository
.GetPageAsync(
e =>
routeAddressGuids.Contains(e.Guid),
1, routeAddressGuids.Count(), cancellationToken))
.Items;
var vehicleEnrollmentIds = vehicleEnrollments.Select(ve => ve.Id);
var allRouteAddressDetails = (await _unitOfWork
.RouteAddressDetailRepository.GetPageAsync(
e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId),
e => e.RouteAddress,
1, int.MaxValue, cancellationToken))
.Items;
foreach (var t in request.Tickets.OrderBy(t => t.Order))
{
var ve = vehicleEnrollments.First(
e => e.Guid == t.VehicleEnrollmentGuid);
var departureRouteAddressId = routeAddresses.First(
ra => ra.Guid == t.DepartureRouteAddressGuid)
.Id;
var arrivalRouteAddressId = routeAddresses.First(
ra => ra.Guid == t.ArrivalRouteAddressGuid)
.Id;
var verad = allRouteAddressDetails
.Where(arad => arad.VehicleEnrollmentId == ve.Id)
.OrderBy(rad => rad.RouteAddress.Order)
.TakeWhile(rad => rad.Id != arrivalRouteAddressId);
// TODO: This counts departure address stop time which is
// not wrong but may be not desired.
var timeToDeparture = verad
.TakeWhile(rad => rad.Id != departureRouteAddressId)
.Aggregate(TimeSpan.Zero, (sum, next) =>
sum + next.TimeToNextAddress + next.CurrentAddressStopTime);
var departureTime = ve.DepartureTime.Add(timeToDeparture);
var timeToArrival = verad.Aggregate(TimeSpan.Zero, (sum, next) =>
sum + next.TimeToNextAddress + next.CurrentAddressStopTime);
var arrivalTime = ve.DepartureTime.Add(timeToArrival);
var costToDeparture = verad
.TakeWhile(rad => rad.Id != departureRouteAddressId)
.Aggregate((decimal)0, (sum, next) =>
sum + next.CostToNextAddress);
var costToArrival = verad
.Aggregate((decimal)0, (sum, next) =>
sum + next.CostToNextAddress);
var cost = costToArrival - costToDeparture;
ticketsDetails.Add(
(t.Order, departureTime, arrivalTime, cost, ve.Currency));
}
}
// Check whether there are overlaps in ticket departure/arrival times.
{
for (int i = 1; i < ticketsDetails.Count; i++)
{
var previousTd = ticketsDetails[i - 1];
var currentTd = ticketsDetails[i];
if (previousTd.arrivalTime >= currentTd.departureTime)
{
throw new ValidationException(
new List<ValidationFailure>
{
new()
{
PropertyName = nameof(request.Tickets)
}
});
}
}
}
// Create entity and insert into a datastore.
var ticketsCostDetails = new List<(short order,
decimal cost, Currency currency)>();
TimeSpan travelTime;
{
travelTime =
ticketsDetails.OrderBy(td => td.order).Last().arrivalTime -
ticketsDetails.OrderBy(td => td.order).First().departureTime;
foreach (var td in ticketsDetails)
{
var initialCurrency = td.currency;
var convertedCurrency =
_sessionCurrencyService.Currency != Currency.Default ?
_sessionCurrencyService.Currency :
initialCurrency;
var cost = td.cost;
var convertedCost = await _currencyConverterService
.ConvertAsync(cost, initialCurrency,
convertedCurrency, cancellationToken);
ticketsCostDetails.Add((td.order, convertedCost, convertedCurrency));
}
}
{
var vehicleEnrollmentGuids =
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
.GetPageAsync(
e => vehicleEnrollmentGuids.Contains(e.Guid),
1, vehicleEnrollmentGuids.Count(), cancellationToken))
.Items;
var routeAddressGuids =
request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat(
request.Tickets.Select(t => t.ArrivalRouteAddressGuid));
var routeAddresses = (await _unitOfWork.RouteAddressRepository
.GetPageAsync(
e => routeAddressGuids.Contains(e.Guid),
e => e.Address.City.Region.Country,
1, routeAddressGuids.Count(), cancellationToken))
.Items;
var entity = new TicketGroup()
{
PassangerFirstName = request.PassangerFirstName,
PassangerLastName = request.PassangerLastName,
PassangerPatronymic = request.PassangerPatronymic,
PassangerSex = request.PassangerSex,
PassangerBirthDate = request.PassangerBirthDate,
PurchaseTime = request.PurchaseTime,
Returned = request.Returned,
TravelTime = travelTime,
Tickets = request.Tickets.Select(
t =>
{
var ve = vehicleEnrollments.First(
ve => ve.Guid == t.VehicleEnrollmentGuid);
var departureRouteAddress = routeAddresses.First(
ra => ra.Guid == t.DepartureRouteAddressGuid);
var arrivalRouteAddress = routeAddresses.First(
ra => ra.Guid == t.ArrivalRouteAddressGuid);
var costDetail = ticketsCostDetails
.SingleOrDefault(td => td.order == t.Order);
return new Ticket()
{
DepartureRouteAddressId = departureRouteAddress.Id,
DepartureRouteAddress = departureRouteAddress,
ArrivalRouteAddressId = arrivalRouteAddress.Id,
ArrivalRouteAddress = arrivalRouteAddress,
Order = t.Order,
Cost = costDetail.cost,
Currency = costDetail.currency,
VehicleEnrollmentId = ve.Id
};
})
.ToArray()
};
entity = await _unitOfWork.TicketGroupRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<TicketGroupDto>(entity);
}
}
}

View File

@ -0,0 +1,107 @@
using cuqmbr.TravelGuide.Application.Common.FluentValidation;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup;
public class AddTicketGroupCommandValidator : AbstractValidator<AddTicketGroupCommand>
{
public AddTicketGroupCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(tg => tg.PassangerFirstName)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(tg => tg.PassangerLastName)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(tg => tg.PassangerPatronymic)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(tg => tg.PassangerSex)
.Must((tg, s) => Sex.Enumerations.ContainsValue(s))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
Sex.Enumerations.Values.Select(e => e.Name))));
RuleFor(tg => tg.PassangerBirthDate)
.GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))));
RuleFor(tg => tg.PurchaseTime)
.GreaterThanOrEqualTo(DateTimeOffset.UtcNow)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
DateTimeOffset.UtcNow));
RuleFor(tg => tg.Tickets)
.IsUnique(t => t.VehicleEnrollmentGuid)
.WithMessage(localizer["FluentValidation.IsUnique"]);
RuleFor(tg => tg.Tickets)
.IsUnique(t => t.Order)
.WithMessage(localizer["FluentValidation.IsUnique"]);
RuleForEach(tg => tg.Tickets).ChildRules(t =>
{
t.RuleFor(t => t.DepartureRouteAddressGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
t.RuleFor(t => t.ArrivalRouteAddressGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
t.RuleFor(t => t.Order)
.GreaterThanOrEqualTo(short.MinValue)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
short.MinValue))
.LessThanOrEqualTo(short.MaxValue)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
short.MaxValue));
t.RuleFor(t => t.VehicleEnrollmentGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
});
}
}

View File

@ -0,0 +1,13 @@
namespace cuqmbr.TravelGuide.Application.TicketGroups.Models;
public sealed class TicketModel
{
public Guid DepartureRouteAddressGuid { get; set; }
public Guid ArrivalRouteAddressGuid { get; set; }
public short Order { get; set; }
public Guid VehicleEnrollmentGuid { get; set; }
}

View File

@ -0,0 +1,53 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.TicketGroups;
public sealed class TicketAddressDto : IMapFrom<Address>
{
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 void Mapping(MappingProfile profile)
{
profile.CreateMap<Address, TicketAddressDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.CountryUuid,
opt => opt.MapFrom(s => s.City.Region.Country.Guid))
.ForMember(
d => d.CountryName,
opt => opt.MapFrom(s => s.City.Region.Country.Name))
.ForMember(
d => d.RegionUuid,
opt => opt.MapFrom(s => s.City.Region.Guid))
.ForMember(
d => d.RegionName,
opt => opt.MapFrom(s => s.City.Region.Name))
.ForMember(
d => d.CityUuid,
opt => opt.MapFrom(s => s.City.Guid))
.ForMember(
d => d.CityName,
opt => opt.MapFrom(s => s.City.Name));
}
}

View File

@ -0,0 +1,50 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.TicketGroups;
public sealed class TicketDto : IMapFrom<Ticket>
{
public Guid Uuid { get; set; }
public Guid DepartureRouteAddressUuid { get; set; }
public Guid ArrivalRouteAddressUuid { get; set; }
public TicketAddressDto DepartureAddress { get; set; }
public TicketAddressDto ArrivalAddress { get; set; }
public short Order { get; set; }
public Guid VehicleEnrollmentUuid { get; set; }
// TODO: Add VehicleEnrollment model
public string Currency { get; set; }
public decimal Cost { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Ticket, TicketDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.DepartureRouteAddressUuid,
opt => opt.MapFrom(s => s.DepartureRouteAddress.Guid))
.ForMember(
d => d.ArrivalRouteAddressUuid,
opt => opt.MapFrom(s => s.ArrivalRouteAddress.Guid))
.ForMember(
d => d.DepartureAddress,
opt => opt.MapFrom(s => s.DepartureRouteAddress.Address))
.ForMember(
d => d.ArrivalAddress,
opt => opt.MapFrom(s => s.ArrivalRouteAddress.Address))
.ForMember(
d => d.VehicleEnrollmentUuid,
opt => opt.MapFrom(s => s.VehicleEnrollment.Guid));
}
}

View File

@ -0,0 +1,45 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.TicketGroups;
public sealed class TicketGroupDto : IMapFrom<TicketGroup>
{
public Guid Uuid { get; set; }
public string PassangerFirstName { get; set; }
public string PassangerLastName { get; set; }
public string PassangerPatronymic { get; set; }
public string PassangerSex { get; set; }
public DateOnly PassangerBirthDate { get; set; }
public DateTimeOffset PurchaseTime { get; set; }
public bool Returned { get; set; }
public TimeSpan TravelTime { get; set; }
public ICollection<TicketDto> Tickets { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<TicketGroup, TicketGroupDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.PassangerSex,
opt => opt.MapFrom(s => s.PassangerSex.Name))
.ForMember(
d => d.PurchaseTime,
opt => opt
.MapFrom<DateTimeOffsetToLocalResolver, DateTimeOffset>(
s => s.PurchaseTime));
}
}

View File

@ -0,0 +1,21 @@
namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels;
public sealed class AddTicketGroupViewModel
{
public string PassangerFirstName { get; set; }
public string PassangerLastName { get; set; }
public string PassangerPatronymic { get; set; }
public string PassangerSex { get; set; }
public DateOnly PassangerBirthDate { get; set; }
public DateTimeOffset PurchaseTime { get; set; }
public bool Returned { get; set; }
public ICollection<TicketViewModel> Tickets { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels;
public sealed class GetTicketGroupsPageFilterViewModel
{
public string? Sex { get; set; }
public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; }
public DateOnly? BirthDateLessThanOrEqualTo { get; set; }
public Guid? CompanyUuid { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels;
public sealed class TicketViewModel
{
public Guid DepartureRouteAddressUuid { get; set; }
public Guid ArrivalRouteAddressUuid { get; set; }
public short Order { get; set; }
public Guid VehicleEnrollmentUuid { get; set; }
}

View File

@ -0,0 +1,21 @@
namespace cuqmbr.TravelGuide.Application.TicketGroups.ViewModels;
public sealed class UpdateTicketGroupViewModel
{
public string PassangerFirstName { get; set; }
public string PassangerLastName { get; set; }
public string PassangerPatronymic { get; set; }
public string PassangerSex { get; set; }
public DateOnly PassangerBirthDate { get; set; }
public DateTimeOffset PurchaseTime { get; set; }
public bool Returned { get; set; }
public ICollection<TicketViewModel> Tickets { get; set; }
}

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

@ -105,13 +105,13 @@ public class AddVehicleEnrollmentCommandHandler :
// Three cases are included:
//
// ---RD---------SD----------RA--->
// ---SD---------RD----------SA--->
// time
//
// ---RD---------SA----------RA--->
// ---SD---------RA----------SA--->
// time
//
// ---SD-----RD-------RA-----SA--->
// ---RD-----SD-------SA-----RA--->
// time
// Where:
// RD - request enrollment departure time
@ -134,12 +134,12 @@ public class AddVehicleEnrollmentCommandHandler :
rad.CurrentAddressStopTime);
return
(departureTime >= requestDepartureTime &&
departureTime <= requestArrivalTime) ||
(arrivalTime >= requestDepartureTime &&
arrivalTime <= requestArrivalTime) ||
(departureTime <= requestDepartureTime &&
arrivalTime >= requestArrivalTime);
(requestDepartureTime >= departureTime &&
requestDepartureTime <= arrivalTime) ||
(requestArrivalTime >= departureTime &&
requestArrivalTime <= arrivalTime) ||
(requestDepartureTime <= departureTime &&
requestArrivalTime >= arrivalTime);
})
.Any();

View File

@ -83,13 +83,13 @@ public class UpdateVehicleEnrollmentCommandHandler :
// Three cases are included:
//
// ---RD---------SD----------RA--->
// ---SD---------RD----------SA--->
// time
//
// ---RD---------SA----------RA--->
// ---SD---------RA----------SA--->
// time
//
// ---SD-----RD-------RA-----SA--->
// ---RD-----SD-------SA-----RA--->
// time
// Where:
// RD - request enrollment departure time
@ -112,12 +112,12 @@ public class UpdateVehicleEnrollmentCommandHandler :
rad.CurrentAddressStopTime);
return
(departureTime >= requestDepartureTime &&
departureTime <= requestArrivalTime) ||
(arrivalTime >= requestDepartureTime &&
arrivalTime <= requestArrivalTime) ||
(departureTime <= requestDepartureTime &&
arrivalTime >= requestArrivalTime);
(requestDepartureTime >= departureTime &&
requestDepartureTime <= arrivalTime) ||
(requestArrivalTime >= departureTime &&
requestArrivalTime <= arrivalTime) ||
(requestDepartureTime <= departureTime &&
requestArrivalTime >= arrivalTime);
})
.Any();

View File

@ -59,6 +59,12 @@
"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": {
"type": "Direct",
"requested": "[1.6.2, )",

View File

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

View File

@ -0,0 +1,30 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Domain.Entities;
public sealed class Ticket : EntityBase
{
public long DepartureRouteAddressId { get; set; }
public RouteAddress DepartureRouteAddress { get; set; }
public long ArrivalRouteAddressId { get; set; }
public RouteAddress ArrivalRouteAddress { get; set; }
public short Order { get; set; }
public Currency Currency { get; set; }
public decimal Cost { get; set; }
public long TicketGroupId { get; set; }
public TicketGroup TicketGroup { get; set; }
public long VehicleEnrollmentId { get; set; }
public VehicleEnrollment VehicleEnrollment { get; set; }
}

View File

@ -0,0 +1,25 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Domain.Entities;
public sealed class TicketGroup : EntityBase
{
public string PassangerFirstName { get; set; }
public string PassangerLastName { get; set; }
public string PassangerPatronymic { get; set; }
public Sex PassangerSex { get; set; }
public DateOnly PassangerBirthDate { get; set; }
public DateTimeOffset PurchaseTime { get; set; }
public bool Returned { get; set; }
public TimeSpan TravelTime { get; set; }
public ICollection<Ticket> Tickets { get; set; }
}

View File

@ -23,4 +23,197 @@ public class VehicleEnrollment : EntityBase
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,221 @@
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.ViewModels;
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Application.TicketGroups;
using cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup;
// using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroupsPage;
// using cuqmbr.TravelGuide.Application.TicketGroups.Queries.GetTicketGroup;
// using cuqmbr.TravelGuide.Application.TicketGroups.Commands.UpdateTicketGroup;
// using cuqmbr.TravelGuide.Application.TicketGroups.Commands.DeleteTicketGroup;
using cuqmbr.TravelGuide.Application.TicketGroups.ViewModels;
using cuqmbr.TravelGuide.Application.TicketGroups.Models;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("ticketGroups")]
public class TicketGroupsController : ControllerBase
{
[HttpPost]
[SwaggerOperation("Add a ticketGroup")]
[SwaggerResponse(
StatusCodes.Status201Created, "Object successfuly created",
typeof(TicketGroupDto))]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Object already exists",
typeof(ProblemDetails))]
[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, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<ActionResult<TicketGroupDto>> Add(
[FromBody] AddTicketGroupViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new AddTicketGroupCommand()
{
PassangerFirstName = viewModel.PassangerFirstName,
PassangerLastName = viewModel.PassangerLastName,
PassangerPatronymic = viewModel.PassangerPatronymic,
PassangerSex = Sex.FromName(viewModel.PassangerSex),
PassangerBirthDate = viewModel.PassangerBirthDate,
PurchaseTime = viewModel.PurchaseTime,
Returned = viewModel.Returned,
Tickets = viewModel.Tickets.Select(e =>
new TicketModel()
{
DepartureRouteAddressGuid =
e.DepartureRouteAddressUuid,
ArrivalRouteAddressGuid =
e.ArrivalRouteAddressUuid,
Order = e.Order,
VehicleEnrollmentGuid =
e.VehicleEnrollmentUuid,
})
.ToArray()
},
cancellationToken));
}
// [HttpGet]
// [SwaggerOperation("Get a list of all ticketGroups")]
// [SwaggerResponse(
// StatusCodes.Status200OK, "Request successful",
// typeof(PaginatedList<TicketGroupDto>))]
// [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.Status500InternalServerError, "Internal server error",
// typeof(ProblemDetails))]
// public async Task<PaginatedList<TicketGroupDto>> GetPage(
// [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
// [FromQuery] SortQuery sortQuery,
// [FromQuery] GetTicketGroupsPageFilterViewModel filterQuery,
// CancellationToken cancellationToken)
// {
// return await Mediator.Send(
// new GetTicketGroupsPageQuery()
// {
// PageNumber = pageQuery.PageNumber,
// PageSize = pageQuery.PageSize,
// Search = searchQuery.Search,
// Sort = sortQuery.Sort,
// LongitudeGreaterOrEqualThan =
// filterQuery.LongitudeGreaterOrEqualThan,
// LongitudeLessOrEqualThan =
// filterQuery.LongitudeLessOrEqualThan,
// LatitudeGreaterOrEqualThan =
// filterQuery.LatitudeGreaterOrEqualThan,
// LatitudeLessOrEqualThan =
// filterQuery.LatitudeLessOrEqualThan,
// VehicleType = VehicleType.FromName(filterQuery.VehicleType),
// CountryGuid = filterQuery.CountryUuid,
// RegionGuid = filterQuery.RegionUuid,
// CityGuid = filterQuery.CityUuid
// },
// cancellationToken);
// }
//
// [HttpGet("{uuid:guid}")]
// [SwaggerOperation("Get a ticketGroup by uuid")]
// [SwaggerResponse(
// StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))]
// [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, "Object not found", typeof(TicketGroupDto))]
// [SwaggerResponse(
// StatusCodes.Status500InternalServerError, "Internal server error",
// typeof(ProblemDetails))]
// public async Task<TicketGroupDto> Get(
// [FromRoute] Guid uuid,
// CancellationToken cancellationToken)
// {
// return await Mediator.Send(new GetTicketGroupQuery() { Guid = uuid },
// cancellationToken);
// }
//
// [HttpPut("{uuid:guid}")]
// [SwaggerOperation("Update a ticketGroup")]
// [SwaggerResponse(
// StatusCodes.Status200OK, "Request successful", typeof(TicketGroupDto))]
// [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Object already exists",
// typeof(ProblemDetails))]
// [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, "Object not found", typeof(TicketGroupDto))]
// [SwaggerResponse(
// StatusCodes.Status404NotFound, "Parent object not found",
// typeof(ProblemDetails))]
// [SwaggerResponse(
// StatusCodes.Status500InternalServerError, "Internal server error",
// typeof(ProblemDetails))]
// public async Task<TicketGroupDto> Update(
// [FromRoute] Guid uuid,
// [FromBody] UpdateTicketGroupViewModel viewModel,
// CancellationToken cancellationToken)
// {
// return await Mediator.Send(
// new UpdateTicketGroupCommand()
// {
// Guid = uuid,
// Name = viewModel.Name,
// Longitude = viewModel.Longitude,
// Latitude = viewModel.Latitude,
// VehicleType = VehicleType.FromName(viewModel.VehicleType),
// CityGuid = viewModel.CityUuid
// },
// cancellationToken);
// }
//
// [HttpDelete("{uuid:guid}")]
// [SwaggerOperation("Delete a ticketGroup")]
// [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")]
// [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, "Object not found",
// typeof(ProblemDetails))]
// [SwaggerResponse(
// StatusCodes.Status500InternalServerError, "Internal server error",
// typeof(ProblemDetails))]
// public async Task<IActionResult> Delete(
// [FromRoute] Guid uuid,
// CancellationToken cancellationToken)
// {
// await Mediator.Send(
// new DeleteTicketGroupCommand() { Guid = uuid },
// cancellationToken);
// return StatusCode(StatusCodes.Status204NoContent);
// }
}

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

View File

@ -528,6 +528,11 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
}
},
"QuikGraph": {
"type": "Transitive",
"resolved": "2.5.0",
"contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.6.0",
@ -589,6 +594,7 @@
"MediatR": "[12.4.1, )",
"MediatR.Behaviors.Authorization": "[12.2.0, )",
"Microsoft.Extensions.Logging": "[9.0.4, )",
"QuikGraph": "[2.5.0, )",
"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,
Currency to, DateTimeOffset time, CancellationToken cancellationToken)
{
if (from.Equals(to))
if (from.Equals(to) || to.Equals(Currency.Default))
{
return amount;
}

View File

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

View File

@ -1,4 +1,5 @@
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using cuqmbr.TravelGuide.Persistence.TypeConverters;
@ -12,6 +13,15 @@ public class InMemoryDbContext : DbContext
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder
.Entity<Vehicle>()
.ToTable("vehicles")
.UseTphMappingStrategy()
.HasDiscriminator(v => v.VehicleType)
.HasValue<Bus>(VehicleType.Bus)
.HasValue<Aircraft>(VehicleType.Aircraft)
.HasValue<Train>(VehicleType.Train);
}
protected override void ConfigureConventions(
@ -19,12 +29,26 @@ public class InMemoryDbContext : DbContext
{
builder
.Properties<VehicleType>()
.HaveColumnType("vehicle_type")
.HaveColumnType("varchar(16)")
.HaveConversion<VehicleTypeConverter>();
builder
.Properties<Currency>()
.HaveColumnType("currency")
.HaveColumnType("varchar(8)")
.HaveConversion<CurrencyConverter>();
builder
.Properties<DocumentType>()
.HaveColumnType("varchar(64)")
.HaveConversion<DocumentTypeConverter>();
builder
.Properties<Sex>()
.HaveColumnType("varchar(32)")
.HaveConversion<SexConverter>();
builder
.Properties<DateTimeOffset>()
.HaveConversion<DateTimeOffsetConverter>();
}
}

View File

@ -28,6 +28,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
new InMemoryRouteAddressRepository(_dbContext);
CompanyRepository = new InMemoryCompanyRepository(_dbContext);
EmployeeRepository = new InMemoryEmployeeRepository(_dbContext);
TicketGroupRepository = new InMemoryTicketGroupRepository(_dbContext);
TicketRepository = new InMemoryTicketRepository(_dbContext);
RouteAddressDetailRepository =
new InMemoryRouteAddressDetailRepository(_dbContext);
}
public CountryRepository CountryRepository { get; init; }
@ -56,6 +60,12 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
public EmployeeRepository EmployeeRepository { get; init; }
public TicketGroupRepository TicketGroupRepository { get; init; }
public TicketRepository TicketRepository { get; init; }
public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; }
public int Save()
{
return _dbContext.SaveChanges();

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories;
public sealed class InMemoryRouteAddressDetailRepository :
InMemoryBaseRepository<RouteAddressDetail>, RouteAddressDetailRepository
{
public InMemoryRouteAddressDetailRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories;
public sealed class InMemoryTicketGroupRepository :
InMemoryBaseRepository<TicketGroup>, TicketGroupRepository
{
public InMemoryTicketGroupRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories;
public sealed class InMemoryTicketRepository :
InMemoryBaseRepository<Ticket>, TicketRepository
{
public InMemoryTicketRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,105 @@
using cuqmbr.TravelGuide.Domain.Entities;
using cuqmbr.TravelGuide.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class TicketConfiguration : BaseConfiguration<Ticket>
{
public override void Configure(EntityTypeBuilder<Ticket> builder)
{
builder
.Property(t => t.Currency)
.HasColumnName("currency")
.IsRequired(true);
builder
.ToTable(
"tickets",
ve => ve.HasCheckConstraint(
"ck_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(ve => ve.Currency)
.Metadata.GetColumnName()}",
$"{builder.Property(ve => ve.Currency)
.Metadata.GetColumnName()} IN ('{String
.Join("', '", Currency.Enumerations
.Values.Select(v => v.Name))}')"));
base.Configure(builder);
builder
.Property(t => t.DepartureRouteAddressId)
.HasColumnName("departure_route_address_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.Property(t => t.ArrivalRouteAddressId)
.HasColumnName("arrival_route_address_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.Property(t => t.Order)
.HasColumnName("order")
.HasColumnType("smallint")
.IsRequired(true);
builder
.Property(t => t.Cost)
.HasColumnName("cost")
.HasColumnType("numeric(24,12)")
.IsRequired(true);
builder
.Property(t => t.TicketGroupId)
.HasColumnName("ticket_group_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.HasOne(t => t.TicketGroup)
.WithMany(tg => tg.Tickets)
.HasForeignKey(t => t.TicketGroupId)
.HasConstraintName(
"fk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(t => t.TicketGroupId).Metadata.GetColumnName()}")
.OnDelete(DeleteBehavior.Cascade);
builder
.HasIndex(t => t.TicketGroupId)
.HasDatabaseName(
"ix_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(t => t.TicketGroupId).Metadata.GetColumnName()}");
builder
.Property(t => t.VehicleEnrollmentId)
.HasColumnName("vehicle_enrollment_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.HasOne(t => t.VehicleEnrollment)
.WithMany(ve => ve.Tickets)
.HasForeignKey(t => t.VehicleEnrollmentId)
.HasConstraintName(
"fk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(t => t.VehicleEnrollmentId).Metadata.GetColumnName()}")
.OnDelete(DeleteBehavior.Cascade);
builder
.HasIndex(t => t.VehicleEnrollmentId)
.HasDatabaseName(
"ix_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(t => t.VehicleEnrollmentId).Metadata.GetColumnName()}");
}
}

View File

@ -0,0 +1,75 @@
using cuqmbr.TravelGuide.Domain.Entities;
using cuqmbr.TravelGuide.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class TicketGroupConfiguration : BaseConfiguration<TicketGroup>
{
public override void Configure(EntityTypeBuilder<TicketGroup> builder)
{
builder
.Property(tg => tg.PassangerSex)
.HasColumnName("passanger_sex")
.IsRequired(true);
builder
.ToTable(
"ticket_groups",
tg => tg.HasCheckConstraint(
"ck_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(tg => tg.PassangerSex)
.Metadata.GetColumnName()}",
$"{builder.Property(g => g.PassangerSex)
.Metadata.GetColumnName()} IN ('{String
.Join("', '", Sex.Enumerations
.Values.Select(v => v.Name))}')"));
base.Configure(builder);
builder
.Property(a => a.PassangerFirstName)
.HasColumnName("passanger_first_name")
.HasColumnType("varchar(32)")
.IsRequired(true);
builder
.Property(a => a.PassangerLastName)
.HasColumnName("passanger_last_name")
.HasColumnType("varchar(32)")
.IsRequired(true);
builder
.Property(a => a.PassangerPatronymic)
.HasColumnName("passanger_patronymic")
.HasColumnType("varchar(32)")
.IsRequired(true);
builder
.Property(a => a.PassangerBirthDate)
.HasColumnName("passanger_birth_date")
.HasColumnType("date")
.IsRequired(true);
builder
.Property(a => a.PurchaseTime)
.HasColumnName("purchase_time")
.HasColumnType("timestamptz")
.IsRequired(true);
builder
.Property(a => a.Returned)
.HasColumnName("returned")
.HasColumnType("boolean")
.IsRequired(true);
builder
.Property(a => a.TravelTime)
.HasColumnName("travel_time")
.HasColumnType("interval")
.IsRequired(true);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.PostgreSql.Migrations
{
/// <inheritdoc />
public partial class Add_Ticket_and_TicketGroup : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateSequence(
name: "ticket_groups_id_sequence",
schema: "application");
migrationBuilder.CreateSequence(
name: "tickets_id_sequence",
schema: "application");
migrationBuilder.CreateTable(
name: "ticket_groups",
schema: "application",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false, defaultValueSql: "nextval('application.ticket_groups_id_sequence')"),
passanger_first_name = table.Column<string>(type: "varchar(32)", nullable: false),
passanger_last_name = table.Column<string>(type: "varchar(32)", nullable: false),
passanger_patronymic = table.Column<string>(type: "varchar(32)", nullable: false),
passanger_sex = table.Column<string>(type: "varchar(32)", nullable: false),
passanger_birth_date = table.Column<DateOnly>(type: "date", nullable: false),
purchase_time = table.Column<DateTimeOffset>(type: "timestamptz", nullable: false),
returned = table.Column<bool>(type: "boolean", nullable: false),
travel_time = table.Column<TimeSpan>(type: "interval", nullable: false),
uuid = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_ticket_groups", x => x.id);
table.UniqueConstraint("altk_ticket_groups_uuid", x => x.uuid);
table.CheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')");
});
migrationBuilder.CreateTable(
name: "tickets",
schema: "application",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false, defaultValueSql: "nextval('application.tickets_id_sequence')"),
departure_route_address_id = table.Column<long>(type: "bigint", nullable: false),
arrival_route_address_id = table.Column<long>(type: "bigint", nullable: false),
order = table.Column<short>(type: "smallint", nullable: false),
currency = table.Column<string>(type: "varchar(8)", nullable: false),
cost = table.Column<decimal>(type: "numeric(24,12)", nullable: false),
ticket_group_id = table.Column<long>(type: "bigint", nullable: false),
vehicle_enrollment_id = table.Column<long>(type: "bigint", nullable: false),
uuid = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_tickets", x => x.id);
table.UniqueConstraint("altk_tickets_uuid", x => x.uuid);
table.CheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')");
table.ForeignKey(
name: "FK_tickets_route_addresses_arrival_route_address_id",
column: x => x.arrival_route_address_id,
principalSchema: "application",
principalTable: "route_addresses",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_tickets_route_addresses_departure_route_address_id",
column: x => x.departure_route_address_id,
principalSchema: "application",
principalTable: "route_addresses",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_tickets_ticket_group_id",
column: x => x.ticket_group_id,
principalSchema: "application",
principalTable: "ticket_groups",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_tickets_vehicle_enrollment_id",
column: x => x.vehicle_enrollment_id,
principalSchema: "application",
principalTable: "vehicle_enrollments",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_tickets_arrival_route_address_id",
schema: "application",
table: "tickets",
column: "arrival_route_address_id");
migrationBuilder.CreateIndex(
name: "IX_tickets_departure_route_address_id",
schema: "application",
table: "tickets",
column: "departure_route_address_id");
migrationBuilder.CreateIndex(
name: "ix_tickets_ticket_group_id",
schema: "application",
table: "tickets",
column: "ticket_group_id");
migrationBuilder.CreateIndex(
name: "ix_tickets_vehicle_enrollment_id",
schema: "application",
table: "tickets",
column: "vehicle_enrollment_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "tickets",
schema: "application");
migrationBuilder.DropTable(
name: "ticket_groups",
schema: "application");
migrationBuilder.DropSequence(
name: "ticket_groups_id_sequence",
schema: "application");
migrationBuilder.DropSequence(
name: "tickets_id_sequence",
schema: "application");
}
}
}

View File

@ -43,6 +43,10 @@ namespace Persistence.PostgreSql.Migrations
modelBuilder.HasSequence("routes_id_sequence");
modelBuilder.HasSequence("ticket_groups_id_sequence");
modelBuilder.HasSequence("tickets_id_sequence");
modelBuilder.HasSequence("vehicle_enrollments_id_sequence");
modelBuilder.HasSequence("vehicles_id_sequence");
@ -466,6 +470,133 @@ namespace Persistence.PostgreSql.Migrations
b.ToTable("route_address_details", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.tickets_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "tickets_id_sequence");
b.Property<long>("ArrivalRouteAddressId")
.HasColumnType("bigint")
.HasColumnName("arrival_route_address_id");
b.Property<decimal>("Cost")
.HasColumnType("numeric(24,12)")
.HasColumnName("cost");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("varchar(8)")
.HasColumnName("currency");
b.Property<long>("DepartureRouteAddressId")
.HasColumnType("bigint")
.HasColumnName("departure_route_address_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<short>("Order")
.HasColumnType("smallint")
.HasColumnName("order");
b.Property<long>("TicketGroupId")
.HasColumnType("bigint")
.HasColumnName("ticket_group_id");
b.Property<long>("VehicleEnrollmentId")
.HasColumnType("bigint")
.HasColumnName("vehicle_enrollment_id");
b.HasKey("Id")
.HasName("pk_tickets");
b.HasAlternateKey("Guid")
.HasName("altk_tickets_uuid");
b.HasIndex("ArrivalRouteAddressId");
b.HasIndex("DepartureRouteAddressId");
b.HasIndex("TicketGroupId")
.HasDatabaseName("ix_tickets_ticket_group_id");
b.HasIndex("VehicleEnrollmentId")
.HasDatabaseName("ix_tickets_vehicle_enrollment_id");
b.ToTable("tickets", "application", t =>
{
t.HasCheckConstraint("ck_tickets_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')");
});
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "ticket_groups_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<DateOnly>("PassangerBirthDate")
.HasColumnType("date")
.HasColumnName("passanger_birth_date");
b.Property<string>("PassangerFirstName")
.IsRequired()
.HasColumnType("varchar(32)")
.HasColumnName("passanger_first_name");
b.Property<string>("PassangerLastName")
.IsRequired()
.HasColumnType("varchar(32)")
.HasColumnName("passanger_last_name");
b.Property<string>("PassangerPatronymic")
.IsRequired()
.HasColumnType("varchar(32)")
.HasColumnName("passanger_patronymic");
b.Property<string>("PassangerSex")
.IsRequired()
.HasColumnType("varchar(32)")
.HasColumnName("passanger_sex");
b.Property<DateTimeOffset>("PurchaseTime")
.HasColumnType("timestamptz")
.HasColumnName("purchase_time");
b.Property<bool>("Returned")
.HasColumnType("boolean")
.HasColumnName("returned");
b.Property<TimeSpan>("TravelTime")
.HasColumnType("interval")
.HasColumnName("travel_time");
b.HasKey("Id")
.HasName("pk_ticket_groups");
b.HasAlternateKey("Guid")
.HasName("altk_ticket_groups_uuid");
b.ToTable("ticket_groups", "application", t =>
{
t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')");
});
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b =>
{
b.Property<long>("Id")
@ -746,6 +877,43 @@ namespace Persistence.PostgreSql.Migrations
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "ArrivalRouteAddress")
.WithMany()
.HasForeignKey("ArrivalRouteAddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "DepartureRouteAddress")
.WithMany()
.HasForeignKey("DepartureRouteAddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", "TicketGroup")
.WithMany("Tickets")
.HasForeignKey("TicketGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_tickets_ticket_group_id");
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment")
.WithMany("Tickets")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_tickets_vehicle_enrollment_id");
b.Navigation("ArrivalRouteAddress");
b.Navigation("DepartureRouteAddress");
b.Navigation("TicketGroup");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company")
@ -823,6 +991,11 @@ namespace Persistence.PostgreSql.Migrations
b.Navigation("Details");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.TicketGroup", b =>
{
b.Navigation("Tickets");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b =>
{
b.Navigation("Enrollments");
@ -831,6 +1004,8 @@ namespace Persistence.PostgreSql.Migrations
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b =>
{
b.Navigation("RouteAddressDetails");
b.Navigation("Tickets");
});
#pragma warning restore 612, 618
}

View File

@ -28,6 +28,10 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork
new PostgreSqlRouteAddressRepository(_dbContext);
CompanyRepository = new PostgreSqlCompanyRepository(_dbContext);
EmployeeRepository = new PostgreSqlEmployeeRepository(_dbContext);
TicketGroupRepository = new PostgreSqlTicketGroupRepository(_dbContext);
TicketRepository = new PostgreSqlTicketRepository(_dbContext);
RouteAddressDetailRepository =
new PostgreSqlRouteAddressDetailRepository(_dbContext);
}
public CountryRepository CountryRepository { get; init; }
@ -56,6 +60,12 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork
public EmployeeRepository EmployeeRepository { get; init; }
public TicketGroupRepository TicketGroupRepository { get; init; }
public TicketRepository TicketRepository { get; init; }
public RouteAddressDetailRepository RouteAddressDetailRepository { get; init; }
public int Save()
{
return _dbContext.SaveChanges();

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories;
public sealed class PostgreSqlRouteAddressDetailRepository :
PostgreSqlBaseRepository<RouteAddressDetail>, RouteAddressDetailRepository
{
public PostgreSqlRouteAddressDetailRepository(PostgreSqlDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories;
public sealed class PostgreSqlTicketGroupRepository :
PostgreSqlBaseRepository<TicketGroup>, TicketGroupRepository
{
public PostgreSqlTicketGroupRepository(PostgreSqlDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories;
public sealed class PostgreSqlTicketRepository :
PostgreSqlBaseRepository<Ticket>, TicketRepository
{
public PostgreSqlTicketRepository(PostgreSqlDbContext dbContext)
: base(dbContext) { }
}

View File

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

View File

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