Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs
Original file line number Diff line number Diff line change
@@ -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<Result> SendTicketAsync(MailRecipient recipient, string eventName, byte[] pdfData);

public Task<Result> SendTicketsAsync(Customer customer, List<TicketWithScanUrl> tickets);
public Task<Result> SendMailAsync(IEnumerable<MailRecipient> recipients, string subject, string content, List<MailAttachment>? attachments);
}
50 changes: 37 additions & 13 deletions TickAPI/TickAPI/Common/Mail/Services/MailService.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,61 @@
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;

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<Result> SendTicketAsync(MailRecipient recipient, string eventName, byte[] pdfData)
public async Task<Result> SendTicketsAsync(Customer customer, List<TicketWithScanUrl> tickets)
{
var subject = $"Ticket for {eventName}";
var htmlContent = "<strong>Download your ticket from attachments</strong>";
var base64Content = Convert.ToBase64String(pdfData);
List<MailAttachment> 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("<strong>Here are your tickets:</strong><br/><ul>");

var attachments = new List<MailAttachment>();

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(
$"<li>Ticket for event <b>{eventName}</b> 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("</ul>");

var recipient = new MailRecipient(customer.Email, customer.FirstName);
return await SendMailAsync([recipient], subject, htmlContent.ToString(), attachments);
}

public async Task<Result> SendMailAsync(IEnumerable<MailRecipient> recipients, string subject, string content,
List<MailAttachment>? attachments = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -13,6 +14,6 @@ public interface IShoppingCartService
public Task<Result> RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail);
public Task<Result> RemoveResellTicketFromCartAsync(Guid ticketId, string customerEmail);
public Task<Result<Dictionary<string, decimal>>> GetDueAmountAsync(string customerEmail);
public Task<Result<PaymentResponsePG>> CheckoutAsync(string customerEmail, decimal amount, string currency,
public Task<Result<CheckoutResult>> CheckoutAsync(string customerEmail, decimal amount, string currency,
string cardNumber, string cardExpiry, string cvv);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)]
Expand Down Expand Up @@ -136,6 +143,27 @@ public async Task<ActionResult<PaymentResponsePG>> 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;
}
}
9 changes: 9 additions & 0 deletions TickAPI/TickAPI/ShoppingCarts/Models/CheckoutResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using TickAPI.Common.Payment.Models;
using TickAPI.Tickets.Models;

namespace TickAPI.ShoppingCarts.Models;

public record CheckoutResult(
List<Ticket> BoughtTickets,
PaymentResponsePG PaymentResponse
);
54 changes: 34 additions & 20 deletions TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,27 +246,27 @@ public async Task<Result<Dictionary<string, decimal>>> GetDueAmountAsync(string
return Result<Dictionary<string, decimal>>.Success(dueAmount);
}

public async Task<Result<PaymentResponsePG>> CheckoutAsync(string customerEmail, decimal amount, string currency,
public async Task<Result<CheckoutResult>> CheckoutAsync(string customerEmail, decimal amount, string currency,
string cardNumber, string cardExpiry, string cvv)
{
var dueAmountResult = await GetDueAmountAsync(customerEmail);

if (dueAmountResult.IsError)
{
return Result<PaymentResponsePG>.PropagateError(dueAmountResult);
return Result<CheckoutResult>.PropagateError(dueAmountResult);
}

var currencyExists = dueAmountResult.Value!.TryGetValue(currency, out var dueAmount);

if (!currencyExists)
{
return Result<PaymentResponsePG>.Failure(StatusCodes.Status400BadRequest,
return Result<CheckoutResult>.Failure(StatusCodes.Status400BadRequest,
$"no tickets paid in {currency} found in cart");
}

if (dueAmount != amount)
{
return Result<PaymentResponsePG>.Failure(StatusCodes.Status400BadRequest,
return Result<CheckoutResult>.Failure(StatusCodes.Status400BadRequest,
$"the given amount {amount} {currency} is different than the expected amount of {dueAmount} {currency}");
}

Expand All @@ -276,14 +276,14 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc

if (paymentResult.IsError)
{
return Result<PaymentResponsePG>.PropagateError(paymentResult);
return Result<CheckoutResult>.PropagateError(paymentResult);
}

var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail);

if (getShoppingCartResult.IsError)
{
return Result<PaymentResponsePG>.PropagateError(getShoppingCartResult);
return Result<CheckoutResult>.PropagateError(getShoppingCartResult);
}

var cart = getShoppingCartResult.Value!;
Expand All @@ -292,7 +292,7 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc

if (getCustomerResult.IsError)
{
return Result<PaymentResponsePG>.PropagateError(getCustomerResult);
return Result<CheckoutResult>.PropagateError(getCustomerResult);
}

var owner = getCustomerResult.Value!;
Expand All @@ -301,32 +301,40 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc

if (generateTicketsResult.IsError)
{
return Result<PaymentResponsePG>.PropagateError(generateTicketsResult);
return Result<CheckoutResult>.PropagateError(generateTicketsResult);
}

var boughtTickets = generateTicketsResult.Value!;

var passOwnershipResult = await PassTicketOwnershipAsync(cart, owner, currency);

if (passOwnershipResult.IsError)
{
return Result<PaymentResponsePG>.PropagateError(passOwnershipResult);
return Result<CheckoutResult>.PropagateError(passOwnershipResult);
}

var resoldTickets = passOwnershipResult.Value!;

List<Ticket> allTickets = [..boughtTickets, ..resoldTickets];

var payment = paymentResult.Value!;

return Result<PaymentResponsePG>.Success(payment);
return Result<CheckoutResult>.Success(new CheckoutResult(allTickets, payment));
}

private async Task<Result> GenerateBoughtTicketsAsync(ShoppingCart cart, Customer owner, string currency)
private async Task<Result<List<Ticket>>> GenerateBoughtTicketsAsync(ShoppingCart cart, Customer owner, string currency)
{
var removals = new List<(Guid id, uint amount)>();

var newTickets = new List<Ticket>();

foreach (var ticket in cart.NewTickets)
{
var ticketTypeResult = await _ticketService.GetTicketTypeByIdAsync(ticket.TicketTypeId);

if (ticketTypeResult.IsError)
{
return Result.PropagateError(ticketTypeResult);
return Result<List<Ticket>>.PropagateError(ticketTypeResult);
}

var type = ticketTypeResult.Value!;
Expand All @@ -341,8 +349,10 @@ private async Task<Result> GenerateBoughtTicketsAsync(ShoppingCart cart, Custome

if (createTicketResult.IsError)
{
return Result.PropagateError(createTicketResult);
return Result<List<Ticket>>.PropagateError(createTicketResult);
}

newTickets.Add(createTicketResult.Value!);
}
}
}
Expand All @@ -353,24 +363,26 @@ private async Task<Result> GenerateBoughtTicketsAsync(ShoppingCart cart, Custome

if (removalResult.IsError)
{
return Result.PropagateError(removalResult);
return Result<List<Ticket>>.PropagateError(removalResult);
}
}

return Result.Success();
return Result<List<Ticket>>.Success(newTickets);
}

private async Task<Result> PassTicketOwnershipAsync(ShoppingCart cart, Customer newOwner, string currency)
private async Task<Result<List<Ticket>>> PassTicketOwnershipAsync(ShoppingCart cart, Customer newOwner, string currency)
{
var removals = new List<Guid>();

var resoldTickets = new List<Ticket>();

foreach (var resellTicket in cart.ResellTickets)
{
var ticketResult = await _ticketService.GetTicketByIdAsync(resellTicket.TicketId);

if (ticketResult.IsError)
{
return Result.PropagateError(ticketResult);
return Result<List<Ticket>>.PropagateError(ticketResult);
}

var ticket = ticketResult.Value!;
Expand All @@ -383,8 +395,10 @@ private async Task<Result> PassTicketOwnershipAsync(ShoppingCart cart, Customer

if (createTicketResult.IsError)
{
return Result.PropagateError(createTicketResult);
return Result<List<Ticket>>.PropagateError(createTicketResult);
}

resoldTickets.Add(ticket);
}
}

Expand All @@ -394,10 +408,10 @@ private async Task<Result> PassTicketOwnershipAsync(ShoppingCart cart, Customer

if (removalResult.IsError)
{
return Result.PropagateError(removalResult);
return Result<List<Ticket>>.PropagateError(removalResult);
}
}

return Result.Success();
return Result<List<Ticket>>.Success(resoldTickets);
}
}
2 changes: 1 addition & 1 deletion TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface ITicketRepository
public IQueryable<Ticket> GetTicketsByCustomerEmail(string email);
public Task<Result> MarkTicketAsUsed(Guid id);
public Task<Result> SetTicketForResell(Guid ticketId, decimal newPrice, string currency);
public Task<Result> AddTicketAsync(Ticket ticket);
public Task<Result<Ticket>> AddTicketAsync(Ticket ticket);
public Task<Result<Ticket>> GetTicketWithDetailsByIdAsync(Guid id);
public Task<Result> ChangeTicketOwnershipAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null);
}
2 changes: 1 addition & 1 deletion TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public Task<Result<GetTicketDetailsResponseDto>> GetTicketDetailsAsync(Guid tick
public Task<Result> SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice, string resellCurrency);
public Task<Result<Ticket>> GetTicketByIdAsync(Guid ticketId);
public Task<Result<TicketType>> GetTicketTypeByIdAsync(Guid ticketTypeId);
public Task<Result> CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null,
public Task<Result<Ticket>> CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null,
string? seats = null);
public Task<Result> ChangeTicketOwnershipViaResellAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null);
}
1 change: 0 additions & 1 deletion TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions TickAPI/TickAPI/Tickets/Models/TicketWithScanUrl.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Loading
Loading