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..7a082c2
--- /dev/null
+++ b/src/Application/Payments/LiqPay/TicketGroups/Commands/ProcessCallback/ProcessCallbackCommandHandler.cs
@@ -0,0 +1,69 @@
+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.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/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/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..888cb97
--- /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.expire_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, )"
}