add LiqPay integration for ticket purchase
This commit is contained in:
parent
e3dd2dd582
commit
4c8ca2e14f
@ -17,6 +17,7 @@
|
|||||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||||
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" />
|
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="QuikGraph" Version="2.5.0" />
|
<PackageReference Include="QuikGraph" Version="2.5.0" />
|
||||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
|
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
17
src/Application/Common/Authorization/AllowAllRequirement.cs
Normal file
17
src/Application/Common/Authorization/AllowAllRequirement.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using MediatR.Behaviors.Authorization;
|
||||||
|
|
||||||
|
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
|
||||||
|
|
||||||
|
public class AllowAllRequirement : IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
class MustBeAuthenticatedRequirementHandler :
|
||||||
|
IAuthorizationHandler<AllowAllRequirement>
|
||||||
|
{
|
||||||
|
public Task<AuthorizationResult> Handle(
|
||||||
|
AllowAllRequirement request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(AuthorizationResult.Succeed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
// using cuqmbr.TravelGuide.Application.Common.Exceptions;
|
|
||||||
using MediatR.Behaviors.Authorization;
|
using MediatR.Behaviors.Authorization;
|
||||||
|
|
||||||
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
|
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
|
||||||
@ -17,8 +16,6 @@ public class MustBeAuthenticatedRequirement : IAuthorizationRequirement
|
|||||||
if (!request.IsAuthenticated)
|
if (!request.IsAuthenticated)
|
||||||
{
|
{
|
||||||
return Task.FromResult(AuthorizationResult.Fail());
|
return Task.FromResult(AuthorizationResult.Fail());
|
||||||
// TODO: Remove UnAuthorizedException, isn't used
|
|
||||||
// throw new UnAuthorizedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(AuthorizationResult.Succeed());
|
return Task.FromResult(AuthorizationResult.Succeed());
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
using cuqmbr.TravelGuide.Domain.Enums;
|
||||||
|
|
||||||
|
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
|
||||||
|
|
||||||
|
public interface LiqPayPaymentService
|
||||||
|
{
|
||||||
|
Task<string> GetPaymentLinkAsync(
|
||||||
|
decimal amount, Currency currency,
|
||||||
|
string orderId, TimeSpan validity, string description,
|
||||||
|
string resultPath, string callbackPath);
|
||||||
|
|
||||||
|
Task<bool> IsValidSignatureAsync(string postData, string postSignature);
|
||||||
|
}
|
@ -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<PaymentLinkDto>
|
||||||
|
{
|
||||||
|
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<TicketGroupPaymentTicketModel> Tickets { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public string ResultPath { get; set; }
|
||||||
|
}
|
@ -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<GetPaymentLinkCommand>
|
||||||
|
{
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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<GetPaymentLinkCommand, PaymentLinkDto>
|
||||||
|
{
|
||||||
|
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<PaymentLinkDto> 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<ValidationFailure>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
PropertyName = nameof(request.Tickets)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departureRouteAddress.Order > arrivalRouteAddress.Order)
|
||||||
|
{
|
||||||
|
throw new ValidationException(
|
||||||
|
new List<ValidationFailure>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
PropertyName = nameof(request.Tickets)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability of free places.
|
||||||
|
{
|
||||||
|
// Get all tickets for vehicle enrollments requested in ticket group.
|
||||||
|
var vehicleEnrollmentGuids =
|
||||||
|
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
|
||||||
|
|
||||||
|
var 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<ValidationFailure>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
PropertyName = nameof(request.Tickets)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate travel time and cost.
|
||||||
|
|
||||||
|
var ticketsDetails = new List<(short order, DateTimeOffset departureTime,
|
||||||
|
DateTimeOffset arrivalTime, decimal cost, Currency currency)>();
|
||||||
|
|
||||||
|
{
|
||||||
|
var vehicleEnrollmentGuids =
|
||||||
|
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
|
||||||
|
|
||||||
|
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
|
||||||
|
.GetPageAsync(
|
||||||
|
e => vehicleEnrollmentGuids.Contains(e.Guid),
|
||||||
|
1, vehicleEnrollmentGuids.Count(), cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
|
||||||
|
var routeAddressGuids =
|
||||||
|
request.Tickets.Select(t => t.DepartureRouteAddressGuid).Concat(
|
||||||
|
request.Tickets.Select(t => t.ArrivalRouteAddressGuid));
|
||||||
|
|
||||||
|
var routeAddresses = (await _unitOfWork.RouteAddressRepository
|
||||||
|
.GetPageAsync(
|
||||||
|
e =>
|
||||||
|
routeAddressGuids.Contains(e.Guid),
|
||||||
|
1, routeAddressGuids.Count(), cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
|
||||||
|
var vehicleEnrollmentIds = vehicleEnrollments.Select(ve => ve.Id);
|
||||||
|
|
||||||
|
var allRouteAddressDetails = (await _unitOfWork
|
||||||
|
.RouteAddressDetailRepository.GetPageAsync(
|
||||||
|
e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId),
|
||||||
|
e => e.RouteAddress,
|
||||||
|
1, int.MaxValue, cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var t in request.Tickets.OrderBy(t => t.Order))
|
||||||
|
{
|
||||||
|
var ve = vehicleEnrollments.First(
|
||||||
|
e => e.Guid == t.VehicleEnrollmentGuid);
|
||||||
|
|
||||||
|
var departureRouteAddressId = routeAddresses.First(
|
||||||
|
ra => ra.Guid == t.DepartureRouteAddressGuid)
|
||||||
|
.Id;
|
||||||
|
var arrivalRouteAddressId = routeAddresses.First(
|
||||||
|
ra => ra.Guid == t.ArrivalRouteAddressGuid)
|
||||||
|
.Id;
|
||||||
|
|
||||||
|
var verad = allRouteAddressDetails
|
||||||
|
.Where(arad => arad.VehicleEnrollmentId == ve.Id)
|
||||||
|
.OrderBy(rad => rad.RouteAddress.Order)
|
||||||
|
.TakeWhile(rad => rad.Id != arrivalRouteAddressId);
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: This counts departure address stop time which is
|
||||||
|
// not wrong but may be not desired.
|
||||||
|
var timeToDeparture = verad
|
||||||
|
.TakeWhile(rad => rad.Id != departureRouteAddressId)
|
||||||
|
.Aggregate(TimeSpan.Zero, (sum, next) =>
|
||||||
|
sum + next.TimeToNextAddress + next.CurrentAddressStopTime);
|
||||||
|
|
||||||
|
var departureTime = ve.DepartureTime.Add(timeToDeparture);
|
||||||
|
|
||||||
|
|
||||||
|
var timeToArrival = verad.Aggregate(TimeSpan.Zero, (sum, next) =>
|
||||||
|
sum + next.TimeToNextAddress + next.CurrentAddressStopTime);
|
||||||
|
|
||||||
|
var arrivalTime = ve.DepartureTime.Add(timeToArrival);
|
||||||
|
|
||||||
|
|
||||||
|
var costToDeparture = verad
|
||||||
|
.TakeWhile(rad => rad.Id != departureRouteAddressId)
|
||||||
|
.Aggregate((decimal)0, (sum, next) =>
|
||||||
|
sum + next.CostToNextAddress);
|
||||||
|
|
||||||
|
var costToArrival = verad
|
||||||
|
.Aggregate((decimal)0, (sum, next) =>
|
||||||
|
sum + next.CostToNextAddress);
|
||||||
|
|
||||||
|
var cost = costToArrival - costToDeparture;
|
||||||
|
|
||||||
|
|
||||||
|
ticketsDetails.Add(
|
||||||
|
(t.Order, departureTime, arrivalTime, cost, ve.Currency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether there are overlaps in ticket departure/arrival times.
|
||||||
|
{
|
||||||
|
for (int i = 1; i < ticketsDetails.Count; i++)
|
||||||
|
{
|
||||||
|
var previousTd = ticketsDetails[i - 1];
|
||||||
|
var currentTd = ticketsDetails[i];
|
||||||
|
|
||||||
|
if (previousTd.arrivalTime >= currentTd.departureTime)
|
||||||
|
{
|
||||||
|
throw new ValidationException(
|
||||||
|
new List<ValidationFailure>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
PropertyName = nameof(request.Tickets)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create entity and insert into a datastore.
|
||||||
|
|
||||||
|
{
|
||||||
|
var 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<GetPaymentLinkCommand>
|
||||||
|
{
|
||||||
|
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"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
@ -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<ProcessCallbackCommand>
|
||||||
|
{
|
||||||
|
public override void BuildPolicy(ProcessCallbackCommand request)
|
||||||
|
{
|
||||||
|
UseRequirement(new AllowAllRequirement());
|
||||||
|
}
|
||||||
|
}
|
@ -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<ProcessCallbackCommand>
|
||||||
|
{
|
||||||
|
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<dynamic>(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();
|
||||||
|
}
|
||||||
|
}
|
@ -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<ProcessCallbackCommand>
|
||||||
|
{
|
||||||
|
public ProcessCallbackCommandValidator(
|
||||||
|
IStringLocalizer localizer,
|
||||||
|
SessionCultureService cultureService)
|
||||||
|
{
|
||||||
|
RuleFor(v => v.Data)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage(localizer["FluentValidation.NotEmpty"]);
|
||||||
|
|
||||||
|
RuleFor(v => v.Signature)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage(localizer["FluentValidation.NotEmpty"]);
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
@ -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<TicketPaymentViewModel> Tickets { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public string ResultPath { get; set; }
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
6
src/Application/Payments/PaymentLinkDto.cs
Normal file
6
src/Application/Payments/PaymentLinkDto.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace cuqmbr.TravelGuide.Application.Payments;
|
||||||
|
|
||||||
|
public sealed class PaymentLinkDto
|
||||||
|
{
|
||||||
|
public string PaymentLink { get; set; }
|
||||||
|
}
|
@ -58,5 +58,8 @@
|
|||||||
"Title": "One or more internal server errors occurred.",
|
"Title": "One or more internal server errors occurred.",
|
||||||
"Detail": "Report this error to service's support team."
|
"Detail": "Report this error to service's support team."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"PaymentProcessing": {
|
||||||
|
"TicketPaymentDescription": "Ticket purchase."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ public record AddTicketGroupCommand : IRequest<TicketGroupDto>
|
|||||||
|
|
||||||
public DateTimeOffset PurchaseTime { get; set; }
|
public DateTimeOffset PurchaseTime { get; set; }
|
||||||
|
|
||||||
public bool Returned { get; set; }
|
public TicketStatus Status { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public ICollection<TicketModel> Tickets { get; set; }
|
public ICollection<TicketModel> Tickets { get; set; }
|
||||||
|
@ -128,11 +128,17 @@ public class AddTicketGroupCommandHandler :
|
|||||||
var vehicleEnrollmentGuids =
|
var vehicleEnrollmentGuids =
|
||||||
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
|
request.Tickets.Select(t => t.VehicleEnrollmentGuid);
|
||||||
|
|
||||||
|
var unavailableTicketStatuses = new TicketStatus[]
|
||||||
|
{
|
||||||
|
TicketStatus.Reserved,
|
||||||
|
TicketStatus.Purchased
|
||||||
|
};
|
||||||
|
|
||||||
var ticketGroupTickets = (await _unitOfWork.TicketRepository
|
var ticketGroupTickets = (await _unitOfWork.TicketRepository
|
||||||
.GetPageAsync(
|
.GetPageAsync(
|
||||||
e =>
|
e =>
|
||||||
vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) &&
|
vehicleEnrollmentGuids.Contains(e.VehicleEnrollment.Guid) &&
|
||||||
e.TicketGroup.Returned == false,
|
unavailableTicketStatuses.Contains(e.TicketGroup.Status),
|
||||||
1, int.MaxValue, cancellationToken))
|
1, int.MaxValue, cancellationToken))
|
||||||
.Items;
|
.Items;
|
||||||
|
|
||||||
@ -431,7 +437,7 @@ public class AddTicketGroupCommandHandler :
|
|||||||
PassangerSex = request.PassangerSex,
|
PassangerSex = request.PassangerSex,
|
||||||
PassangerBirthDate = request.PassangerBirthDate,
|
PassangerBirthDate = request.PassangerBirthDate,
|
||||||
PurchaseTime = request.PurchaseTime,
|
PurchaseTime = request.PurchaseTime,
|
||||||
Returned = request.Returned,
|
Status = request.Status,
|
||||||
TravelTime = travelTime,
|
TravelTime = travelTime,
|
||||||
Tickets = request.Tickets.Select(
|
Tickets = request.Tickets.Select(
|
||||||
t =>
|
t =>
|
||||||
|
@ -67,6 +67,15 @@ public class AddTicketGroupCommandValidator : AbstractValidator<AddTicketGroupCo
|
|||||||
localizer["FluentValidation.GreaterThanOrEqualTo"],
|
localizer["FluentValidation.GreaterThanOrEqualTo"],
|
||||||
DateTimeOffset.UtcNow));
|
DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
RuleFor(tg => 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)
|
RuleFor(tg => tg.Tickets)
|
||||||
.IsUnique(t => t.VehicleEnrollmentGuid)
|
.IsUnique(t => t.VehicleEnrollmentGuid)
|
||||||
.WithMessage(localizer["FluentValidation.IsUnique"]);
|
.WithMessage(localizer["FluentValidation.IsUnique"]);
|
||||||
|
@ -14,7 +14,7 @@ public sealed class AddTicketGroupViewModel
|
|||||||
|
|
||||||
public DateTimeOffset PurchaseTime { get; set; }
|
public DateTimeOffset PurchaseTime { get; set; }
|
||||||
|
|
||||||
public bool Returned { get; set; }
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public ICollection<TicketViewModel> Tickets { get; set; }
|
public ICollection<TicketViewModel> Tickets { get; set; }
|
||||||
|
@ -59,6 +59,12 @@
|
|||||||
"Microsoft.Extensions.Options": "9.0.4"
|
"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": {
|
"QuikGraph": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.5.0, )",
|
"requested": "[2.5.0, )",
|
||||||
|
@ -4,6 +4,8 @@ using PersistenceConfigurationOptions =
|
|||||||
cuqmbr.TravelGuide.Persistence.ConfigurationOptions;
|
cuqmbr.TravelGuide.Persistence.ConfigurationOptions;
|
||||||
using ApplicationConfigurationOptions =
|
using ApplicationConfigurationOptions =
|
||||||
cuqmbr.TravelGuide.Application.ConfigurationOptions;
|
cuqmbr.TravelGuide.Application.ConfigurationOptions;
|
||||||
|
using InfrastructureConfigurationOptions =
|
||||||
|
cuqmbr.TravelGuide.Infrastructure.ConfigurationOptions;
|
||||||
using IdentityConfigurationOptions =
|
using IdentityConfigurationOptions =
|
||||||
cuqmbr.TravelGuide.Identity.ConfigurationOptions;
|
cuqmbr.TravelGuide.Identity.ConfigurationOptions;
|
||||||
|
|
||||||
@ -33,6 +35,10 @@ public static class Configuration
|
|||||||
configuration.GetSection(
|
configuration.GetSection(
|
||||||
ApplicationConfigurationOptions.SectionName));
|
ApplicationConfigurationOptions.SectionName));
|
||||||
|
|
||||||
|
services.AddOptions<InfrastructureConfigurationOptions>().Bind(
|
||||||
|
configuration.GetSection(
|
||||||
|
InfrastructureConfigurationOptions.SectionName));
|
||||||
|
|
||||||
services.AddOptions<IdentityConfigurationOptions>().Bind(
|
services.AddOptions<IdentityConfigurationOptions>().Bind(
|
||||||
configuration.GetSection(
|
configuration.GetSection(
|
||||||
IdentityConfigurationOptions.SectionName));
|
IdentityConfigurationOptions.SectionName));
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
|
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
|
||||||
|
using cuqmbr.TravelGuide.Infrastructure.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace cuqmbr.TravelGuide.Configuration.Infrastructure;
|
namespace cuqmbr.TravelGuide.Configuration.Infrastructure;
|
||||||
@ -14,7 +15,10 @@ public static class Configuration
|
|||||||
services
|
services
|
||||||
.AddScoped<
|
.AddScoped<
|
||||||
CurrencyConverterService,
|
CurrencyConverterService,
|
||||||
ExchangeApiCurrencyConverterService>();
|
ExchangeApiCurrencyConverterService>()
|
||||||
|
.AddScoped<
|
||||||
|
cuqmbr.TravelGuide.Application.Common.Interfaces.Services.LiqPayPaymentService,
|
||||||
|
cuqmbr.TravelGuide.Infrastructure.Services.LiqPayPaymentService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
@ -843,6 +843,7 @@
|
|||||||
"MediatR": "[12.4.1, )",
|
"MediatR": "[12.4.1, )",
|
||||||
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
||||||
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )",
|
||||||
"QuikGraph": "[2.5.0, )",
|
"QuikGraph": "[2.5.0, )",
|
||||||
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ public sealed class TicketGroup : EntityBase
|
|||||||
|
|
||||||
public DateTimeOffset PurchaseTime { get; set; }
|
public DateTimeOffset PurchaseTime { get; set; }
|
||||||
|
|
||||||
public bool Returned { get; set; }
|
public TicketStatus Status { get; set; }
|
||||||
|
|
||||||
public TimeSpan TravelTime { get; set; }
|
public TimeSpan TravelTime { get; set; }
|
||||||
|
|
||||||
|
25
src/Domain/Enums/TicketStatus.cs
Normal file
25
src/Domain/Enums/TicketStatus.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace cuqmbr.TravelGuide.Domain.Enums;
|
||||||
|
|
||||||
|
public abstract class TicketStatus : Enumeration<TicketStatus>
|
||||||
|
{
|
||||||
|
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") { }
|
||||||
|
}
|
||||||
|
}
|
92
src/HttpApi/Controllers/PaymentController.cs
Normal file
92
src/HttpApi/Controllers/PaymentController.cs
Normal file
@ -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<ActionResult<PaymentLinkDto>> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using cuqmbr.TravelGuide.Application.Common.Models;
|
|
||||||
using cuqmbr.TravelGuide.Application.Common.ViewModels;
|
|
||||||
using cuqmbr.TravelGuide.Domain.Enums;
|
using cuqmbr.TravelGuide.Domain.Enums;
|
||||||
using cuqmbr.TravelGuide.Application.TicketGroups;
|
using cuqmbr.TravelGuide.Application.TicketGroups;
|
||||||
using cuqmbr.TravelGuide.Application.TicketGroups.Commands.AddTicketGroup;
|
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.ViewModels;
|
||||||
using cuqmbr.TravelGuide.Application.TicketGroups.Models;
|
using cuqmbr.TravelGuide.Application.TicketGroups.Models;
|
||||||
|
|
||||||
@ -56,7 +50,7 @@ public class TicketGroupsController : ControllerBase
|
|||||||
PassangerSex = Sex.FromName(viewModel.PassangerSex),
|
PassangerSex = Sex.FromName(viewModel.PassangerSex),
|
||||||
PassangerBirthDate = viewModel.PassangerBirthDate,
|
PassangerBirthDate = viewModel.PassangerBirthDate,
|
||||||
PurchaseTime = viewModel.PurchaseTime,
|
PurchaseTime = viewModel.PurchaseTime,
|
||||||
Returned = viewModel.Returned,
|
Status = TicketStatus.FromName(viewModel.Status),
|
||||||
Tickets = viewModel.Tickets.Select(e =>
|
Tickets = viewModel.Tickets.Select(e =>
|
||||||
new TicketModel()
|
new TicketModel()
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,16 @@
|
|||||||
"Localization": {
|
"Localization": {
|
||||||
"DefaultCultureName": "en-US",
|
"DefaultCultureName": "en-US",
|
||||||
"CacheDuration": "00:30:00"
|
"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": {
|
"Identity": {
|
||||||
|
@ -13,6 +13,16 @@
|
|||||||
"Localization": {
|
"Localization": {
|
||||||
"DefaultCultureName": "en-US",
|
"DefaultCultureName": "en-US",
|
||||||
"CacheDuration": "00:30:00"
|
"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": {
|
"Identity": {
|
||||||
|
@ -1084,6 +1084,7 @@
|
|||||||
"MediatR": "[12.4.1, )",
|
"MediatR": "[12.4.1, )",
|
||||||
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
||||||
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )",
|
||||||
"QuikGraph": "[2.5.0, )",
|
"QuikGraph": "[2.5.0, )",
|
||||||
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
||||||
}
|
}
|
||||||
|
@ -520,6 +520,11 @@
|
|||||||
"System.Security.Principal.Windows": "4.5.0"
|
"System.Security.Principal.Windows": "4.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||||
|
},
|
||||||
"Npgsql": {
|
"Npgsql": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "9.0.3",
|
"resolved": "9.0.3",
|
||||||
@ -594,6 +599,7 @@
|
|||||||
"MediatR": "[12.4.1, )",
|
"MediatR": "[12.4.1, )",
|
||||||
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
||||||
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )",
|
||||||
"QuikGraph": "[2.5.0, )",
|
"QuikGraph": "[2.5.0, )",
|
||||||
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,23 @@ namespace cuqmbr.TravelGuide.Infrastructure;
|
|||||||
|
|
||||||
public sealed class ConfigurationOptions
|
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; }
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
|
|||||||
using cuqmbr.TravelGuide.Domain.Enums;
|
using cuqmbr.TravelGuide.Domain.Enums;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace cuqmbr.TravelGuide.Infrastructure.Services;
|
||||||
|
|
||||||
// https://github.com/fawazahmed0/exchange-api
|
// https://github.com/fawazahmed0/exchange-api
|
||||||
|
|
||||||
public sealed class ExchangeApiCurrencyConverterService :
|
public sealed class ExchangeApiCurrencyConverterService :
|
||||||
|
78
src/Infrastructure/Services/LiqPayPaymentService.cs
Normal file
78
src/Infrastructure/Services/LiqPayPaymentService.cs
Normal file
@ -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> configurationOptions,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_configuration = configurationOptions.Value.PaymentProcessing.LiqPay;
|
||||||
|
_callbackAddressBase =
|
||||||
|
configurationOptions.Value.PaymentProcessing.CallbackAddressBase;
|
||||||
|
_resultAddressBase =
|
||||||
|
configurationOptions.Value.PaymentProcessing.ResultAddressBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> 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<bool> 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));
|
||||||
|
}
|
||||||
|
}
|
@ -254,6 +254,7 @@
|
|||||||
"MediatR": "[12.4.1, )",
|
"MediatR": "[12.4.1, )",
|
||||||
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
||||||
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )",
|
||||||
"QuikGraph": "[2.5.0, )",
|
"QuikGraph": "[2.5.0, )",
|
||||||
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,11 @@ public class InMemoryDbContext : DbContext
|
|||||||
.HaveColumnType("varchar(32)")
|
.HaveColumnType("varchar(32)")
|
||||||
.HaveConversion<SexConverter>();
|
.HaveConversion<SexConverter>();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Properties<TicketStatus>()
|
||||||
|
.HaveColumnType("varchar(32)")
|
||||||
|
.HaveConversion<TicketStatusConverter>();
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Properties<DateTimeOffset>()
|
.Properties<DateTimeOffset>()
|
||||||
.HaveConversion<DateTimeOffsetConverter>();
|
.HaveConversion<DateTimeOffsetConverter>();
|
||||||
|
@ -14,18 +14,36 @@ public class TicketGroupConfiguration : BaseConfiguration<TicketGroup>
|
|||||||
.HasColumnName("passanger_sex")
|
.HasColumnName("passanger_sex")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Property(tg => tg.Status)
|
||||||
|
.HasColumnName("status")
|
||||||
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.ToTable(
|
.ToTable(
|
||||||
"ticket_groups",
|
"ticket_groups",
|
||||||
tg => tg.HasCheckConstraint(
|
tg =>
|
||||||
"ck_" +
|
{
|
||||||
$"{builder.Metadata.GetTableName()}_" +
|
tg.HasCheckConstraint(
|
||||||
$"{builder.Property(tg => tg.PassangerSex)
|
"ck_" +
|
||||||
|
$"{builder.Metadata.GetTableName()}_" +
|
||||||
|
$"{builder.Property(tg => tg.PassangerSex)
|
||||||
.Metadata.GetColumnName()}",
|
.Metadata.GetColumnName()}",
|
||||||
$"{builder.Property(g => g.PassangerSex)
|
$"{builder.Property(g => g.PassangerSex)
|
||||||
.Metadata.GetColumnName()} IN ('{String
|
.Metadata.GetColumnName()} IN ('{String
|
||||||
.Join("', '", Sex.Enumerations
|
.Join("', '", Sex.Enumerations
|
||||||
.Values.Select(v => v.Name))}')"));
|
.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);
|
base.Configure(builder);
|
||||||
|
|
||||||
@ -60,12 +78,6 @@ public class TicketGroupConfiguration : BaseConfiguration<TicketGroup>
|
|||||||
.HasColumnType("timestamptz")
|
.HasColumnType("timestamptz")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
|
||||||
.Property(a => a.Returned)
|
|
||||||
.HasColumnName("returned")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.IsRequired(true);
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.TravelTime)
|
.Property(a => a.TravelTime)
|
||||||
.HasColumnName("travel_time")
|
.HasColumnName("travel_time")
|
||||||
|
1019
src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs
generated
Normal file
1019
src/Persistence/PostgreSql/Migrations/20250524184743_Add_status_to_Ticket_Group.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Persistence.PostgreSql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_status_to_Ticket_Group : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "returned",
|
||||||
|
schema: "application",
|
||||||
|
table: "ticket_groups");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
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')");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<bool>(
|
||||||
|
name: "returned",
|
||||||
|
schema: "application",
|
||||||
|
table: "ticket_groups",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -577,9 +577,10 @@ namespace Persistence.PostgreSql.Migrations
|
|||||||
.HasColumnType("timestamptz")
|
.HasColumnType("timestamptz")
|
||||||
.HasColumnName("purchase_time");
|
.HasColumnName("purchase_time");
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
b.Property<string>("Status")
|
||||||
.HasColumnType("boolean")
|
.IsRequired()
|
||||||
.HasColumnName("returned");
|
.HasColumnType("varchar(32)")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
b.Property<TimeSpan>("TravelTime")
|
b.Property<TimeSpan>("TravelTime")
|
||||||
.HasColumnType("interval")
|
.HasColumnType("interval")
|
||||||
@ -594,6 +595,8 @@ namespace Persistence.PostgreSql.Migrations
|
|||||||
b.ToTable("ticket_groups", "application", t =>
|
b.ToTable("ticket_groups", "application", t =>
|
||||||
{
|
{
|
||||||
t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')");
|
t.HasCheckConstraint("ck_ticket_groups_passanger_sex", "passanger_sex IN ('male', 'female')");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("ck_ticket_groups_status", "status IN ('reserved', 'returned', 'purchased')");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,6 +54,11 @@ public class PostgreSqlDbContext : DbContext
|
|||||||
.HaveColumnType("varchar(32)")
|
.HaveColumnType("varchar(32)")
|
||||||
.HaveConversion<SexConverter>();
|
.HaveConversion<SexConverter>();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Properties<TicketStatus>()
|
||||||
|
.HaveColumnType("varchar(32)")
|
||||||
|
.HaveConversion<TicketStatusConverter>();
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Properties<DateTimeOffset>()
|
.Properties<DateTimeOffset>()
|
||||||
.HaveConversion<DateTimeOffsetConverter>();
|
.HaveConversion<DateTimeOffsetConverter>();
|
||||||
|
13
src/Persistence/TypeConverters/TicketStatusConverter.cs
Normal file
13
src/Persistence/TypeConverters/TicketStatusConverter.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using cuqmbr.TravelGuide.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace cuqmbr.TravelGuide.Persistence.TypeConverters;
|
||||||
|
|
||||||
|
public class TicketStatusConverter : ValueConverter<TicketStatus, string>
|
||||||
|
{
|
||||||
|
public TicketStatusConverter()
|
||||||
|
: base(
|
||||||
|
v => v.Name,
|
||||||
|
v => TicketStatus.FromName(v))
|
||||||
|
{ }
|
||||||
|
}
|
@ -266,6 +266,11 @@
|
|||||||
"resolved": "9.0.4",
|
"resolved": "9.0.4",
|
||||||
"contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
|
"contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
|
||||||
},
|
},
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||||
|
},
|
||||||
"Npgsql": {
|
"Npgsql": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "9.0.3",
|
"resolved": "9.0.3",
|
||||||
@ -334,6 +339,7 @@
|
|||||||
"MediatR": "[12.4.1, )",
|
"MediatR": "[12.4.1, )",
|
||||||
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
||||||
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )",
|
||||||
"QuikGraph": "[2.5.0, )",
|
"QuikGraph": "[2.5.0, )",
|
||||||
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
||||||
}
|
}
|
||||||
|
@ -987,6 +987,7 @@
|
|||||||
"MediatR": "[12.4.1, )",
|
"MediatR": "[12.4.1, )",
|
||||||
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
"MediatR.Behaviors.Authorization": "[12.2.0, )",
|
||||||
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
"Microsoft.Extensions.Logging": "[9.0.4, )",
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )",
|
||||||
"QuikGraph": "[2.5.0, )",
|
"QuikGraph": "[2.5.0, )",
|
||||||
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
"System.Linq.Dynamic.Core": "[1.6.2, )"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user