diff --git a/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs b/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs index f4b3444..ad0cfe1 100644 --- a/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs +++ b/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs @@ -1,11 +1,12 @@ using TickAPI.Common.Mail.Models; using TickAPI.Common.Results; +using TickAPI.Customers.Models; +using TickAPI.Tickets.Models; namespace TickAPI.Common.Mail.Abstractions; public interface IMailService { - public Task SendTicketAsync(MailRecipient recipient, string eventName, byte[] pdfData); - + public Task SendTicketsAsync(Customer customer, List tickets); public Task SendMailAsync(IEnumerable recipients, string subject, string content, List? attachments); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Mail/Services/MailService.cs b/TickAPI/TickAPI/Common/Mail/Services/MailService.cs index b35b5c4..21923b4 100644 --- a/TickAPI/TickAPI/Common/Mail/Services/MailService.cs +++ b/TickAPI/TickAPI/Common/Mail/Services/MailService.cs @@ -1,8 +1,12 @@ -using SendGrid; +using System.Text; +using SendGrid; using SendGrid.Helpers.Mail; using TickAPI.Common.Mail.Abstractions; using TickAPI.Common.Mail.Models; +using TickAPI.Common.QR.Abstractions; using TickAPI.Common.Results; +using TickAPI.Customers.Models; +using TickAPI.Tickets.Models; namespace TickAPI.Common.Mail.Services; @@ -10,28 +14,48 @@ public class MailService : IMailService { private readonly SendGridClient _client; private readonly EmailAddress _fromEmailAddress; + private readonly IQRCodeService _qrCodeService; - public MailService(IConfiguration configuration) + public MailService(IConfiguration configuration, IQRCodeService qrCodeService) { + _qrCodeService = qrCodeService; var apiKey = configuration["SendGrid:ApiKey"]; _client = new SendGridClient(apiKey); var fromEmail = configuration["SendGrid:FromEmail"]; var fromName = configuration["SendGrid:FromName"]; _fromEmailAddress = new EmailAddress(fromEmail, fromName); } - - public async Task SendTicketAsync(MailRecipient recipient, string eventName, byte[] pdfData) + + public async Task SendTicketsAsync(Customer customer, List tickets) { - var subject = $"Ticket for {eventName}"; - var htmlContent = "Download your ticket from attachments"; - var base64Content = Convert.ToBase64String(pdfData); - List attachments = [ - new MailAttachment("ticket.pdf", base64Content, "application/pdf") - ]; - var res = await SendMailAsync([recipient], subject, htmlContent, attachments); - return res; - } + var subject = "Your New Tickets"; + var htmlContent = new StringBuilder(); + htmlContent.AppendLine("Here are your tickets:
    "); + + var attachments = new List(); + + foreach (var tWithScanUrl in tickets) + { + var ticket = tWithScanUrl.Ticket; + var eventName = ticket.Type.Event.Name; + var eventDate = ticket.Type.Event.StartDate.ToString("yyyy-MM-dd"); + + htmlContent.AppendLine( + $"
  • Ticket for event {eventName} on {eventDate} " + ); + + var pdfData = _qrCodeService.GenerateQrCode(tWithScanUrl.ScanUrl); + + var base64Content = Convert.ToBase64String(pdfData); + attachments.Add(new MailAttachment($"ticket_{ticket.Id}.pdf", base64Content, "application/pdf")); + } + + htmlContent.AppendLine("
"); + var recipient = new MailRecipient(customer.Email, customer.FirstName); + return await SendMailAsync([recipient], subject, htmlContent.ToString(), attachments); + } + public async Task SendMailAsync(IEnumerable recipients, string subject, string content, List? attachments = null) { diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 609ff79..9bd10a3 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -2,6 +2,7 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.ShoppingCarts.Models; namespace TickAPI.ShoppingCarts.Abstractions; @@ -13,6 +14,6 @@ public interface IShoppingCartService public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task RemoveResellTicketFromCartAsync(Guid ticketId, string customerEmail); public Task>> GetDueAmountAsync(string customerEmail); - public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, + public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 88542c5..94d0f9b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -2,10 +2,13 @@ using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Mail.Abstractions; using TickAPI.Common.Payment.Models; +using TickAPI.Customers.Abstractions; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Request; using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Controllers; @@ -15,11 +18,15 @@ public class ShoppingCartsController : ControllerBase { private readonly IShoppingCartService _shoppingCartService; private readonly IClaimsService _claimsService; + private readonly IMailService _mailService; + private readonly ICustomerService _customerService; - public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaimsService claimsService) + public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaimsService claimsService, IMailService mailService, ICustomerService customerService) { _shoppingCartService = shoppingCartService; _claimsService = claimsService; + _mailService = mailService; + _customerService = customerService; } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -136,6 +143,27 @@ public async Task> Checkout([FromBody] CheckoutD var checkoutResult = await _shoppingCartService.CheckoutAsync(email, checkoutDto.Amount, checkoutDto.Currency, checkoutDto.CardNumber, checkoutDto.CardExpiry, checkoutDto.Cvv); - return checkoutResult.ToObjectResult(); + if (checkoutResult.IsError) + { + return checkoutResult.ToObjectResult(); + } + + var checkout = checkoutResult.Value!; + + var customerResult = await _customerService.GetCustomerByEmailAsync(email); + if (customerResult.IsError) + { + return customerResult.ToObjectResult(); + } + + var customer = customerResult.Value!; + + await _mailService.SendTicketsAsync(customer, checkout.BoughtTickets.Select(t => + { + var scanUrl = Url.Action("ScanTicket", "Tickets", new { id = t.Id }, Request.Scheme)!; + return new TicketWithScanUrl(t, scanUrl); + }).ToList()); + + return checkout.PaymentResponse; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/CheckoutResult.cs b/TickAPI/TickAPI/ShoppingCarts/Models/CheckoutResult.cs new file mode 100644 index 0000000..cb4870f --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Models/CheckoutResult.cs @@ -0,0 +1,9 @@ +using TickAPI.Common.Payment.Models; +using TickAPI.Tickets.Models; + +namespace TickAPI.ShoppingCarts.Models; + +public record CheckoutResult( + List BoughtTickets, + PaymentResponsePG PaymentResponse +); diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 2f01bc6..2a00ca2 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -246,27 +246,27 @@ public async Task>> GetDueAmountAsync(string return Result>.Success(dueAmount); } - public async Task> CheckoutAsync(string customerEmail, decimal amount, string currency, + public async Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv) { var dueAmountResult = await GetDueAmountAsync(customerEmail); if (dueAmountResult.IsError) { - return Result.PropagateError(dueAmountResult); + return Result.PropagateError(dueAmountResult); } var currencyExists = dueAmountResult.Value!.TryGetValue(currency, out var dueAmount); if (!currencyExists) { - return Result.Failure(StatusCodes.Status400BadRequest, + return Result.Failure(StatusCodes.Status400BadRequest, $"no tickets paid in {currency} found in cart"); } if (dueAmount != amount) { - return Result.Failure(StatusCodes.Status400BadRequest, + return Result.Failure(StatusCodes.Status400BadRequest, $"the given amount {amount} {currency} is different than the expected amount of {dueAmount} {currency}"); } @@ -276,14 +276,14 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc if (paymentResult.IsError) { - return Result.PropagateError(paymentResult); + return Result.PropagateError(paymentResult); } var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) { - return Result.PropagateError(getShoppingCartResult); + return Result.PropagateError(getShoppingCartResult); } var cart = getShoppingCartResult.Value!; @@ -292,7 +292,7 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc if (getCustomerResult.IsError) { - return Result.PropagateError(getCustomerResult); + return Result.PropagateError(getCustomerResult); } var owner = getCustomerResult.Value!; @@ -301,32 +301,40 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc if (generateTicketsResult.IsError) { - return Result.PropagateError(generateTicketsResult); + return Result.PropagateError(generateTicketsResult); } + var boughtTickets = generateTicketsResult.Value!; + var passOwnershipResult = await PassTicketOwnershipAsync(cart, owner, currency); if (passOwnershipResult.IsError) { - return Result.PropagateError(passOwnershipResult); + return Result.PropagateError(passOwnershipResult); } + + var resoldTickets = passOwnershipResult.Value!; + + List allTickets = [..boughtTickets, ..resoldTickets]; var payment = paymentResult.Value!; - return Result.Success(payment); + return Result.Success(new CheckoutResult(allTickets, payment)); } - private async Task GenerateBoughtTicketsAsync(ShoppingCart cart, Customer owner, string currency) + private async Task>> GenerateBoughtTicketsAsync(ShoppingCart cart, Customer owner, string currency) { var removals = new List<(Guid id, uint amount)>(); + var newTickets = new List(); + foreach (var ticket in cart.NewTickets) { var ticketTypeResult = await _ticketService.GetTicketTypeByIdAsync(ticket.TicketTypeId); if (ticketTypeResult.IsError) { - return Result.PropagateError(ticketTypeResult); + return Result>.PropagateError(ticketTypeResult); } var type = ticketTypeResult.Value!; @@ -341,8 +349,10 @@ private async Task GenerateBoughtTicketsAsync(ShoppingCart cart, Custome if (createTicketResult.IsError) { - return Result.PropagateError(createTicketResult); + return Result>.PropagateError(createTicketResult); } + + newTickets.Add(createTicketResult.Value!); } } } @@ -353,24 +363,26 @@ private async Task GenerateBoughtTicketsAsync(ShoppingCart cart, Custome if (removalResult.IsError) { - return Result.PropagateError(removalResult); + return Result>.PropagateError(removalResult); } } - return Result.Success(); + return Result>.Success(newTickets); } - private async Task PassTicketOwnershipAsync(ShoppingCart cart, Customer newOwner, string currency) + private async Task>> PassTicketOwnershipAsync(ShoppingCart cart, Customer newOwner, string currency) { var removals = new List(); + var resoldTickets = new List(); + foreach (var resellTicket in cart.ResellTickets) { var ticketResult = await _ticketService.GetTicketByIdAsync(resellTicket.TicketId); if (ticketResult.IsError) { - return Result.PropagateError(ticketResult); + return Result>.PropagateError(ticketResult); } var ticket = ticketResult.Value!; @@ -383,8 +395,10 @@ private async Task PassTicketOwnershipAsync(ShoppingCart cart, Customer if (createTicketResult.IsError) { - return Result.PropagateError(createTicketResult); + return Result>.PropagateError(createTicketResult); } + + resoldTickets.Add(ticket); } } @@ -394,10 +408,10 @@ private async Task PassTicketOwnershipAsync(ShoppingCart cart, Customer if (removalResult.IsError) { - return Result.PropagateError(removalResult); + return Result>.PropagateError(removalResult); } } - return Result.Success(); + return Result>.Success(resoldTickets); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index e441b09..9536346 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -14,7 +14,7 @@ public interface ITicketRepository public IQueryable GetTicketsByCustomerEmail(string email); public Task MarkTicketAsUsed(Guid id); public Task SetTicketForResell(Guid ticketId, decimal newPrice, string currency); - public Task AddTicketAsync(Ticket ticket); + public Task> AddTicketAsync(Ticket ticket); public Task> GetTicketWithDetailsByIdAsync(Guid id); public Task ChangeTicketOwnershipAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 30c46a2..15a2123 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -24,7 +24,7 @@ public Task> GetTicketDetailsAsync(Guid tick public Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice, string resellCurrency); public Task> GetTicketByIdAsync(Guid ticketId); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); - public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, + public Task> CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, string? seats = null); public Task ChangeTicketOwnershipViaResellAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 54209b7..4de33ef 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -5,7 +5,6 @@ using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; using TickAPI.Common.Pagination.Responses; -using TickAPI.Common.Results; using TickAPI.Tickets.DTOs.Request; namespace TickAPI.Tickets.Controllers; diff --git a/TickAPI/TickAPI/Tickets/Models/TicketWithScanUrl.cs b/TickAPI/TickAPI/Tickets/Models/TicketWithScanUrl.cs new file mode 100644 index 0000000..97ca0ab --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/TicketWithScanUrl.cs @@ -0,0 +1,13 @@ +namespace TickAPI.Tickets.Models; + +public class TicketWithScanUrl +{ + public TicketWithScanUrl(Ticket ticket, string scanUrl) + { + Ticket = ticket; + ScanUrl = scanUrl; + } + + public Ticket Ticket { get; set; } + public string ScanUrl { get; set; } +} diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 90d3244..f32917f 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -72,20 +72,20 @@ public async Task MarkTicketAsUsed(Guid id) return Result.Success(); } - public async Task AddTicketAsync(Ticket ticket) + public async Task> AddTicketAsync(Ticket ticket) { var maxCount = ticket.Type.MaxCount; if (maxCount <= _tickApiDbContext.Tickets.Count(t => t.Type.Id == ticket.Type.Id)) { - return Result.Failure(StatusCodes.Status400BadRequest, + return Result.Failure(StatusCodes.Status400BadRequest, "The ticket you are trying to buy has already reached its max count"); } _tickApiDbContext.Tickets.Add(ticket); await _tickApiDbContext.SaveChangesAsync(); - return Result.Success(); + return Result.Success(ticket); } public async Task> GetTicketWithDetailsByIdAsync(Guid id) diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index c9de357..96f2a76 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -188,7 +188,7 @@ public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) return Result.Success(ticketTypeResult.Value!); } - public async Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, + public async Task> CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, string? seats = null) { var ticket = new Ticket