From 6830fea5633e960dc73010fcee41322924f8eae4 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Tue, 20 May 2025 20:39:09 +0300 Subject: [PATCH] add ticket group creation --- .../FluentValidation/CustomValidators.cs | 31 + .../RouteAddressDetailRepository.cs | 7 + .../Repositories/TicketGroupRepository.cs | 6 + .../Repositories/TicketRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 6 + .../Resources/Localization/en-US.json | 3 +- .../AddTicketGroup/AddTicketGroupCommand.cs | 25 + .../AddTicketGroupCommandAuthorizer.cs | 31 + .../AddTicketGroupCommandHandler.cs | 477 ++++++++ .../AddTicketGroupCommandValidator.cs | 107 ++ .../TicketGroups/Models/TicketModel.cs | 13 + .../TicketGroups/TicketAddressDto.cs | 53 + src/Application/TicketGroups/TicketDto.cs | 50 + .../TicketGroups/TicketGroupDto.cs | 45 + .../ViewModels/AddTicketGroupViewModel.cs | 21 + .../GetTicketGroupsPageFilterViewModel.cs | 12 + .../ViewModels/TicketViewModel.cs | 13 + .../ViewModels/UpdateTicketGroupViewModel.cs | 21 + .../AddVehicleEnrollmentCommandHandler.cs | 18 +- .../UpdateVehicleEnrollmentCommandHandler.cs | 18 +- src/Domain/Entities/Ticket.cs | 30 + src/Domain/Entities/TicketGroup.cs | 25 + .../Controllers/TicketGroupsController.cs | 221 ++++ src/Persistence/InMemory/InMemoryDbContext.cs | 28 +- .../InMemory/InMemoryUnitOfWork.cs | 10 + .../InMemoryRouteAddressDetailRepository.cs | 11 + .../InMemoryTicketGroupRepository.cs | 11 + .../Repositories/InMemoryTicketRepository.cs | 11 + .../Configurations/TicketConfiguration.cs | 105 ++ .../TicketGroupConfiguration.cs | 75 ++ ...941_Add_Ticket_and_TicketGroup.Designer.cs | 1016 +++++++++++++++++ ...250519212941_Add_Ticket_and_TicketGroup.cs | 140 +++ .../PostgreSqlDbContextModelSnapshot.cs | 175 +++ .../PostgreSql/PostgreSqlUnitOfWork.cs | 10 + .../PostgreSqlRouteAddressDetailRepository.cs | 11 + .../PostgreSqlTicketGroupRepository.cs | 11 + .../PostgreSqlTicketRepository.cs | 11 + 37 files changed, 2843 insertions(+), 21 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs create mode 100644 src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs create mode 100644 src/Application/TicketGroups/Models/TicketModel.cs create mode 100644 src/Application/TicketGroups/TicketAddressDto.cs create mode 100644 src/Application/TicketGroups/TicketDto.cs create mode 100644 src/Application/TicketGroups/TicketGroupDto.cs create mode 100644 src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs create mode 100644 src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs create mode 100644 src/Application/TicketGroups/ViewModels/TicketViewModel.cs create mode 100644 src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs create mode 100644 src/Domain/Entities/Ticket.cs create mode 100644 src/Domain/Entities/TicketGroup.cs create mode 100644 src/HttpApi/Controllers/TicketGroupsController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs index aa2dadb..f7b1de1 100644 --- a/src/Application/Common/FluentValidation/CustomValidators.cs +++ b/src/Application/Common/FluentValidation/CustomValidators.cs @@ -21,4 +21,35 @@ public static class CustomValidators ruleBuilder .Matches(@"^\+[0-9]{7,15}$"); } + + public static IRuleBuilderOptions> + IsUnique( + this IRuleBuilder> ruleBuilder, + Func 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( + this IEnumerable elements, Func selector) + { + var hashSet = new HashSet(); + foreach (var element in elements.Select(selector)) + { + if (!hashSet.Contains(element)) + hashSet.Add(element); + else + return false; + } + return true; + } } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs new file mode 100644 index 0000000..2bac9d4 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/RouteAddressDetailRepository.cs @@ -0,0 +1,7 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface RouteAddressDetailRepository : + BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs new file mode 100644 index 0000000..ead97c3 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/TicketGroupRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface TicketGroupRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs new file mode 100644 index 0000000..57b96aa --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/TicketRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface TicketRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 7c0b461..de5c4ee 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -30,6 +30,12 @@ public interface UnitOfWork : IDisposable EmployeeRepository EmployeeRepository { get; } + TicketGroupRepository TicketGroupRepository { get; } + + TicketRepository TicketRepository { get; } + + RouteAddressDetailRepository RouteAddressDetailRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 65c8189..be9ac72 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -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.", diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs new file mode 100644 index 0000000..554344b --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs @@ -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 +{ + 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 Tickets { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs new file mode 100644 index 0000000..b2c5deb --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandAuthorizer.cs @@ -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 +{ + 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 + }); + } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs new file mode 100644 index 0000000..efe1455 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -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 +{ + 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 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 + { + new() + { + PropertyName = nameof(request.Tickets) + } + }); + } + + if (departureRouteAddress.Order > arrivalRouteAddress.Order) + { + throw new ValidationException( + new List + { + 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 + { + 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 + { + 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(entity); + } + } +} diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs new file mode 100644 index 0000000..8ead4b4 --- /dev/null +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs @@ -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 +{ + 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"]); + }); + } +} diff --git a/src/Application/TicketGroups/Models/TicketModel.cs b/src/Application/TicketGroups/Models/TicketModel.cs new file mode 100644 index 0000000..2cd2960 --- /dev/null +++ b/src/Application/TicketGroups/Models/TicketModel.cs @@ -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; } +} diff --git a/src/Application/TicketGroups/TicketAddressDto.cs b/src/Application/TicketGroups/TicketAddressDto.cs new file mode 100644 index 0000000..d62e5c2 --- /dev/null +++ b/src/Application/TicketGroups/TicketAddressDto.cs @@ -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
+{ + 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() + .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)); + } +} diff --git a/src/Application/TicketGroups/TicketDto.cs b/src/Application/TicketGroups/TicketDto.cs new file mode 100644 index 0000000..93194a5 --- /dev/null +++ b/src/Application/TicketGroups/TicketDto.cs @@ -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 +{ + 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() + .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)); + } +} diff --git a/src/Application/TicketGroups/TicketGroupDto.cs b/src/Application/TicketGroups/TicketGroupDto.cs new file mode 100644 index 0000000..ae9e0e8 --- /dev/null +++ b/src/Application/TicketGroups/TicketGroupDto.cs @@ -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 +{ + 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 Tickets { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .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( + s => s.PurchaseTime)); + } +} diff --git a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs new file mode 100644 index 0000000..dd292ff --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs @@ -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 Tickets { get; set; } +} diff --git a/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs new file mode 100644 index 0000000..82399f3 --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/GetTicketGroupsPageFilterViewModel.cs @@ -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; } +} diff --git a/src/Application/TicketGroups/ViewModels/TicketViewModel.cs b/src/Application/TicketGroups/ViewModels/TicketViewModel.cs new file mode 100644 index 0000000..15560d1 --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/TicketViewModel.cs @@ -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; } +} diff --git a/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs new file mode 100644 index 0000000..b58310e --- /dev/null +++ b/src/Application/TicketGroups/ViewModels/UpdateTicketGroupViewModel.cs @@ -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 Tickets { get; set; } +} diff --git a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs index 3f4dac9..9688d98 100644 --- a/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/AddVehicleEnrollment/AddVehicleEnrollmentCommandHandler.cs @@ -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(); diff --git a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs index 1b2d0b7..d562c45 100644 --- a/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs +++ b/src/Application/VehicleEnrollments/Commands/UpdateVehicleEnrollment/UpdateVehicleEnrollmentCommandHandler.cs @@ -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(); diff --git a/src/Domain/Entities/Ticket.cs b/src/Domain/Entities/Ticket.cs new file mode 100644 index 0000000..3865d77 --- /dev/null +++ b/src/Domain/Entities/Ticket.cs @@ -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; } +} diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs new file mode 100644 index 0000000..b044ceb --- /dev/null +++ b/src/Domain/Entities/TicketGroup.cs @@ -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 Tickets { get; set; } +} diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs new file mode 100644 index 0000000..59b8cb6 --- /dev/null +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -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> 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))] + // [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> 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 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 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 Delete( + // [FromRoute] Guid uuid, + // CancellationToken cancellationToken) + // { + // await Mediator.Send( + // new DeleteTicketGroupCommand() { Guid = uuid }, + // cancellationToken); + // return StatusCode(StatusCodes.Status204NoContent); + // } +} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index e27b3ba..3dc85f8 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -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() + .ToTable("vehicles") + .UseTphMappingStrategy() + .HasDiscriminator(v => v.VehicleType) + .HasValue(VehicleType.Bus) + .HasValue(VehicleType.Aircraft) + .HasValue(VehicleType.Train); } protected override void ConfigureConventions( @@ -19,12 +29,26 @@ public class InMemoryDbContext : DbContext { builder .Properties() - .HaveColumnType("vehicle_type") + .HaveColumnType("varchar(16)") .HaveConversion(); builder .Properties() - .HaveColumnType("currency") + .HaveColumnType("varchar(8)") .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + + builder + .Properties() + .HaveConversion(); } } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index f7f1d15..4b257ff 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -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(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs new file mode 100644 index 0000000..992e9a1 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRouteAddressDetailRepository.cs @@ -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, RouteAddressDetailRepository +{ + public InMemoryRouteAddressDetailRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs new file mode 100644 index 0000000..613715b --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketGroupRepository.cs @@ -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, TicketGroupRepository +{ + public InMemoryTicketGroupRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs new file mode 100644 index 0000000..ab406b0 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryTicketRepository.cs @@ -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, TicketRepository +{ + public InMemoryTicketRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs new file mode 100644 index 0000000..2b2771d --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TicketConfiguration.cs @@ -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 +{ + public override void Configure(EntityTypeBuilder 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()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs new file mode 100644 index 0000000..9bc2947 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -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 +{ + public override void Configure(EntityTypeBuilder 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); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs new file mode 100644 index 0000000..37b26bc --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.Designer.cs @@ -0,0 +1,1016 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250519212941_Add_Ticket_and_TicketGroup")] + partial class Add_Ticket_and_TicketGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + 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"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Returned") + .HasColumnType("boolean") + .HasColumnName("returned"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + 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") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + 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"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs new file mode 100644 index 0000000..6cbe97e --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250519212941_Add_Ticket_and_TicketGroup.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Ticket_and_TicketGroup : Migration + { + /// + 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(type: "bigint", nullable: false, defaultValueSql: "nextval('application.ticket_groups_id_sequence')"), + passanger_first_name = table.Column(type: "varchar(32)", nullable: false), + passanger_last_name = table.Column(type: "varchar(32)", nullable: false), + passanger_patronymic = table.Column(type: "varchar(32)", nullable: false), + passanger_sex = table.Column(type: "varchar(32)", nullable: false), + passanger_birth_date = table.Column(type: "date", nullable: false), + purchase_time = table.Column(type: "timestamptz", nullable: false), + returned = table.Column(type: "boolean", nullable: false), + travel_time = table.Column(type: "interval", nullable: false), + uuid = table.Column(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(type: "bigint", nullable: false, defaultValueSql: "nextval('application.tickets_id_sequence')"), + departure_route_address_id = table.Column(type: "bigint", nullable: false), + arrival_route_address_id = table.Column(type: "bigint", nullable: false), + order = table.Column(type: "smallint", nullable: false), + currency = table.Column(type: "varchar(8)", nullable: false), + cost = table.Column(type: "numeric(24,12)", nullable: false), + ticket_group_id = table.Column(type: "bigint", nullable: false), + vehicle_enrollment_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(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"); + } + + /// + 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"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index c2eb06f..fb4e67f 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.tickets_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "tickets_id_sequence"); + + b.Property("ArrivalRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("arrival_route_address_id"); + + b.Property("Cost") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureRouteAddressId") + .HasColumnType("bigint") + .HasColumnName("departure_route_address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("TicketGroupId") + .HasColumnType("bigint") + .HasColumnName("ticket_group_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.ticket_groups_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "ticket_groups_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("PassangerBirthDate") + .HasColumnType("date") + .HasColumnName("passanger_birth_date"); + + b.Property("PassangerFirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_first_name"); + + b.Property("PassangerLastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_last_name"); + + b.Property("PassangerPatronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_patronymic"); + + b.Property("PassangerSex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("passanger_sex"); + + b.Property("PurchaseTime") + .HasColumnType("timestamptz") + .HasColumnName("purchase_time"); + + b.Property("Returned") + .HasColumnType("boolean") + .HasColumnName("returned"); + + b.Property("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("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 } diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 46ff3a9..9b324a2 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -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(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs new file mode 100644 index 0000000..d88305f --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRouteAddressDetailRepository.cs @@ -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, RouteAddressDetailRepository +{ + public PostgreSqlRouteAddressDetailRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs new file mode 100644 index 0000000..204d808 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketGroupRepository.cs @@ -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, TicketGroupRepository +{ + public PostgreSqlTicketGroupRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs new file mode 100644 index 0000000..b2f53f6 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlTicketRepository.cs @@ -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, TicketRepository +{ + public PostgreSqlTicketRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +}