From afe626bd78f34d73ba0800bfe907d505279e9eda Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Mon, 26 May 2025 12:16:46 +0300 Subject: [PATCH] add LiqPay integration for ticket purchase hosted services deletes ticket groups with reserved status that were created more than 10 minutes ago payment link expires in 10 minutes from the time it was created --- src/Application/Application.csproj | 1 + .../Authorization/AllowAllRequirement.cs | 17 + .../MustBeAuthenticatedRequirement.cs | 3 - .../Services/LiqPayPaymentService.cs | 13 + .../GetPaymentLink/GetPaymentLinkCommand.cs | 25 + .../GetPaymentLinkCommandAuthorizer.cs | 32 + .../GetPaymentLinkCommandHandler.cs | 474 ++++++++ .../GetPaymentLinkCommandValidator.cs | 101 ++ .../ProcessCallback/ProcessCallbackCommand.cs | 11 + .../ProcessCallbackCommandAuthorizer.cs | 14 + .../ProcessCallbackCommandHandler.cs | 70 ++ .../ProcessCallbackCommandValidator.cs | 23 + .../Models/TicketGroupPaymentTicketModel.cs | 14 + .../ViewModels/CallbackViewModel.cs | 9 + .../ViewModels/TicketGroupPaymentViewModel.cs | 21 + .../ViewModels/TicketPaymentViewModel.cs | 14 + src/Application/Payments/PaymentLinkDto.cs | 6 + .../Resources/Localization/en-US.json | 3 + .../AddTicketGroup/AddTicketGroupCommand.cs | 2 +- .../AddTicketGroupCommandHandler.cs | 10 +- .../AddTicketGroupCommandValidator.cs | 9 + .../RemoveOldReservedTicketGroupsCommand.cs | 8 + ...ldReservedTicketGroupsCommandAuthorizer.cs | 14 + ...veOldReservedTicketGroupsCommandHandler.cs | 44 + ...OldReservedTicketGroupsCommandValidator.cs | 20 + .../ViewModels/AddTicketGroupViewModel.cs | 2 +- src/Application/packages.lock.json | 6 + .../Configuration/Configuration.cs | 6 + .../Infrastructure/Configuration.cs | 6 +- src/Configuration/packages.lock.json | 1 + src/Domain/Entities/TicketGroup.cs | 2 +- src/Domain/Enums/TicketStatus.cs | 25 + src/HttpApi/Controllers/PaymentController.cs | 92 ++ .../Controllers/TicketGroupsController.cs | 8 +- .../ReservedTicketRemoverHostedService.cs | 41 + src/HttpApi/Program.cs | 3 + src/HttpApi/appsettings.Development.json | 10 + src/HttpApi/appsettings.json | 10 + src/HttpApi/packages.lock.json | 1 + src/Identity/packages.lock.json | 6 + src/Infrastructure/ConfigurationOptions.cs | 20 +- .../ExchangeApiCurrencyConverterService.cs | 2 + .../Services/LiqPayPaymentService.cs | 78 ++ src/Infrastructure/packages.lock.json | 1 + src/Persistence/InMemory/InMemoryDbContext.cs | 5 + .../TicketGroupConfiguration.cs | 38 +- ...743_Add_status_to_Ticket_Group.Designer.cs | 1019 +++++++++++++++++ ...250524184743_Add_status_to_Ticket_Group.cs | 55 + .../PostgreSqlDbContextModelSnapshot.cs | 9 +- .../PostgreSql/PostgreSqlDbContext.cs | 5 + .../TypeConverters/TicketStatusConverter.cs | 13 + src/Persistence/packages.lock.json | 6 + .../packages.lock.json | 1 + 53 files changed, 2396 insertions(+), 33 deletions(-) create mode 100644 src/Application/Common/Authorization/AllowAllRequirement.cs create mode 100644 src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs create mode 100644 src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs create mode 100644 src/Application/Payments/PaymentLinkDto.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs create mode 100644 src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs create mode 100644 src/Domain/Enums/TicketStatus.cs create mode 100644 src/HttpApi/Controllers/PaymentController.cs create mode 100644 src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs create mode 100644 src/Infrastructure/Services/LiqPayPaymentService.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs create mode 100644 src/Persistence/TypeConverters/TicketStatusConverter.cs diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 9b7a24d..0e77ba6 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Application/Common/Authorization/AllowAllRequirement.cs b/src/Application/Common/Authorization/AllowAllRequirement.cs new file mode 100644 index 0000000..05c7d78 --- /dev/null +++ b/src/Application/Common/Authorization/AllowAllRequirement.cs @@ -0,0 +1,17 @@ +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class AllowAllRequirement : IAuthorizationRequirement +{ + class MustBeAuthenticatedRequirementHandler : + IAuthorizationHandler + { + public Task Handle( + AllowAllRequirement request, + CancellationToken cancellationToken) + { + return Task.FromResult(AuthorizationResult.Succeed()); + } + } +} diff --git a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs index 809c638..685e587 100644 --- a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs +++ b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs @@ -1,4 +1,3 @@ -// using cuqmbr.TravelGuide.Application.Common.Exceptions; using MediatR.Behaviors.Authorization; namespace cuqmbr.TravelGuide.Application.Common.Authorization; @@ -17,8 +16,6 @@ public class MustBeAuthenticatedRequirement : IAuthorizationRequirement if (!request.IsAuthenticated) { return Task.FromResult(AuthorizationResult.Fail()); - // TODO: Remove UnAuthorizedException, isn't used - // throw new UnAuthorizedException(); } return Task.FromResult(AuthorizationResult.Succeed()); diff --git a/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs b/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs new file mode 100644 index 0000000..4a0551f --- /dev/null +++ b/src/Application/Common/Interfaces/Services/LiqPayPaymentService.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +public interface LiqPayPaymentService +{ + Task GetPaymentLinkAsync( + decimal amount, Currency currency, + string orderId, TimeSpan validity, string description, + string resultPath, string callbackPath); + + Task IsValidSignatureAsync(string postData, string postSignature); +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs new file mode 100644 index 0000000..98f3956 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommand.cs @@ -0,0 +1,25 @@ +using cuqmbr.TravelGuide.Application.Payments.LiqPay.TicketGroups.Models; +using cuqmbr.TravelGuide.Domain.Enums; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public record GetPaymentLinkCommand : 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 ICollection Tickets { get; set; } + + + public string ResultPath { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs new file mode 100644 index 0000000..a1ad297 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandAuthorizer.cs @@ -0,0 +1,32 @@ +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.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetPaymentLinkCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetPaymentLinkCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs new file mode 100644 index 0000000..28c9328 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandHandler.cs @@ -0,0 +1,474 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Domain.Enums; +using FluentValidation.Results; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + private readonly CurrencyConverterService _currencyConverterService; + + private readonly LiqPayPaymentService _liqPayPaymentService; + + private readonly IStringLocalizer _localizer; + + public GetPaymentLinkCommandHandler( + UnitOfWork unitOfWork, + CurrencyConverterService currencyConverterService, + LiqPayPaymentService liqPayPaymentService, + IStringLocalizer localizer) + { + _unitOfWork = unitOfWork; + _currencyConverterService = currencyConverterService; + _liqPayPaymentService = liqPayPaymentService; + _localizer = localizer; + } + + public async Task Handle( + GetPaymentLinkCommand 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 unavailableTicketStatuses = new TicketStatus[] + { + TicketStatus.Reserved, + TicketStatus.Purchased + }; + + var ticketGroupTickets = (await _unitOfWork.TicketRepository + .GetPageAsync( + e => + vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && + unavailableTicketStatuses.Contains(e.TicketGroup.Status), + 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 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 travelTime = + ticketsDetails.OrderBy(td => td.order).Last().arrivalTime - + ticketsDetails.OrderBy(td => td.order).First().departureTime; + + var entity = new TicketGroup() + { + PassangerFirstName = request.PassangerFirstName, + PassangerLastName = request.PassangerLastName, + PassangerPatronymic = request.PassangerPatronymic, + PassangerSex = request.PassangerSex, + PassangerBirthDate = request.PassangerBirthDate, + PurchaseTime = DateTimeOffset.UtcNow, + Status = TicketStatus.Reserved, + 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 detail = ticketsDetails + .SingleOrDefault(td => td.order == t.Order); + + var currency = Currency.UAH; + var cost = _currencyConverterService + .ConvertAsync( + detail.cost, detail.currency, currency, + cancellationToken).Result; + + return new Ticket() + { + DepartureRouteAddressId = departureRouteAddress.Id, + DepartureRouteAddress = departureRouteAddress, + ArrivalRouteAddressId = arrivalRouteAddress.Id, + ArrivalRouteAddress = arrivalRouteAddress, + Order = t.Order, + Cost = cost, + Currency = currency, + VehicleEnrollmentId = ve.Id + }; + }) + .ToArray() + }; + + entity = await _unitOfWork.TicketGroupRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + + var amount = entity.Tickets.Sum(e => e.Cost); + var guid = entity.Guid; + var validity = TimeSpan.FromMinutes(10); + var resultPath = request.ResultPath; + var callbackPath = "/payments/liqPay/ticket/callback"; + + var paymentLink = await _liqPayPaymentService + .GetPaymentLinkAsync( + amount, Currency.UAH, guid.ToString(), validity, + _localizer["PaymentProcessing.TicketPaymentDescription"], + resultPath, callbackPath); + + return new PaymentLinkDto() { PaymentLink = paymentLink }; + } + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs new file mode 100644 index 0000000..7fbc8eb --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/GetPaymentLink/GetPaymentLinkCommandValidator.cs @@ -0,0 +1,101 @@ +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.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; + +public class GetPaymentLinkCommandValidator : + AbstractValidator +{ + public GetPaymentLinkCommandValidator( + 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.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/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs new file mode 100644 index 0000000..60b6068 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public record ProcessCallbackCommand : IRequest +{ + public string Data { get; set; } + + public string Signature { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs new file mode 100644 index 0000000..200d9db --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy(ProcessCallbackCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs new file mode 100644 index 0000000..099fc63 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs @@ -0,0 +1,70 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using System.Text; +using Newtonsoft.Json; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly LiqPayPaymentService _liqPayPaymentService; + + public ProcessCallbackCommandHandler( + UnitOfWork unitOfWork, + LiqPayPaymentService liqPayPaymentService) + { + _unitOfWork = unitOfWork; + _liqPayPaymentService = liqPayPaymentService; + } + + public async Task Handle( + ProcessCallbackCommand request, + CancellationToken cancellationToken) + { + var isSignatureValid = await _liqPayPaymentService + .IsValidSignatureAsync(request.Data, request.Signature); + + if (!isSignatureValid) + { + throw new ForbiddenException(); + } + + var dataBytes = Convert.FromBase64String(request.Data); + var dataJson = Encoding.UTF8.GetString(dataBytes); + + var data = JsonConvert.DeserializeObject(dataJson); + + string status = data.status; + + var ticketGroupGuid = Guid.Parse((string)data.order_id); + var ticketGroup = await _unitOfWork.TicketGroupRepository + .GetOneAsync(e => e.Guid == ticketGroupGuid, cancellationToken); + + if (ticketGroup == null || + ticketGroup.Status == TicketStatus.Purchased) + { + throw new ForbiddenException(); + } + + if (status.Equals("error") || status.Equals("failure")) + { + await _unitOfWork.TicketGroupRepository + .DeleteOneAsync(ticketGroup, cancellationToken); + } + else if (status.Equals("success")) + { + ticketGroup.Status = TicketStatus.Purchased; + await _unitOfWork.TicketGroupRepository + .UpdateOneAsync(ticketGroup, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs new file mode 100644 index 0000000..70faa59 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandValidator.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +public class ProcessCallbackCommandValidator : + AbstractValidator +{ + public ProcessCallbackCommandValidator( + IStringLocalizer localizer, + SessionCultureService cultureService) + { + RuleFor(v => v.Data) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Signature) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs b/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs new file mode 100644 index 0000000..4bc5a3b --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/Models/TicketGroupPaymentTicketModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Models; + +public sealed class TicketGroupPaymentTicketModel +{ + 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/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs new file mode 100644 index 0000000..ca696c0 --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/CallbackViewModel.cs @@ -0,0 +1,9 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class CallbackViewModel +{ + public string Data { get; set; } + + public string Signature { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs new file mode 100644 index 0000000..4d6d83b --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketGroupPaymentViewModel.cs @@ -0,0 +1,21 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class TicketGroupPaymentViewModel +{ + 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 ICollection Tickets { get; set; } + + + public string ResultPath { get; set; } +} diff --git a/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs new file mode 100644 index 0000000..f30684f --- /dev/null +++ b/src/Application/Payments/LiqPay/TicketGroups/ViewModels/TicketPaymentViewModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; + +public sealed class TicketPaymentViewModel +{ + 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/Payments/PaymentLinkDto.cs b/src/Application/Payments/PaymentLinkDto.cs new file mode 100644 index 0000000..d47bde9 --- /dev/null +++ b/src/Application/Payments/PaymentLinkDto.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Payments; + +public sealed class PaymentLinkDto +{ + public string PaymentLink { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index be9ac72..dbec72c 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -58,5 +58,8 @@ "Title": "One or more internal server errors occurred.", "Detail": "Report this error to service's support team." } + }, + "PaymentProcessing": { + "TicketPaymentDescription": "Ticket purchase." } } diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs index 554344b..a77bfee 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommand.cs @@ -18,7 +18,7 @@ public record AddTicketGroupCommand : IRequest public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public TicketStatus Status { get; set; } public ICollection Tickets { get; set; } diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs index efe1455..25d4092 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandHandler.cs @@ -128,11 +128,17 @@ public class AddTicketGroupCommandHandler : var vehicleEnrollmentGuids = request.Tickets.Select(t => t.VehicleEnrollmentGuid); + var unavailableTicketStatuses = new TicketStatus[] + { + TicketStatus.Reserved, + TicketStatus.Purchased + }; + var ticketGroupTickets = (await _unitOfWork.TicketRepository .GetPageAsync( e => vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) && - e.TicketGroup.Returned == false, + unavailableTicketStatuses.Contains(e.TicketGroup.Status), 1, int.MaxValue, cancellationToken)) .Items; @@ -431,7 +437,7 @@ public class AddTicketGroupCommandHandler : PassangerSex = request.PassangerSex, PassangerBirthDate = request.PassangerBirthDate, PurchaseTime = request.PurchaseTime, - Returned = request.Returned, + Status = request.Status, TravelTime = travelTime, Tickets = request.Tickets.Select( t => diff --git a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs index 8ead4b4..58a8029 100644 --- a/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs +++ b/src/Application/TicketGroups/Commands/AddTicketGroup/AddTicketGroupCommandValidator.cs @@ -67,6 +67,15 @@ public class AddTicketGroupCommandValidator : AbstractValidator tg.Status) + .Must((tg, s) => TicketStatus.Enumerations.ContainsValue(s)) + .WithMessage( + String.Format( + localizer["FluentValidation.MustBeInEnum"], + String.Join( + ", ", + TicketStatus.Enumerations.Values.Select(e => e.Name)))); + RuleFor(tg => tg.Tickets) .IsUnique(t => t.VehicleEnrollmentGuid) .WithMessage(localizer["FluentValidation.IsUnique"]); diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs new file mode 100644 index 0000000..9284fd0 --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.RemoveOldReservedTicketGroups; + +public record RemoveOldReservedTicketGroupsCommand : IRequest +{ + public TimeSpan ReservedFor { get; set; } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs new file mode 100644 index 0000000..b73e99f --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandAuthorizer.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups.Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandAuthorizer : + AbstractRequestAuthorizer +{ + public override void BuildPolicy( + RemoveOldReservedTicketGroupsCommand request) + { + UseRequirement(new AllowAllRequirement()); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs new file mode 100644 index 0000000..37373af --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public RemoveOldReservedTicketGroupsCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + RemoveOldReservedTicketGroupsCommand request, + CancellationToken cancellationToken) + { + var statusesToRemove = new TicketStatus[] + { + TicketStatus.Reserved + }; + + var entities = (await _unitOfWork.TicketGroupRepository + .GetPageAsync( + e => + statusesToRemove.Contains(e.Status) && + DateTimeOffset.UtcNow - e.PurchaseTime > request.ReservedFor, + 1, int.MaxValue, cancellationToken)) + .Items; + + foreach (var entity in entities) + { + await _unitOfWork.TicketGroupRepository + .DeleteOneAsync(entity, cancellationToken); + } + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs new file mode 100644 index 0000000..71a17d2 --- /dev/null +++ b/src/Application/TicketGroups/Commands/RemoveOldReservedTicketGroups/RemoveOldReservedTicketGroupsCommandValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +public class RemoveOldReservedTicketGroupsCommandValidator : + AbstractValidator +{ + public RemoveOldReservedTicketGroupsCommandValidator( + IStringLocalizer localizer) + { + RuleFor(v => v.ReservedFor) + .GreaterThanOrEqualTo(TimeSpan.Zero) + .WithMessage( + String.Format( + localizer["FluentValidation.GreaterThanOrEqualTo"], + TimeSpan.Zero)); + } +} diff --git a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs index dd292ff..9cac564 100644 --- a/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs +++ b/src/Application/TicketGroups/ViewModels/AddTicketGroupViewModel.cs @@ -14,7 +14,7 @@ public sealed class AddTicketGroupViewModel public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public string Status { get; set; } public ICollection Tickets { get; set; } diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json index ac19a92..14b60f0 100644 --- a/src/Application/packages.lock.json +++ b/src/Application/packages.lock.json @@ -59,6 +59,12 @@ "Microsoft.Extensions.Options": "9.0.4" } }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "QuikGraph": { "type": "Direct", "requested": "[2.5.0, )", diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs index 0c6eaa9..5a8d825 100644 --- a/src/Configuration/Configuration/Configuration.cs +++ b/src/Configuration/Configuration/Configuration.cs @@ -4,6 +4,8 @@ using PersistenceConfigurationOptions = cuqmbr.TravelGuide.Persistence.ConfigurationOptions; using ApplicationConfigurationOptions = cuqmbr.TravelGuide.Application.ConfigurationOptions; +using InfrastructureConfigurationOptions = + cuqmbr.TravelGuide.Infrastructure.ConfigurationOptions; using IdentityConfigurationOptions = cuqmbr.TravelGuide.Identity.ConfigurationOptions; @@ -33,6 +35,10 @@ public static class Configuration configuration.GetSection( ApplicationConfigurationOptions.SectionName)); + services.AddOptions().Bind( + configuration.GetSection( + InfrastructureConfigurationOptions.SectionName)); + services.AddOptions().Bind( configuration.GetSection( IdentityConfigurationOptions.SectionName)); diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs index 7e8793c..3df6bb0 100644 --- a/src/Configuration/Infrastructure/Configuration.cs +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -1,4 +1,5 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Infrastructure.Services; using Microsoft.Extensions.DependencyInjection; namespace cuqmbr.TravelGuide.Configuration.Infrastructure; @@ -14,7 +15,10 @@ public static class Configuration services .AddScoped< CurrencyConverterService, - ExchangeApiCurrencyConverterService>(); + ExchangeApiCurrencyConverterService>() + .AddScoped< + cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService, + cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>(); return services; } diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index 8fb4534..f253c37 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -843,6 +843,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Domain/Entities/TicketGroup.cs b/src/Domain/Entities/TicketGroup.cs index b044ceb..c40a2ae 100644 --- a/src/Domain/Entities/TicketGroup.cs +++ b/src/Domain/Entities/TicketGroup.cs @@ -16,7 +16,7 @@ public sealed class TicketGroup : EntityBase public DateTimeOffset PurchaseTime { get; set; } - public bool Returned { get; set; } + public TicketStatus Status { get; set; } public TimeSpan TravelTime { get; set; } diff --git a/src/Domain/Enums/TicketStatus.cs b/src/Domain/Enums/TicketStatus.cs new file mode 100644 index 0000000..b3c0b3a --- /dev/null +++ b/src/Domain/Enums/TicketStatus.cs @@ -0,0 +1,25 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +public abstract class TicketStatus : Enumeration +{ + public static readonly TicketStatus Reserved = new ReservedTicketStatus(); + public static readonly TicketStatus Returned = new ReturnedTicketStatus(); + public static readonly TicketStatus Purchased = new PurchasedTicketStatus(); + + protected TicketStatus(int value, string name) : base(value, name) { } + + private sealed class ReservedTicketStatus : TicketStatus + { + public ReservedTicketStatus() : base(0, "reserved") { } + } + + private sealed class ReturnedTicketStatus : TicketStatus + { + public ReturnedTicketStatus() : base(1, "returned") { } + } + + private sealed class PurchasedTicketStatus : TicketStatus + { + public PurchasedTicketStatus() : base(2, "purchased") { } + } +} diff --git a/src/HttpApi/Controllers/PaymentController.cs b/src/HttpApi/Controllers/PaymentController.cs new file mode 100644 index 0000000..b7c2e70 --- /dev/null +++ b/src/HttpApi/Controllers/PaymentController.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Payments; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Models; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.ViewModels; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.GetPaymentLink; +using cuqmbr.TravelGuide.Application.Payments.LiqPay + .TicketGroups.Commands.ProcessCallback; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("payments")] +public class PaymentController : ControllerBase +{ + [HttpPost("liqPay/ticket/getLink")] + [SwaggerOperation("Get payment link for provided ticket")] + [SwaggerResponse( + StatusCodes.Status200OK, "Successfuly created", + typeof(PaymentLinkDto))] + [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> LiqPayTicketGetLink( + [FromBody] TicketGroupPaymentViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status200OK, + await Mediator.Send( + new GetPaymentLinkCommand() + { + PassangerFirstName = viewModel.PassangerFirstName, + PassangerLastName = viewModel.PassangerLastName, + PassangerPatronymic = viewModel.PassangerPatronymic, + PassangerSex = Sex.FromName(viewModel.PassangerSex), + PassangerBirthDate = viewModel.PassangerBirthDate, + Tickets = viewModel.Tickets.Select(e => + new TicketGroupPaymentTicketModel() + { + DepartureRouteAddressGuid = e.DepartureRouteAddressUuid, + ArrivalRouteAddressGuid = e.ArrivalRouteAddressUuid, + Order = e.Order, + VehicleEnrollmentGuid = e.VehicleEnrollmentUuid + }) + .ToArray(), + ResultPath = viewModel.ResultPath + }, + cancellationToken)); + } + + [Consumes("application/x-www-form-urlencoded")] + [HttpPost("liqPay/ticket/callback")] + [SwaggerOperation("Process LiqPay callback for ticket")] + [SwaggerResponse( + StatusCodes.Status200OK, "Successfuly processed")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task LiqPayTicketCallback( + [FromForm] CallbackViewModel viewModel, + CancellationToken cancellationToken) + { + await Mediator.Send( + new ProcessCallbackCommand() + { + Data = viewModel.Data, + Signature = viewModel.Signature + }, + cancellationToken); + } +} diff --git a/src/HttpApi/Controllers/TicketGroupsController.cs b/src/HttpApi/Controllers/TicketGroupsController.cs index 59b8cb6..adf8e3d 100644 --- a/src/HttpApi/Controllers/TicketGroupsController.cs +++ b/src/HttpApi/Controllers/TicketGroupsController.cs @@ -1,14 +1,8 @@ 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; @@ -56,7 +50,7 @@ public class TicketGroupsController : ControllerBase PassangerSex = Sex.FromName(viewModel.PassangerSex), PassangerBirthDate = viewModel.PassangerBirthDate, PurchaseTime = viewModel.PurchaseTime, - Returned = viewModel.Returned, + Status = TicketStatus.FromName(viewModel.Status), Tickets = viewModel.Tickets.Select(e => new TicketModel() { diff --git a/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs b/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs new file mode 100644 index 0000000..93de31a --- /dev/null +++ b/src/HttpApi/HostedServices/ReservedTicketRemoverHostedService.cs @@ -0,0 +1,41 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.TicketGroups + .Commands.RemoveOldReservedTicketGroups; + +namespace cuqmbr.TravelGuide.HttpApi.HostedServices; + +public class ReservedTicketRemoverHostedService : BackgroundService +{ + private Timer _timer = null; + + public ReservedTicketRemoverHostedService( + IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider; + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _timer = new Timer(async (state) => + { + using var scope = ServiceProvider.CreateScope(); + var _mediator = scope.ServiceProvider + .GetRequiredService(); + + await _mediator.Send( + new RemoveOldReservedTicketGroupsCommand() + { + ReservedFor = TimeSpan.FromMinutes(10) + }, cancellationToken); + }, + null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Dispose(); + await base.StopAsync(cancellationToken); + } +} diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs index 472d41e..015f880 100644 --- a/src/HttpApi/Program.cs +++ b/src/HttpApi/Program.cs @@ -6,6 +6,7 @@ using cuqmbr.TravelGuide.Configuration.Configuration; using cuqmbr.TravelGuide.Configuration.Logging; using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.HttpApi.Services; +using cuqmbr.TravelGuide.HttpApi.HostedServices; using cuqmbr.TravelGuide.HttpApi.Middlewares; using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; using System.Net; @@ -33,6 +34,8 @@ services.AddScoped(); services.AddScoped(); services.AddScoped(); +services.AddHostedService(); + services.AddControllers(); diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 2bdff62..8f2afcb 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -13,6 +13,16 @@ "Localization": { "DefaultCultureName": "en-US", "CacheDuration": "00:30:00" + }, + "Infrastructure": { + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } } }, "Identity": { diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json index 2bdff62..8f2afcb 100644 --- a/src/HttpApi/appsettings.json +++ b/src/HttpApi/appsettings.json @@ -13,6 +13,16 @@ "Localization": { "DefaultCultureName": "en-US", "CacheDuration": "00:30:00" + }, + "Infrastructure": { + "PaymentProcessing": { + "CallbackAddressBase": "https://api.travel-guide.cuqmbr.xyz", + "ResultAddressBase": "https://travel-guide.cuqmbr.xyz", + "LiqPay": { + "PublicKey": "sandbox_xxxxxxxxxxxx", + "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } } }, "Identity": { diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index 58c6c3e..2d89b37 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -1084,6 +1084,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json index 22ffc53..5c6200a 100644 --- a/src/Identity/packages.lock.json +++ b/src/Identity/packages.lock.json @@ -520,6 +520,11 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -594,6 +599,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs index 1d4b8a4..338297e 100644 --- a/src/Infrastructure/ConfigurationOptions.cs +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -2,5 +2,23 @@ namespace cuqmbr.TravelGuide.Infrastructure; public sealed class ConfigurationOptions { - public static string SectionName { get; } = "Infrastructure"; + public static string SectionName { get; } = "Application:Infrastructure"; + + public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } +} + +public sealed class PaymentProcessingConfigurationOptions +{ + public string CallbackAddressBase { get; set; } + + public string ResultAddressBase { get; set; } + + public LiqPayConfigurationOptions LiqPay { get; set; } +} + +public sealed class LiqPayConfigurationOptions +{ + public string PublicKey { get; set; } + + public string PrivateKey { get; set; } } diff --git a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs index ee4d9d0..e2bd323 100644 --- a/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs +++ b/src/Infrastructure/Services/ExchangeApiCurrencyConverterService.cs @@ -3,6 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Domain.Enums; using Newtonsoft.Json; +namespace cuqmbr.TravelGuide.Infrastructure.Services; + // https://github.com/fawazahmed0/exchange-api public sealed class ExchangeApiCurrencyConverterService : diff --git a/src/Infrastructure/Services/LiqPayPaymentService.cs b/src/Infrastructure/Services/LiqPayPaymentService.cs new file mode 100644 index 0000000..452305f --- /dev/null +++ b/src/Infrastructure/Services/LiqPayPaymentService.cs @@ -0,0 +1,78 @@ +using System.Dynamic; +using System.Security.Cryptography; +using System.Text; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class LiqPayPaymentService : + cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService +{ + private readonly LiqPayConfigurationOptions _configuration; + private readonly string _callbackAddressBase; + private readonly string _resultAddressBase; + + private readonly IHttpClientFactory _httpClientFactory; + + public LiqPayPaymentService( + IOptions configurationOptions, + IHttpClientFactory httpClientFactory) + { + _configuration = configurationOptions.Value.PaymentProcessing.LiqPay; + _callbackAddressBase = + configurationOptions.Value.PaymentProcessing.CallbackAddressBase; + _resultAddressBase = + configurationOptions.Value.PaymentProcessing.ResultAddressBase; + } + + public Task GetPaymentLinkAsync( + decimal amount, Currency currency, + string orderId, TimeSpan validity, string description, + string resultPath, string callbackPath) + { + dynamic request = new ExpandoObject(); + + request.version = 3; + request.public_key = _configuration.PublicKey; + request.action = "pay"; + request.amount = amount; + request.currency = currency.Name.ToUpper(); + request.description = description; + request.order_id = orderId; + request.expired_date = DateTimeOffset.UtcNow.Add(validity) + .ToString("yyyy-MM-dd HH:mm:ss"); + request.result_url = $"{_resultAddressBase}{resultPath}"; + request.server_url = $"{_callbackAddressBase}{callbackPath}"; + + var requestJsonString = (string)JsonConvert.SerializeObject(request); + + + var requestJsonStringBytes = Encoding.UTF8.GetBytes(requestJsonString); + + var data = Convert.ToBase64String(requestJsonStringBytes); + + var signature = Convert.ToBase64String(SHA1.HashData( + Encoding.UTF8.GetBytes( + _configuration.PrivateKey + + data + + _configuration.PrivateKey))); + + + return Task.FromResult( + "https://www.liqpay.ua/api/3/checkout" + + $"?data={data}&signature={signature}"); + } + + public Task IsValidSignatureAsync(string postData, string postSignature) + { + var signature = Convert.ToBase64String(SHA1.HashData( + Encoding.UTF8.GetBytes( + _configuration.PrivateKey + + postData + + _configuration.PrivateKey))); + + return Task.FromResult(postSignature.Equals(signature)); + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index f67a272..b07e4dd 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -254,6 +254,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 3dc85f8..41f3171 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -47,6 +47,11 @@ public class InMemoryDbContext : DbContext .HaveColumnType("varchar(32)") .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + builder .Properties() .HaveConversion(); diff --git a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs index 9bc2947..10e4a65 100644 --- a/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/TicketGroupConfiguration.cs @@ -14,18 +14,36 @@ public class TicketGroupConfiguration : BaseConfiguration .HasColumnName("passanger_sex") .IsRequired(true); + builder + .Property(tg => tg.Status) + .HasColumnName("status") + .IsRequired(true); + builder .ToTable( "ticket_groups", - tg => tg.HasCheckConstraint( - "ck_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(tg => tg.PassangerSex) + tg => + { + tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.PassangerSex) .Metadata.GetColumnName()}", - $"{builder.Property(g => g.PassangerSex) + $"{builder.Property(g => g.PassangerSex) .Metadata.GetColumnName()} IN ('{String - .Join("', '", Sex.Enumerations - .Values.Select(v => v.Name))}')")); + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')"); + + tg.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(tg => tg.Status) + .Metadata.GetColumnName()}", + $"{builder.Property(g => g.Status) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", TicketStatus.Enumerations + .Values.Select(v => v.Name))}')"); + }); base.Configure(builder); @@ -60,12 +78,6 @@ public class TicketGroupConfiguration : BaseConfiguration .HasColumnType("timestamptz") .IsRequired(true); - builder - .Property(a => a.Returned) - .HasColumnName("returned") - .HasColumnType("boolean") - .IsRequired(true); - builder .Property(a => a.TravelTime) .HasColumnName("travel_time") diff --git a/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs new file mode 100644 index 0000000..6338c55 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs @@ -0,0 +1,1019 @@ +// +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("20250524184743_Add_status_to_Ticket_Group")] + partial class Add_status_to_Ticket_Group + { + /// + 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("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); + + 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')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); + }); + }); + + 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/20250524184743_Add_status_to_Ticket_Group.cs b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs new file mode 100644 index 0000000..d3036f4 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_status_to_Ticket_Group : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "returned", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddColumn( + name: "status", + schema: "application", + table: "ticket_groups", + type: "varchar(32)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddCheckConstraint( + name: "ck_ticket_groups_status", + schema: "application", + table: "ticket_groups", + sql: "status IN ('reserved', 'returned', 'purchased')"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "ck_ticket_groups_status", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.DropColumn( + name: "status", + schema: "application", + table: "ticket_groups"); + + migrationBuilder.AddColumn( + name: "returned", + schema: "application", + table: "ticket_groups", + type: "boolean", + nullable: false, + defaultValue: false); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index fb4e67f..e4f59b1 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -577,9 +577,10 @@ namespace Persistence.PostgreSql.Migrations .HasColumnType("timestamptz") .HasColumnName("purchase_time"); - b.Property("Returned") - .HasColumnType("boolean") - .HasColumnName("returned"); + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("status"); b.Property("TravelTime") .HasColumnType("interval") @@ -594,6 +595,8 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("ticket_groups", "application", t => { t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')"); + + t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')"); }); }); diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index 11e5d63..0f5571c 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -54,6 +54,11 @@ public class PostgreSqlDbContext : DbContext .HaveColumnType("varchar(32)") .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + builder .Properties() .HaveConversion(); diff --git a/src/Persistence/TypeConverters/TicketStatusConverter.cs b/src/Persistence/TypeConverters/TicketStatusConverter.cs new file mode 100644 index 0000000..5220fc8 --- /dev/null +++ b/src/Persistence/TypeConverters/TicketStatusConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class TicketStatusConverter : ValueConverter +{ + public TicketStatusConverter() + : base( + v => v.Name, + v => TicketStatus.FromName(v)) + { } +} diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index 3cb01b7..4833b36 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -266,6 +266,11 @@ "resolved": "9.0.4", "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "Npgsql": { "type": "Transitive", "resolved": "9.0.3", @@ -334,6 +339,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" } diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 4967b9d..117f2e7 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -987,6 +987,7 @@ "MediatR": "[12.4.1, )", "MediatR.Behaviors.Authorization": "[12.2.0, )", "Microsoft.Extensions.Logging": "[9.0.4, )", + "Newtonsoft.Json": "[13.0.3, )", "QuikGraph": "[2.5.0, )", "System.Linq.Dynamic.Core": "[1.6.2, )" }