add payment email notifications
This commit is contained in:
parent
68a9e06eeb
commit
6a9504d6ff
@ -17,6 +17,8 @@ public record GetPaymentLinkCommand : IRequest<PaymentLinkDto>
|
|||||||
|
|
||||||
public DateOnly PassangerBirthDate { get; set; }
|
public DateOnly PassangerBirthDate { get; set; }
|
||||||
|
|
||||||
|
public string? PassangerEmail { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public ICollection<TicketGroupPaymentTicketModel> Tickets { get; set; }
|
public ICollection<TicketGroupPaymentTicketModel> Tickets { get; set; }
|
||||||
|
|
||||||
|
@ -21,16 +21,27 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
|
|
||||||
private readonly IStringLocalizer _localizer;
|
private readonly IStringLocalizer _localizer;
|
||||||
|
|
||||||
|
private readonly EmailSenderService _emailSender;
|
||||||
|
|
||||||
|
private readonly SessionTimeZoneService _sessionTimeZoneService;
|
||||||
|
private readonly SessionCultureService _sessionCultureService;
|
||||||
|
|
||||||
public GetPaymentLinkCommandHandler(
|
public GetPaymentLinkCommandHandler(
|
||||||
UnitOfWork unitOfWork,
|
UnitOfWork unitOfWork,
|
||||||
CurrencyConverterService currencyConverterService,
|
CurrencyConverterService currencyConverterService,
|
||||||
LiqPayPaymentService liqPayPaymentService,
|
LiqPayPaymentService liqPayPaymentService,
|
||||||
IStringLocalizer localizer)
|
IStringLocalizer localizer,
|
||||||
|
EmailSenderService emailSender,
|
||||||
|
SessionTimeZoneService SessionTimeZoneService,
|
||||||
|
SessionCultureService sessionCultureService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_currencyConverterService = currencyConverterService;
|
_currencyConverterService = currencyConverterService;
|
||||||
_liqPayPaymentService = liqPayPaymentService;
|
_liqPayPaymentService = liqPayPaymentService;
|
||||||
_localizer = localizer;
|
_localizer = localizer;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
_sessionTimeZoneService = SessionTimeZoneService;
|
||||||
|
_sessionCultureService = sessionCultureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaymentLinkDto> Handle(
|
public async Task<PaymentLinkDto> Handle(
|
||||||
@ -336,7 +347,7 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
|
|
||||||
|
|
||||||
var costToDeparture = verad
|
var costToDeparture = verad
|
||||||
.TakeWhile(rad => rad.Id != departureRouteAddressId)
|
.TakeWhile(rad => rad.RouteAddressId != departureRouteAddressId)
|
||||||
.Aggregate((decimal)0, (sum, next) =>
|
.Aggregate((decimal)0, (sum, next) =>
|
||||||
sum + next.CostToNextAddress);
|
sum + next.CostToNextAddress);
|
||||||
|
|
||||||
@ -412,6 +423,7 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
PurchaseTime = DateTimeOffset.UtcNow,
|
PurchaseTime = DateTimeOffset.UtcNow,
|
||||||
Status = TicketStatus.Reserved,
|
Status = TicketStatus.Reserved,
|
||||||
TravelTime = travelTime,
|
TravelTime = travelTime,
|
||||||
|
PassangerEmail = request.PassangerEmail,
|
||||||
Tickets = request.Tickets.Select(
|
Tickets = request.Tickets.Select(
|
||||||
t =>
|
t =>
|
||||||
{
|
{
|
||||||
@ -428,12 +440,6 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
var detail = ticketsDetails
|
var detail = ticketsDetails
|
||||||
.SingleOrDefault(td => td.order == t.Order);
|
.SingleOrDefault(td => td.order == t.Order);
|
||||||
|
|
||||||
var currency = Currency.UAH;
|
|
||||||
var cost = _currencyConverterService
|
|
||||||
.ConvertAsync(
|
|
||||||
detail.cost, detail.currency, currency,
|
|
||||||
cancellationToken).Result;
|
|
||||||
|
|
||||||
return new Ticket()
|
return new Ticket()
|
||||||
{
|
{
|
||||||
DepartureRouteAddressId = departureRouteAddress.Id,
|
DepartureRouteAddressId = departureRouteAddress.Id,
|
||||||
@ -441,8 +447,8 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
ArrivalRouteAddressId = arrivalRouteAddress.Id,
|
ArrivalRouteAddressId = arrivalRouteAddress.Id,
|
||||||
ArrivalRouteAddress = arrivalRouteAddress,
|
ArrivalRouteAddress = arrivalRouteAddress,
|
||||||
Order = t.Order,
|
Order = t.Order,
|
||||||
Cost = cost,
|
Cost = detail.cost,
|
||||||
Currency = currency,
|
Currency = detail.currency,
|
||||||
VehicleEnrollmentId = ve.Id
|
VehicleEnrollmentId = ve.Id
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -456,7 +462,11 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
_unitOfWork.Dispose();
|
_unitOfWork.Dispose();
|
||||||
|
|
||||||
|
|
||||||
var amount = entity.Tickets.Sum(e => e.Cost);
|
var amount = entity.Tickets.Sum(e =>
|
||||||
|
_currencyConverterService
|
||||||
|
.ConvertAsync(
|
||||||
|
e.Cost, e.Currency, Currency.UAH,
|
||||||
|
cancellationToken).Result);
|
||||||
var guid = entity.Guid;
|
var guid = entity.Guid;
|
||||||
var validity = TimeSpan.FromMinutes(10);
|
var validity = TimeSpan.FromMinutes(10);
|
||||||
var resultPath = request.ResultPath;
|
var resultPath = request.ResultPath;
|
||||||
@ -465,9 +475,31 @@ public class GetPaymentLinkCommandHandler :
|
|||||||
var paymentLink = _liqPayPaymentService
|
var paymentLink = _liqPayPaymentService
|
||||||
.GetPaymentLink(
|
.GetPaymentLink(
|
||||||
amount, Currency.UAH, guid.ToString(), validity,
|
amount, Currency.UAH, guid.ToString(), validity,
|
||||||
_localizer["PaymentProcessing.TicketPaymentDescription"],
|
_localizer["PaymentProcessing.Ticket.PaymentDescription"],
|
||||||
resultPath, callbackPath);
|
resultPath, callbackPath);
|
||||||
|
|
||||||
|
if (request.PassangerEmail != null)
|
||||||
|
{
|
||||||
|
var validUntil = DateTimeOffset.UtcNow
|
||||||
|
.Add(validity)
|
||||||
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset);
|
||||||
|
|
||||||
|
var subject =
|
||||||
|
_localizer["PaymentProcessing.Ticket" +
|
||||||
|
".Email.PaymentCreated.Subject"];
|
||||||
|
|
||||||
|
var body = String.Format(
|
||||||
|
_sessionCultureService.Culture,
|
||||||
|
_localizer["PaymentProcessing.Ticket" +
|
||||||
|
".Email.PaymentCreated.Body"],
|
||||||
|
Currency.UAH.Round(amount), Currency.UAH.Name,
|
||||||
|
validUntil, paymentLink);
|
||||||
|
|
||||||
|
await _emailSender.SendAsync(
|
||||||
|
new[] { request.PassangerEmail }, subject,
|
||||||
|
body, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
return new PaymentLinkDto() { PaymentLink = paymentLink };
|
return new PaymentLinkDto() { PaymentLink = paymentLink };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,15 @@ public class GetPaymentLinkCommandValidator :
|
|||||||
cultureService.Culture,
|
cultureService.Culture,
|
||||||
localizer["FluentValidation.GreaterThanOrEqualTo"],
|
localizer["FluentValidation.GreaterThanOrEqualTo"],
|
||||||
DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))));
|
DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))));
|
||||||
|
|
||||||
|
When(tg => tg.PassangerEmail != null, () =>
|
||||||
|
{
|
||||||
|
RuleFor(v => v.PassangerEmail)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage(localizer["FluentValidation.NotEmpty"])
|
||||||
|
.IsEmail()
|
||||||
|
.WithMessage(localizer["FluentValidation.IsEmail"]);
|
||||||
|
});
|
||||||
|
|
||||||
RuleFor(tg => tg.Tickets)
|
RuleFor(tg => tg.Tickets)
|
||||||
.IsUnique(t => t.VehicleEnrollmentGuid)
|
.IsUnique(t => t.VehicleEnrollmentGuid)
|
||||||
|
@ -5,6 +5,8 @@ using cuqmbr.TravelGuide.Application.Common.Exceptions;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using cuqmbr.TravelGuide.Domain.Enums;
|
using cuqmbr.TravelGuide.Domain.Enums;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using cuqmbr.TravelGuide.Domain.Entities;
|
||||||
|
|
||||||
namespace cuqmbr.TravelGuide.Application.Payments.LiqPay
|
namespace cuqmbr.TravelGuide.Application.Payments.LiqPay
|
||||||
.TicketGroups.Commands.ProcessCallback;
|
.TicketGroups.Commands.ProcessCallback;
|
||||||
@ -13,20 +15,31 @@ public class ProcessCallbackCommandHandler :
|
|||||||
IRequestHandler<ProcessCallbackCommand>
|
IRequestHandler<ProcessCallbackCommand>
|
||||||
{
|
{
|
||||||
private readonly UnitOfWork _unitOfWork;
|
private readonly UnitOfWork _unitOfWork;
|
||||||
|
|
||||||
private readonly LiqPayPaymentService _liqPayPaymentService;
|
private readonly LiqPayPaymentService _liqPayPaymentService;
|
||||||
|
|
||||||
|
private readonly IStringLocalizer _localizer;
|
||||||
|
|
||||||
|
private readonly EmailSenderService _emailSender;
|
||||||
|
|
||||||
public ProcessCallbackCommandHandler(
|
public ProcessCallbackCommandHandler(
|
||||||
UnitOfWork unitOfWork,
|
UnitOfWork unitOfWork,
|
||||||
LiqPayPaymentService liqPayPaymentService)
|
LiqPayPaymentService liqPayPaymentService,
|
||||||
|
IStringLocalizer localizer,
|
||||||
|
EmailSenderService emailSender)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_liqPayPaymentService = liqPayPaymentService;
|
_liqPayPaymentService = liqPayPaymentService;
|
||||||
|
_localizer = localizer;
|
||||||
|
_emailSender = emailSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(
|
public async Task Handle(
|
||||||
ProcessCallbackCommand request,
|
ProcessCallbackCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Validate signature.
|
||||||
|
|
||||||
var isSignatureValid = _liqPayPaymentService
|
var isSignatureValid = _liqPayPaymentService
|
||||||
.IsValidSignature(request.Data, request.Signature);
|
.IsValidSignature(request.Data, request.Signature);
|
||||||
|
|
||||||
@ -35,6 +48,9 @@ public class ProcessCallbackCommandHandler :
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parse request data.
|
||||||
|
|
||||||
var dataBytes = Convert.FromBase64String(request.Data);
|
var dataBytes = Convert.FromBase64String(request.Data);
|
||||||
var dataJson = Encoding.UTF8.GetString(dataBytes);
|
var dataJson = Encoding.UTF8.GetString(dataBytes);
|
||||||
|
|
||||||
@ -42,9 +58,11 @@ public class ProcessCallbackCommandHandler :
|
|||||||
|
|
||||||
string status = data.status;
|
string status = data.status;
|
||||||
|
|
||||||
|
|
||||||
var ticketGroupGuid = Guid.Parse((string)data.order_id);
|
var ticketGroupGuid = Guid.Parse((string)data.order_id);
|
||||||
var ticketGroup = await _unitOfWork.TicketGroupRepository
|
var ticketGroup = await _unitOfWork.TicketGroupRepository
|
||||||
.GetOneAsync(e => e.Guid == ticketGroupGuid, cancellationToken);
|
.GetOneAsync(e => e.Guid == ticketGroupGuid,
|
||||||
|
e => e.Tickets, cancellationToken);
|
||||||
|
|
||||||
if (ticketGroup == null ||
|
if (ticketGroup == null ||
|
||||||
ticketGroup.Status == TicketStatus.Purchased)
|
ticketGroup.Status == TicketStatus.Purchased)
|
||||||
@ -52,6 +70,9 @@ public class ProcessCallbackCommandHandler :
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Process callback status
|
||||||
|
|
||||||
if (status.Equals("error") || status.Equals("failure"))
|
if (status.Equals("error") || status.Equals("failure"))
|
||||||
{
|
{
|
||||||
await _unitOfWork.TicketGroupRepository
|
await _unitOfWork.TicketGroupRepository
|
||||||
@ -59,12 +80,228 @@ public class ProcessCallbackCommandHandler :
|
|||||||
}
|
}
|
||||||
else if (status.Equals("success"))
|
else if (status.Equals("success"))
|
||||||
{
|
{
|
||||||
|
// Update ticket status
|
||||||
|
|
||||||
ticketGroup.Status = TicketStatus.Purchased;
|
ticketGroup.Status = TicketStatus.Purchased;
|
||||||
await _unitOfWork.TicketGroupRepository
|
await _unitOfWork.TicketGroupRepository
|
||||||
.UpdateOneAsync(ticketGroup, cancellationToken);
|
.UpdateOneAsync(ticketGroup, cancellationToken);
|
||||||
|
|
||||||
|
|
||||||
|
// Hydrate ticket group
|
||||||
|
|
||||||
|
var vehicleEnrollmentIds =
|
||||||
|
ticketGroup.Tickets.Select(t => t.VehicleEnrollmentId);
|
||||||
|
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
|
||||||
|
.GetPageAsync(
|
||||||
|
ve => vehicleEnrollmentIds.Contains(ve.Id),
|
||||||
|
ve => ve.Route.RouteAddresses,
|
||||||
|
1, vehicleEnrollmentIds.Count(), cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
var routeAddressIds = vehicleEnrollments
|
||||||
|
.SelectMany(ve => ve.Route.RouteAddresses)
|
||||||
|
.Select(ra => ra.Id);
|
||||||
|
var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository
|
||||||
|
.GetPageAsync(
|
||||||
|
rad => routeAddressIds.Contains(rad.RouteAddressId),
|
||||||
|
1, routeAddressIds.Count(), cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
var addressIds = vehicleEnrollments
|
||||||
|
.SelectMany(ve => ve.Route.RouteAddresses)
|
||||||
|
.Select(ra => ra.AddressId);
|
||||||
|
var addresses = (await _unitOfWork.AddressRepository
|
||||||
|
.GetPageAsync(
|
||||||
|
a => addressIds.Contains(a.Id),
|
||||||
|
a => a.City.Region.Country,
|
||||||
|
1, addressIds.Count(), cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
var vehicleIds = vehicleEnrollments
|
||||||
|
.Select(ve => ve.VehicleId);
|
||||||
|
var vehicles = (await _unitOfWork.VehicleRepository
|
||||||
|
.GetPageAsync(
|
||||||
|
v => vehicleIds.Contains(v.Id),
|
||||||
|
v => v.Company,
|
||||||
|
1, vehicleIds.Count(), cancellationToken))
|
||||||
|
.Items;
|
||||||
|
|
||||||
|
foreach (var ve in vehicleEnrollments)
|
||||||
|
{
|
||||||
|
ve.Vehicle = vehicles.Single(v => v.Id == ve.VehicleId);
|
||||||
|
|
||||||
|
foreach (var ra in ve.Route.RouteAddresses)
|
||||||
|
{
|
||||||
|
ra.Address = addresses.Single(a => a.Id == ra.AddressId);
|
||||||
|
ra.Details = routeAddressDetails
|
||||||
|
.Where(rad => rad.RouteAddressId == ra.Id)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var t in ticketGroup.Tickets)
|
||||||
|
{
|
||||||
|
t.VehicleEnrollment = vehicleEnrollments
|
||||||
|
.Single(ve => ve.Id == t.VehicleEnrollmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
|
||||||
|
if (ticketGroup.PassangerEmail != null)
|
||||||
|
{
|
||||||
|
var subject =
|
||||||
|
_localizer["PaymentProcessing.Ticket" +
|
||||||
|
".Email.PaymentCompleted.Subject"];
|
||||||
|
|
||||||
|
var ticketDetails = GetTicketDetails(ticketGroup);
|
||||||
|
|
||||||
|
var body = String.Format(
|
||||||
|
_localizer["PaymentProcessing.Ticket" +
|
||||||
|
".Email.PaymentCompleted.Body"],
|
||||||
|
ticketDetails);
|
||||||
|
|
||||||
|
await _emailSender.SendAsync(
|
||||||
|
new[] { ticketGroup.PassangerEmail }, subject,
|
||||||
|
body, cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await _unitOfWork.SaveAsync(cancellationToken);
|
await _unitOfWork.SaveAsync(cancellationToken);
|
||||||
_unitOfWork.Dispose();
|
_unitOfWork.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetTicketDetails(TicketGroup ticketGroup)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine("General:");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Ticket uuid: {ticketGroup.Guid}");
|
||||||
|
sb.AppendLine($"Purchase Time: {ticketGroup.PurchaseTime}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
var departureRouteAddressId =
|
||||||
|
ticketGroup.Tickets.First().DepartureRouteAddressId;
|
||||||
|
var arrivalRouteAddressId =
|
||||||
|
ticketGroup.Tickets.Last().ArrivalRouteAddressId;
|
||||||
|
|
||||||
|
var departureTime =
|
||||||
|
ticketGroup.Tickets.First()
|
||||||
|
.VehicleEnrollment.GetDepartureTime(departureRouteAddressId);
|
||||||
|
var arrivalTime =
|
||||||
|
ticketGroup.Tickets.Last()
|
||||||
|
.VehicleEnrollment.GetArrivalTime(arrivalRouteAddressId);
|
||||||
|
|
||||||
|
var departureAddress =
|
||||||
|
ticketGroup.Tickets.First()
|
||||||
|
.VehicleEnrollment.Route.RouteAddresses
|
||||||
|
.Single(ra => ra.Id == departureRouteAddressId)
|
||||||
|
.Address;
|
||||||
|
var arrivalAddress =
|
||||||
|
ticketGroup.Tickets.Last()
|
||||||
|
.VehicleEnrollment.Route.RouteAddresses
|
||||||
|
.Single(ra => ra.Id == arrivalRouteAddressId)
|
||||||
|
.Address;
|
||||||
|
|
||||||
|
var departureAddressName =
|
||||||
|
$"{departureAddress.City.Region.Country.Name}, " +
|
||||||
|
$"{departureAddress.City.Region.Name}, " +
|
||||||
|
$"{departureAddress.City.Name}, " +
|
||||||
|
$"{departureAddress.Name}";
|
||||||
|
var arrivalAddressName =
|
||||||
|
$"{arrivalAddress.City.Region.Country.Name}, " +
|
||||||
|
$"{arrivalAddress.City.Region.Name}, " +
|
||||||
|
$"{arrivalAddress.City.Name}, " +
|
||||||
|
$"{arrivalAddress.Name}";
|
||||||
|
|
||||||
|
sb.AppendLine($"Departure: {departureAddressName} at {departureTime}.");
|
||||||
|
sb.AppendLine($"Arrival: {arrivalAddressName} at {arrivalTime}.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine($"Passanger details:");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"First Name: {ticketGroup.PassangerFirstName}.");
|
||||||
|
sb.AppendLine($"Last Name: {ticketGroup.PassangerLastName}.");
|
||||||
|
sb.AppendLine($"Patronymic: {ticketGroup.PassangerPatronymic}.");
|
||||||
|
sb.AppendLine($"Sex: {ticketGroup.PassangerSex}.");
|
||||||
|
sb.AppendLine($"Birth Date: {ticketGroup.PassangerBirthDate}.");
|
||||||
|
sb.AppendLine($"Email: {ticketGroup.PassangerEmail}.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
|
||||||
|
sb.AppendLine("Vehicle enrollments' details:");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
foreach (var t in ticketGroup.Tickets)
|
||||||
|
{
|
||||||
|
departureRouteAddressId = t.DepartureRouteAddressId;
|
||||||
|
arrivalRouteAddressId = t.ArrivalRouteAddressId;
|
||||||
|
|
||||||
|
departureTime =
|
||||||
|
t.VehicleEnrollment.GetDepartureTime(departureRouteAddressId);
|
||||||
|
arrivalTime =
|
||||||
|
t.VehicleEnrollment.GetArrivalTime(arrivalRouteAddressId);
|
||||||
|
|
||||||
|
departureAddress =
|
||||||
|
t.VehicleEnrollment.Route.RouteAddresses
|
||||||
|
.Single(ra => ra.Id == departureRouteAddressId)
|
||||||
|
.Address;
|
||||||
|
arrivalAddress =
|
||||||
|
t.VehicleEnrollment.Route.RouteAddresses
|
||||||
|
.Single(ra => ra.Id == arrivalRouteAddressId)
|
||||||
|
.Address;
|
||||||
|
|
||||||
|
departureAddressName =
|
||||||
|
$"{departureAddress.City.Region.Country.Name}, " +
|
||||||
|
$"{departureAddress.City.Region.Name}, " +
|
||||||
|
$"{departureAddress.City.Name}, " +
|
||||||
|
$"{departureAddress.Name}";
|
||||||
|
arrivalAddressName =
|
||||||
|
$"{arrivalAddress.City.Region.Country.Name}, " +
|
||||||
|
$"{arrivalAddress.City.Region.Name}, " +
|
||||||
|
$"{arrivalAddress.City.Name}, " +
|
||||||
|
$"{arrivalAddress.Name}";
|
||||||
|
|
||||||
|
var vehicle = t.VehicleEnrollment.Vehicle;
|
||||||
|
var company = vehicle.Company;
|
||||||
|
|
||||||
|
sb.AppendLine($"Departure: {departureAddressName} at {departureTime}.");
|
||||||
|
sb.AppendLine($"Arrival: {arrivalAddressName} at {arrivalTime}.");
|
||||||
|
|
||||||
|
if (vehicle is Bus)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"Vehicle: Bus, {((Bus)vehicle).Model}, " +
|
||||||
|
$"{((Bus)vehicle).Number}.");
|
||||||
|
}
|
||||||
|
else if (vehicle is Aircraft)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"Vehicle: Aircraft, {((Aircraft)vehicle).Model}, " +
|
||||||
|
$"{((Aircraft)vehicle).Number}.");
|
||||||
|
}
|
||||||
|
else if (vehicle is Train)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"Vehicle: Train, {((Train)vehicle).Model}, " +
|
||||||
|
$"{((Train)vehicle).Number}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine($"Company: {company.Name}, ({company.ContactEmail}, " +
|
||||||
|
$"{company.ContactPhoneNumber}).");
|
||||||
|
|
||||||
|
var cost = t.Currency.Round(
|
||||||
|
t.VehicleEnrollment
|
||||||
|
.GetCost(departureRouteAddressId,arrivalRouteAddressId));
|
||||||
|
sb.AppendLine($"Cost: {cost} {t.Currency.Name}");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ public sealed class TicketGroupPaymentViewModel
|
|||||||
|
|
||||||
public DateOnly PassangerBirthDate { get; set; }
|
public DateOnly PassangerBirthDate { get; set; }
|
||||||
|
|
||||||
|
public string? PassangerEmail { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public ICollection<TicketPaymentViewModel> Tickets { get; set; }
|
public ICollection<TicketPaymentViewModel> Tickets { get; set; }
|
||||||
|
|
||||||
|
@ -62,6 +62,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PaymentProcessing": {
|
"PaymentProcessing": {
|
||||||
"TicketPaymentDescription": "Ticket purchase."
|
"Ticket": {
|
||||||
|
"PaymentDescription": "Ticket purchase.",
|
||||||
|
"Email": {
|
||||||
|
"PaymentCreated": {
|
||||||
|
"Subject": "Ticket purchase payment link.",
|
||||||
|
"Body": "You have reserved a ticket. Payment amount is {0} {1} Payment link is valid until {2}.\n\nLink: {3}"
|
||||||
|
},
|
||||||
|
"PaymentCompleted": {
|
||||||
|
"Subject": "Ticket purchase complete.",
|
||||||
|
"Body": "Payment is succeeded.\n\n\nTicket details:\n\n{0}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,16 +357,6 @@ public class SearchAllQueryHandler :
|
|||||||
tag = path.Select(e => e.Tag).Last();
|
tag = path.Select(e => e.Tag).Last();
|
||||||
|
|
||||||
|
|
||||||
lastRouteAddressGuid = vehicleEnrollments
|
|
||||||
.Single(e => e.Id == tag.VehicleEnrollmentId)
|
|
||||||
.RouteAddressDetails
|
|
||||||
.Select(e => e.RouteAddress)
|
|
||||||
.OrderBy(e => e.Order)
|
|
||||||
.SkipWhile(e => e.Order != tag.RouteAddress.Order)
|
|
||||||
.Take(2)
|
|
||||||
.ElementAt(1)
|
|
||||||
.Guid;
|
|
||||||
|
|
||||||
costToNextAddress = await _currencyConverterService
|
costToNextAddress = await _currencyConverterService
|
||||||
.ConvertAsync(tag.CostToNextAddress,
|
.ConvertAsync(tag.CostToNextAddress,
|
||||||
tag.VehicleEnrollment.Currency,
|
tag.VehicleEnrollment.Currency,
|
||||||
@ -388,7 +378,7 @@ public class SearchAllQueryHandler :
|
|||||||
CostToNextAddress = 0,
|
CostToNextAddress = 0,
|
||||||
CurrentAddressStopTime = tag.CurrentAddressStopTime,
|
CurrentAddressStopTime = tag.CurrentAddressStopTime,
|
||||||
Order = addressOrder,
|
Order = addressOrder,
|
||||||
RouteAddressUuid = lastRouteAddressGuid
|
RouteAddressUuid = tag.RouteAddress.Guid
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,8 +25,9 @@ public static class Configuration
|
|||||||
.AddCommandLine(args)
|
.AddCommandLine(args)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
services.AddOptions<PersistenceConfigurationOptions>()
|
services.AddOptions<PersistenceConfigurationOptions>().Bind(
|
||||||
.Bind(configuration);
|
configuration.GetSection(
|
||||||
|
PersistenceConfigurationOptions.SectionName));
|
||||||
|
|
||||||
services.AddOptions<ApplicationConfigurationOptions>()
|
services.AddOptions<ApplicationConfigurationOptions>()
|
||||||
.Bind(configuration);
|
.Bind(configuration);
|
||||||
|
@ -14,6 +14,8 @@ public sealed class TicketGroup : EntityBase
|
|||||||
|
|
||||||
public DateOnly PassangerBirthDate { get; set; }
|
public DateOnly PassangerBirthDate { get; set; }
|
||||||
|
|
||||||
|
public string? PassangerEmail { get; set; }
|
||||||
|
|
||||||
public DateTimeOffset PurchaseTime { get; set; }
|
public DateTimeOffset PurchaseTime { get; set; }
|
||||||
|
|
||||||
public TicketStatus Status { get; set; }
|
public TicketStatus Status { get; set; }
|
||||||
|
@ -121,7 +121,7 @@ public class VehicleEnrollment : EntityBase
|
|||||||
.OrderBy(e => e.RouteAddress.Order);
|
.OrderBy(e => e.RouteAddress.Order);
|
||||||
|
|
||||||
var departureRouteAddressDetail = orderedRouteAddressDetails
|
var departureRouteAddressDetail = orderedRouteAddressDetails
|
||||||
.Single(e => e.Id == DepartureRouteAddressId);
|
.Single(e => e.RouteAddressId == DepartureRouteAddressId);
|
||||||
|
|
||||||
var timeInStops = TimeSpan.Zero;
|
var timeInStops = TimeSpan.Zero;
|
||||||
foreach (var routeAddressDetail in orderedRouteAddressDetails)
|
foreach (var routeAddressDetail in orderedRouteAddressDetails)
|
||||||
@ -159,8 +159,8 @@ public class VehicleEnrollment : EntityBase
|
|||||||
return
|
return
|
||||||
RouteAddressDetails
|
RouteAddressDetails
|
||||||
.OrderBy(e => e.RouteAddress.Order)
|
.OrderBy(e => e.RouteAddress.Order)
|
||||||
.SkipWhile(e => e.Id != DepartureRouteAddressId)
|
.SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId)
|
||||||
.TakeWhile(e => e.Id != ArrivalRouteAddressId)
|
.TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId)
|
||||||
.Count() - 1;
|
.Count() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,8 +180,8 @@ public class VehicleEnrollment : EntityBase
|
|||||||
return
|
return
|
||||||
RouteAddressDetails
|
RouteAddressDetails
|
||||||
.OrderBy(e => e.RouteAddress.Order)
|
.OrderBy(e => e.RouteAddress.Order)
|
||||||
.SkipWhile(e => e.Id != DepartureRouteAddressId)
|
.SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId)
|
||||||
.TakeWhile(e => e.Id != ArrivalRouteAddressId)
|
.TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId)
|
||||||
.Aggregate(TimeSpan.Zero,
|
.Aggregate(TimeSpan.Zero,
|
||||||
(sum, next) => sum += next.TimeToNextAddress);
|
(sum, next) => sum += next.TimeToNextAddress);
|
||||||
}
|
}
|
||||||
@ -202,8 +202,8 @@ public class VehicleEnrollment : EntityBase
|
|||||||
return
|
return
|
||||||
RouteAddressDetails
|
RouteAddressDetails
|
||||||
.OrderBy(e => e.RouteAddress.Order)
|
.OrderBy(e => e.RouteAddress.Order)
|
||||||
.SkipWhile(e => e.Id != DepartureRouteAddressId)
|
.SkipWhile(e => e.RouteAddressId != DepartureRouteAddressId)
|
||||||
.TakeWhile(e => e.Id != ArrivalRouteAddressId)
|
.TakeWhile(e => e.RouteAddressId != ArrivalRouteAddressId)
|
||||||
.Aggregate((decimal)0,
|
.Aggregate((decimal)0,
|
||||||
(sum, next) => sum += next.CostToNextAddress);
|
(sum, next) => sum += next.CostToNextAddress);
|
||||||
}
|
}
|
||||||
|
@ -14,24 +14,39 @@ public abstract class Currency : Enumeration<Currency>
|
|||||||
|
|
||||||
protected Currency(int value, string name) : base(value, name) { }
|
protected Currency(int value, string name) : base(value, name) { }
|
||||||
|
|
||||||
|
protected virtual byte DecimalDigits { get; } = byte.MaxValue;
|
||||||
|
|
||||||
|
public decimal Round(decimal amount)
|
||||||
|
{
|
||||||
|
return Math.Round(amount, DecimalDigits);
|
||||||
|
}
|
||||||
|
|
||||||
// When no currency is specified
|
// When no currency is specified
|
||||||
private sealed class DefaultCurrency : Currency
|
private sealed class DefaultCurrency : Currency
|
||||||
{
|
{
|
||||||
public DefaultCurrency() : base(Int32.MaxValue, "DEFAULT") { }
|
public DefaultCurrency() : base(Int32.MaxValue, "DEFAULT") { }
|
||||||
|
|
||||||
|
protected override byte DecimalDigits => 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class USDCurrency : Currency
|
private sealed class USDCurrency : Currency
|
||||||
{
|
{
|
||||||
public USDCurrency() : base(840, "USD") { }
|
public USDCurrency() : base(840, "USD") { }
|
||||||
|
|
||||||
|
protected override byte DecimalDigits => 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class EURCurrency : Currency
|
private sealed class EURCurrency : Currency
|
||||||
{
|
{
|
||||||
public EURCurrency() : base(978, "EUR") { }
|
public EURCurrency() : base(978, "EUR") { }
|
||||||
|
|
||||||
|
protected override byte DecimalDigits => 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class UAHCurrency : Currency
|
private sealed class UAHCurrency : Currency
|
||||||
{
|
{
|
||||||
public UAHCurrency() : base(980, "UAH") { }
|
public UAHCurrency() : base(980, "UAH") { }
|
||||||
|
|
||||||
|
protected override byte DecimalDigits => 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ public class PaymentController : ControllerBase
|
|||||||
PassangerPatronymic = viewModel.PassangerPatronymic,
|
PassangerPatronymic = viewModel.PassangerPatronymic,
|
||||||
PassangerSex = Sex.FromName(viewModel.PassangerSex),
|
PassangerSex = Sex.FromName(viewModel.PassangerSex),
|
||||||
PassangerBirthDate = viewModel.PassangerBirthDate,
|
PassangerBirthDate = viewModel.PassangerBirthDate,
|
||||||
|
PassangerEmail = viewModel.PassangerEmail,
|
||||||
Tickets = viewModel.Tickets.Select(e =>
|
Tickets = viewModel.Tickets.Select(e =>
|
||||||
new TicketGroupPaymentTicketModel()
|
new TicketGroupPaymentTicketModel()
|
||||||
{
|
{
|
||||||
|
@ -49,37 +49,43 @@ public class TicketGroupConfiguration : BaseConfiguration<TicketGroup>
|
|||||||
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.PassangerFirstName)
|
.Property(tg => tg.PassangerFirstName)
|
||||||
.HasColumnName("passanger_first_name")
|
.HasColumnName("passanger_first_name")
|
||||||
.HasColumnType("varchar(32)")
|
.HasColumnType("varchar(32)")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.PassangerLastName)
|
.Property(tg => tg.PassangerLastName)
|
||||||
.HasColumnName("passanger_last_name")
|
.HasColumnName("passanger_last_name")
|
||||||
.HasColumnType("varchar(32)")
|
.HasColumnType("varchar(32)")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.PassangerPatronymic)
|
.Property(tg => tg.PassangerPatronymic)
|
||||||
.HasColumnName("passanger_patronymic")
|
.HasColumnName("passanger_patronymic")
|
||||||
.HasColumnType("varchar(32)")
|
.HasColumnType("varchar(32)")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.PassangerBirthDate)
|
.Property(tg => tg.PassangerBirthDate)
|
||||||
.HasColumnName("passanger_birth_date")
|
.HasColumnName("passanger_birth_date")
|
||||||
.HasColumnType("date")
|
.HasColumnType("date")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.PurchaseTime)
|
.Property(tg => tg.PassangerEmail)
|
||||||
|
.HasColumnName("passanger_email")
|
||||||
|
.HasColumnType("varchar(256)")
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Property(tg => tg.PurchaseTime)
|
||||||
.HasColumnName("purchase_time")
|
.HasColumnName("purchase_time")
|
||||||
.HasColumnType("timestamptz")
|
.HasColumnType("timestamptz")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Property(a => a.TravelTime)
|
.Property(tg => tg.TravelTime)
|
||||||
.HasColumnName("travel_time")
|
.HasColumnName("travel_time")
|
||||||
.HasColumnType("interval")
|
.HasColumnType("interval")
|
||||||
.IsRequired(true);
|
.IsRequired(true);
|
||||||
|
1339
src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs
generated
Normal file
1339
src/Persistence/PostgreSql/Migrations/20250529131846_Add_email_to_Ticket_Group.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Persistence.PostgreSql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_email_to_Ticket_Group : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "passanger_email",
|
||||||
|
schema: "application",
|
||||||
|
table: "ticket_groups",
|
||||||
|
type: "varchar(256)",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "passanger_email",
|
||||||
|
schema: "application",
|
||||||
|
table: "ticket_groups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -737,6 +737,10 @@ namespace Persistence.PostgreSql.Migrations
|
|||||||
.HasColumnType("date")
|
.HasColumnType("date")
|
||||||
.HasColumnName("passanger_birth_date");
|
.HasColumnName("passanger_birth_date");
|
||||||
|
|
||||||
|
b.Property<string>("PassangerEmail")
|
||||||
|
.HasColumnType("varchar(256)")
|
||||||
|
.HasColumnName("passanger_email");
|
||||||
|
|
||||||
b.Property<string>("PassangerFirstName")
|
b.Property<string>("PassangerFirstName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("varchar(32)")
|
.HasColumnType("varchar(32)")
|
||||||
|
Loading…
Reference in New Issue
Block a user