From be2f07cdc56249fb5fd8ff12c98b10680e01d47a Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 27 Apr 2025 00:20:21 +0200 Subject: [PATCH 01/32] added files for shopping cart service --- TickAPI/TickAPI/Program.cs | 5 +++++ .../ShoppingCarts/Abstractions/IShoppingCartService.cs | 6 ++++++ TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs | 8 ++++++++ .../TickAPI/ShoppingCarts/Services/ShoppingCartService.cs | 8 ++++++++ 4 files changed, 27 insertions(+) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 59b7d54..f946f72 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -38,6 +38,8 @@ using TickAPI.Common.Claims.Services; using TickAPI.Common.Redis.Abstractions; using TickAPI.Common.Redis.Services; +using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -112,6 +114,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add shopping cart services. +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs new file mode 100644 index 0000000..80e4e6d --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.Abstractions; + +public interface IShoppingCartService +{ + +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs new file mode 100644 index 0000000..6c39b9c --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -0,0 +1,8 @@ +using TickAPI.Tickets.Models; + +namespace TickAPI.ShoppingCarts.Models; + +public class ShoppingCart +{ + public List Tickets { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs new file mode 100644 index 0000000..b1c9d15 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -0,0 +1,8 @@ +using TickAPI.ShoppingCarts.Abstractions; + +namespace TickAPI.ShoppingCarts.Services; + +public class ShoppingCartService : IShoppingCartService +{ + +} \ No newline at end of file From 109be6078df181594f2ea18629317f457b2398ce Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 27 Apr 2025 02:03:47 +0200 Subject: [PATCH 02/32] added necessary endpoint mocks for the shopping carts controller --- .../Controllers/ShoppingCartsController.cs | 32 +++++++++++++++++++ .../ShoppingCarts/Models/ShoppingCart.cs | 2 +- .../Tickets/Models/ShoppingCartTicket.cs | 9 ++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs create mode 100644 TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs new file mode 100644 index 0000000..1651954 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.ShoppingCarts.Abstractions; + +namespace TickAPI.ShoppingCarts.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ShoppingCartsController : ControllerBase +{ + private readonly IShoppingCartService _shoppingCartService; + + public ShoppingCartsController(IShoppingCartService shoppingCartService) + { + _shoppingCartService = shoppingCartService; + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost("add-ticket")] + public async Task AddTicket() + { + throw new NotImplementedException(); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost("checkout")] + public async Task Checkout() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs index 6c39b9c..c734465 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -4,5 +4,5 @@ namespace TickAPI.ShoppingCarts.Models; public class ShoppingCart { - public List Tickets { get; set; } + public List Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs new file mode 100644 index 0000000..c19904b --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs @@ -0,0 +1,9 @@ +namespace TickAPI.Tickets.Models; + +public class ShoppingCartTicket +{ + public Guid TicketTypeId { get; set; } + public Guid CustomerId { get; set; } + public string NameOnTicket { get; set; } + public string? Seats { get; set; } +} \ No newline at end of file From d13818015d1c3d290049243ef3060732f9819710 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 27 Apr 2025 02:08:07 +0200 Subject: [PATCH 03/32] added mocks for the rest of necessary endpoints --- .../Controllers/ShoppingCartsController.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 1651954..5c19622 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -17,12 +17,26 @@ public ShoppingCartsController(IShoppingCartService shoppingCartService) } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpPost("add-ticket")] + [HttpPost("ticket")] public async Task AddTicket() { throw new NotImplementedException(); } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("tickets")] + public async Task GetTickets() + { + throw new NotImplementedException(); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpDelete("ticket")] + public async Task DeleteTicket() + { + throw new NotImplementedException(); + } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] public async Task Checkout() From 64e56701fed724ab2b5746d61682d1c702bf2c2d Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 3 May 2025 00:57:59 +0200 Subject: [PATCH 04/32] added the base of ShoppingCartService.cs methods (headers still require changing) --- .../Abstractions/IShoppingCartService.cs | 9 ++++++-- .../Services/ShoppingCartService.cs | 23 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 80e4e6d..c72e3a9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -1,6 +1,11 @@ -namespace TickAPI.ShoppingCarts.Abstractions; +using TickAPI.Common.Results; + +namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - + public Task AddTicketAsync(); + public Task GetTicketsAsync(); + public Task RemoveTicketAsync(); + public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index b1c9d15..8233773 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,8 +1,27 @@ -using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.Common.Results; +using TickAPI.ShoppingCarts.Abstractions; namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { - + public Task AddTicketAsync() + { + throw new NotImplementedException(); + } + + public Task GetTicketsAsync() + { + throw new NotImplementedException(); + } + + public Task RemoveTicketAsync() + { + throw new NotImplementedException(); + } + + public Task CheckoutAsync() + { + throw new NotImplementedException(); + } } \ No newline at end of file From e6619a4d06f1c32f816196914da6d35e4ff8f606 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 3 May 2025 02:17:32 +0200 Subject: [PATCH 05/32] added acquiring cart contents (from service) and adding ticket to cart (via controller) --- .../Common/Results/ResultTests.cs | 25 ++++++++ TickAPI/TickAPI/Common/Results/Result.cs | 10 +++ TickAPI/TickAPI/Program.cs | 5 ++ .../Abstractions/IShoppingCartRepository.cs | 11 ++++ .../Abstractions/IShoppingCartService.cs | 2 +- .../Controllers/ShoppingCartsController.cs | 26 ++++++-- .../DTOs/Request/AddNewTicketDto.cs | 7 +++ .../ShoppingCarts/Models/ShoppingCart.cs | 3 +- .../Repositories/ShoppingCartRepository.cs | 61 +++++++++++++++++++ .../Services/ShoppingCartService.cs | 36 ++++++++++- TickAPI/TickAPI/TickAPI.csproj | 4 ++ ...CartTicket.cs => ShoppingCartNewTicket.cs} | 5 +- .../Models/ShoppingCartResellTicket.cs | 7 +++ 13 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs rename TickAPI/TickAPI/Tickets/Models/{ShoppingCartTicket.cs => ShoppingCartNewTicket.cs} (51%) create mode 100644 TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index f09668d..0c26548 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -30,6 +30,21 @@ public void Failure_ShouldReturnResultWithError() Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + [Fact] public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWithError() { @@ -45,6 +60,16 @@ public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWi Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } + [Fact] public void PropagateError_WhenGenericResultWithSuccessPassed_ShouldThrowArgumentException() { diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index d339858..3d03d84 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -26,6 +26,16 @@ public static Result Failure(int statusCode, string errorMsg) return new Result(false, statusCode, errorMsg); } + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } + public static Result PropagateError(Result other) { if (other.IsSuccess) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 26fe234..9180548 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -43,6 +43,7 @@ using TickAPI.Common.Payment.Health; using TickAPI.Common.Payment.Services; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; // Builder constants @@ -118,6 +119,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add shopping cart services. +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs new file mode 100644 index 0000000..af2d2d8 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -0,0 +1,11 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.Models; + +namespace TickAPI.ShoppingCarts.Abstractions; + +public interface IShoppingCartRepository +{ + public Task> GetShoppingCartByEmailAsync(string customerEmail); + public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index c72e3a9..12103bb 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -4,7 +4,7 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - public Task AddTicketAsync(); + public Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); public Task GetTicketsAsync(); public Task RemoveTicketAsync(); public Task CheckoutAsync(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 5c19622..3f3e16a 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.DTOs.Request; namespace TickAPI.ShoppingCarts.Controllers; @@ -10,17 +12,33 @@ namespace TickAPI.ShoppingCarts.Controllers; public class ShoppingCartsController : ControllerBase { private readonly IShoppingCartService _shoppingCartService; + private readonly IClaimsService _claimsService; - public ShoppingCartsController(IShoppingCartService shoppingCartService) + public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaimsService claimsService) { _shoppingCartService = shoppingCartService; + _claimsService = claimsService; } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpPost("ticket")] - public async Task AddTicket() + [HttpPost] + public async Task AddTicket([FromBody] AddNewTicketDto addNewTicketDto) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var addTicketResult = await _shoppingCartService.AddNewTicketAsync(addNewTicketDto.TicketTypeId, email, + addNewTicketDto.NameOnTicket, addNewTicketDto.Seats); + if (addTicketResult.IsError) + { + return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); + } + + return Ok(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs new file mode 100644 index 0000000..de86962 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs @@ -0,0 +1,7 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record AddNewTicketDto( + Guid TicketTypeId, + string? NameOnTicket, + string? Seats +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs index c734465..2ed9b5f 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -4,5 +4,6 @@ namespace TickAPI.ShoppingCarts.Models; public class ShoppingCart { - public List Tickets { get; set; } + public List Tickets { get; set; } = []; + public List ResellTickets { get; set; } = []; } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs new file mode 100644 index 0000000..6cda1c9 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -0,0 +1,61 @@ +using TickAPI.Common.Redis.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Models; + +namespace TickAPI.ShoppingCarts.Repositories; + +public class ShoppingCartRepository : IShoppingCartRepository +{ + private readonly IRedisService _redisService; + private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(15); + + public ShoppingCartRepository(IRedisService redisService) + { + _redisService = redisService; + } + + public async Task> GetShoppingCartByEmailAsync(string customerEmail) + { + var cartKey = GetCartKey(customerEmail); + ShoppingCart? cart; + + try + { + cart = await _redisService.GetObjectAsync(cartKey); + await _redisService.KeyExpireAsync(cartKey, DefaultExpiry); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(cart ?? new ShoppingCart()); + } + + public async Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart) + { + var cartKey = GetCartKey(customerEmail); + + try + { + var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, DefaultExpiry); + if (!res) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "The shopping cart could not be updated."); + } + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(); + } + + private static string GetCartKey(string customerEmail) + { + return $"cart:{customerEmail}"; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 8233773..a02c8ee 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,13 +1,45 @@ using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { - public Task AddTicketAsync() + private readonly IShoppingCartRepository _shoppingCartRepository; + + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository) { - throw new NotImplementedException(); + _shoppingCartRepository = shoppingCartRepository; + } + + public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) + { + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + cart.Tickets.Add(new ShoppingCartNewTicket() + { + TicketTypeId = ticketTypeId, + NameOnTicket = nameOnTicket, + Seats = seats, + }); + + var updateShoppingCartResult = await _shoppingCartRepository.UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); } public Task GetTicketsAsync() diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index a4ca3a8..4ee1745 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -23,6 +23,10 @@ + + + + diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs similarity index 51% rename from TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs rename to TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs index c19904b..6521bc0 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -1,9 +1,8 @@ namespace TickAPI.Tickets.Models; -public class ShoppingCartTicket +public class ShoppingCartNewTicket { public Guid TicketTypeId { get; set; } - public Guid CustomerId { get; set; } - public string NameOnTicket { get; set; } + public string? NameOnTicket { get; set; } public string? Seats { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs new file mode 100644 index 0000000..81fc5a4 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Tickets.Models; + +public class ShoppingCartResellTicket +{ + public Guid TicketId { get; set; } + public string? NameOnTicket { get; set; } +} \ No newline at end of file From b209d463c573b2895fb8a832e7c6b1df48976752 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 9 May 2025 23:54:34 +0200 Subject: [PATCH 06/32] removed unnecessary using directive --- TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index a02c8ee..8c89917 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,5 +1,4 @@ using TickAPI.Common.Results; -using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.Tickets.Models; From 7f5f88a70e2fc3542026ea40d9c0480a96476d3b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 15 May 2025 01:32:33 +0200 Subject: [PATCH 07/32] added getting tickets from cart --- .../Abstractions/IShoppingCartService.cs | 4 +++- .../GetShoppingCartTicketsResponseDto.cs | 8 ++++++++ .../ShoppingCarts/Models/ShoppingCart.cs | 2 +- .../Services/ShoppingCartService.cs | 18 +++++++++++++++--- TickAPI/TickAPI/TickAPI.csproj | 4 ---- 5 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 12103bb..226d9f9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -1,11 +1,13 @@ using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.DTOs.Response; namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { public Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); - public Task GetTicketsAsync(); + public Task> GetTicketsAsync(string customerEmail); public Task RemoveTicketAsync(); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs new file mode 100644 index 0000000..c4c3f96 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs @@ -0,0 +1,8 @@ +using TickAPI.Tickets.Models; + +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsResponseDto( + List NewTickets, + List ResellTickets +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs index 2ed9b5f..4d480eb 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -4,6 +4,6 @@ namespace TickAPI.ShoppingCarts.Models; public class ShoppingCart { - public List Tickets { get; set; } = []; + public List NewTickets { get; set; } = []; public List ResellTickets { get; set; } = []; } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 8c89917..83e2421 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,5 +1,7 @@ using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.DTOs.Response; using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Services; @@ -24,7 +26,7 @@ public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEm var cart = getShoppingCartResult.Value!; - cart.Tickets.Add(new ShoppingCartNewTicket() + cart.NewTickets.Add(new ShoppingCartNewTicket() { TicketTypeId = ticketTypeId, NameOnTicket = nameOnTicket, @@ -41,9 +43,19 @@ public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEm return Result.Success(); } - public Task GetTicketsAsync() + public async Task> GetTicketsAsync(string customerEmail) { - throw new NotImplementedException(); + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + var result = new GetShoppingCartTicketsResponseDto(cart.NewTickets, cart.ResellTickets); + + return Result.Success(result); } public Task RemoveTicketAsync() diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index ecbd7d6..fb79365 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -25,10 +25,6 @@ - - - - From 91fb5269ab5c4804c65712454ba55e993d5e25f3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 15 May 2025 01:58:12 +0200 Subject: [PATCH 08/32] removed NameOnTicket being necessary --- ...01_MadeNameOnTicketUnnecessary.Designer.cs | 391 ++++++++++++++++++ ...50514235701_MadeNameOnTicketUnnecessary.cs | 36 ++ .../TickApiDbContextModelSnapshot.cs | 1 - TickAPI/TickAPI/Tickets/Models/Ticket.cs | 2 +- 4 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs diff --git a/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs new file mode 100644 index 0000000..e146523 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs @@ -0,0 +1,391 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250514235701_MadeNameOnTicketUnnecessary")] + partial class MadeNameOnTicketUnnecessary + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Used") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs new file mode 100644 index 0000000..3cbba3e --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class MadeNameOnTicketUnnecessary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NameOnTicket", + table: "Tickets", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NameOnTicket", + table: "Tickets", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 50fd0cc..dc78ef4 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -276,7 +276,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("NameOnTicket") - .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("OwnerId") diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 49a68ac..631f8db 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -8,7 +8,7 @@ public class Ticket public Guid Id { get; set; } public TicketType Type { get; set; } public Customer Owner { get; set; } - public string NameOnTicket { get; set; } + public string? NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } public bool Used { get; set; } From c3222dff85d72142af07031eb4432a558a72fdab Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 15 May 2025 02:02:30 +0200 Subject: [PATCH 09/32] implemented the basic version of getting tickets --- .../Abstractions/IShoppingCartService.cs | 6 +++--- .../Controllers/ShoppingCartsController.cs | 20 ++++++++++++++++--- .../Services/ShoppingCartService.cs | 6 +++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 226d9f9..8bd2aee 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -6,8 +6,8 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - public Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); - public Task> GetTicketsAsync(string customerEmail); - public Task RemoveTicketAsync(); + public Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); + public Task> GetTicketsFromCartAsync(string customerEmail); + public Task RemoveTicketFromCartAsync(); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 3f3e16a..aca5cc2 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -4,6 +4,7 @@ using TickAPI.Common.Claims.Abstractions; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Request; +using TickAPI.ShoppingCarts.DTOs.Response; namespace TickAPI.ShoppingCarts.Controllers; @@ -31,7 +32,7 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke } var email = emailResult.Value!; - var addTicketResult = await _shoppingCartService.AddNewTicketAsync(addNewTicketDto.TicketTypeId, email, + var addTicketResult = await _shoppingCartService.AddNewTicketToCartAsync(addNewTicketDto.TicketTypeId, email, addNewTicketDto.NameOnTicket, addNewTicketDto.Seats); if (addTicketResult.IsError) { @@ -43,9 +44,22 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpGet("tickets")] - public async Task GetTickets() + public async Task> GetTickets() { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var getTicketsResult = await _shoppingCartService.GetTicketsFromCartAsync(email); + if (getTicketsResult.IsError) + { + return StatusCode(getTicketsResult.StatusCode, getTicketsResult.ErrorMsg); + } + + return Ok(getTicketsResult.Value); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 83e2421..4df22fd 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -15,7 +15,7 @@ public ShoppingCartService(IShoppingCartRepository shoppingCartRepository) _shoppingCartRepository = shoppingCartRepository; } - public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) + public async Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); @@ -43,7 +43,7 @@ public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEm return Result.Success(); } - public async Task> GetTicketsAsync(string customerEmail) + public async Task> GetTicketsFromCartAsync(string customerEmail) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); @@ -58,7 +58,7 @@ public async Task> GetTicketsAsync(str return Result.Success(result); } - public Task RemoveTicketAsync() + public Task RemoveTicketFromCartAsync() { throw new NotImplementedException(); } From 95698fe539c4cbe76efbc82ee60de24787b7e326 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 16 May 2025 19:41:08 +0200 Subject: [PATCH 10/32] reworked ticket buying logic to make the pipeline from frontend more accessible --- .../Tickets/Services/TicketServiceTests.cs | 53 ++++++++++++------- TickAPI/TickAPI/Program.cs | 5 ++ .../Abstractions/IShoppingCartService.cs | 4 +- .../Controllers/ShoppingCartsController.cs | 11 ++-- .../DTOs/Request/AddNewTicketDto.cs | 3 +- .../Services/ShoppingCartService.cs | 40 +++++++++++--- .../Abstractions/ITicketTypeRepository.cs | 10 ++++ .../Repositories/TicketTypeRepository.cs | 32 +++++++++++ .../Tickets/Abstractions/ITicketService.cs | 2 + .../Tickets/Models/ShoppingCartNewTicket.cs | 3 +- .../Models/ShoppingCartResellTicket.cs | 1 - .../TickAPI/Tickets/Services/TicketService.cs | 32 ++++++++++- 12 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs create mode 100644 TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 331853f..aeaae9a 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -14,6 +14,7 @@ using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; using TickAPI.Tickets.Services; +using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; namespace TickAPI.Tests.Tickets.Services; @@ -28,6 +29,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var ticketList = new List(new Ticket[10]); var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -35,7 +37,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -53,6 +55,7 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var ticketList = new List(new Ticket[50]); var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -60,7 +63,7 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -111,6 +114,8 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(allTickets); + var ticketTypeRepositoryMock = new Mock(); + var paginatedTickets = new PaginatedData( new List { ticket1, ticket2 }, page, @@ -156,7 +161,7 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -206,6 +211,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + var ticketTypeRepositoryMock = new Mock(); + var paginatedData = new PaginatedData( new List(), page, @@ -234,7 +241,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -272,6 +279,8 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) @@ -279,7 +288,7 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -304,6 +313,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + var ticketTypeRepositoryMock = new Mock(); + var paginatedData = new PaginatedData( new List(), page, @@ -332,7 +343,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -380,9 +391,10 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT } }, }; - string email = "123@123.com"; - string scanurl = "http://localhost"; - Mock ticketRepositoryMock = new Mock(); + const string email = "123@123.com"; + const string scanurl = "http://localhost"; + var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); @@ -392,7 +404,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT var qrServiceMock = new Mock(); qrServiceMock.Setup(m => m.GenerateQrCode(scanurl)).Returns([]); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -426,18 +438,19 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldR // Arrange - Guid ticketId = Guid.NewGuid(); - string email = "123@123.com"; - string scanUrl = "http://localhost"; + var ticketId = Guid.NewGuid(); + const string email = "123@123.com"; + const string scanUrl = "http://localhost"; - Mock ticketRepositoryMock = new Mock(); + var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.GetTicketWithDetailsByIdAndEmailAsync(ticketId, email)). ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + "for this user")); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -513,6 +526,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(tickets.AsQueryable()); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(tickets.AsQueryable(), pageSize, page)) @@ -523,7 +537,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -555,6 +569,8 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(emptyTickets.AsQueryable()); + + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(emptyTickets.AsQueryable(), pageSize, page)).ReturnsAsync(paginatedResult); @@ -563,7 +579,7 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -580,9 +596,10 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() var guid = Guid.NewGuid(); var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success()); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var res = await sut.ScanTicket(guid); diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index e5cf94f..7d05eb3 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -47,6 +47,8 @@ using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; +using TickAPI.TicketTypes.Abstractions; +using TickAPI.TicketTypes.Repositories; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -125,6 +127,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add ticket type services +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 8bd2aee..5360d18 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -6,8 +6,8 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - public Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); + public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); - public Task RemoveTicketFromCartAsync(); + public Task RemoveNewTicketsFromCartAsync(); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index aca5cc2..ad73641 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -23,7 +23,7 @@ public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaims [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost] - public async Task AddTicket([FromBody] AddNewTicketDto addNewTicketDto) + public async Task AddTickets([FromBody] AddNewTicketDto addNewTicketDto) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -32,8 +32,9 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke } var email = emailResult.Value!; - var addTicketResult = await _shoppingCartService.AddNewTicketToCartAsync(addNewTicketDto.TicketTypeId, email, - addNewTicketDto.NameOnTicket, addNewTicketDto.Seats); + var addTicketResult = + await _shoppingCartService.AddNewTicketsToCartAsync(addNewTicketDto.TicketTypeId, addNewTicketDto.Amount, + email); if (addTicketResult.IsError) { return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); @@ -43,7 +44,7 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpGet("tickets")] + [HttpGet] public async Task> GetTickets() { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); @@ -63,7 +64,7 @@ public async Task> GetTickets() } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpDelete("ticket")] + [HttpDelete] public async Task DeleteTicket() { throw new NotImplementedException(); diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs index de86962..48c3d8e 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs @@ -2,6 +2,5 @@ public record AddNewTicketDto( Guid TicketTypeId, - string? NameOnTicket, - string? Seats + uint Amount ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 4df22fd..47a67cd 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,7 +1,9 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Services; @@ -9,14 +11,28 @@ namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { private readonly IShoppingCartRepository _shoppingCartRepository; + private readonly ITicketService _ticketService; - public ShoppingCartService(IShoppingCartRepository shoppingCartRepository) + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService) { _shoppingCartRepository = shoppingCartRepository; + _ticketService = ticketService; } - public async Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) + public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { + var availabilityResult = _ticketService.CheckTicketAvailabilityByTypeId(ticketTypeId, amount); + + if (availabilityResult.IsError) + { + return Result.PropagateError(availabilityResult); + } + + if (!availabilityResult.Value) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"not enough available tickets of type {ticketTypeId}"); + } + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) @@ -26,12 +42,20 @@ public async Task AddNewTicketToCartAsync(Guid ticketTypeId, string cust var cart = getShoppingCartResult.Value!; - cart.NewTickets.Add(new ShoppingCartNewTicket() + var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); + + if (existingEntry != null) { - TicketTypeId = ticketTypeId, - NameOnTicket = nameOnTicket, - Seats = seats, - }); + existingEntry.Quantity += amount; + } + else + { + cart.NewTickets.Add(new ShoppingCartNewTicket + { + TicketTypeId = ticketTypeId, + Quantity = amount + }); + } var updateShoppingCartResult = await _shoppingCartRepository.UpdateShoppingCartAsync(customerEmail, cart); @@ -58,7 +82,7 @@ public async Task> GetTicketsFromCartA return Result.Success(result); } - public Task RemoveTicketFromCartAsync() + public Task RemoveNewTicketsFromCartAsync() { throw new NotImplementedException(); } diff --git a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs new file mode 100644 index 0000000..403d5d2 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs @@ -0,0 +1,10 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.TicketTypes.Abstractions; + +public interface ITicketTypeRepository +{ + public Result GetTicketTypeById(Guid ticketTypeId); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs new file mode 100644 index 0000000..2395a62 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.TicketTypes.Abstractions; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.TicketTypes.Repositories; + +public class TicketTypeRepository : ITicketTypeRepository +{ + private readonly TickApiDbContext _tickApiDbContext; + + public TicketTypeRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + public Result GetTicketTypeById(Guid ticketTypeId) + { + var ticketType = + _tickApiDbContext.TicketTypes + .FirstOrDefault(t => t.Id == ticketTypeId); + + if (ticketType == null) + { + return Result.Failure(StatusCodes.Status404NotFound,$"ticket type with id {ticketTypeId} not found"); + } + + return Result.Success(ticketType); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index c722a8c..d32de7a 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -10,6 +10,8 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId); + public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount); public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs index 6521bc0..539c9ea 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -3,6 +3,5 @@ public class ShoppingCartNewTicket { public Guid TicketTypeId { get; set; } - public string? NameOnTicket { get; set; } - public string? Seats { get; set; } + public uint? Quantity { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs index 81fc5a4..9ddb9ac 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs @@ -3,5 +3,4 @@ public class ShoppingCartResellTicket { public Guid TicketId { get; set; } - public string? NameOnTicket { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 27b60fd..50b9e34 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -8,6 +8,7 @@ using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Filters; +using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -15,11 +16,14 @@ namespace TickAPI.Tickets.Services; public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; + private readonly ITicketTypeRepository _ticketTypeRepository; private readonly IPaginationService _paginationService; private readonly IQRCodeService _qrCodeService; - public TicketService(ITicketRepository ticketRepository, IPaginationService paginationService, IQRCodeService qrCodeService) + public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, + IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; + _ticketTypeRepository = ticketTypeRepository; _paginationService = paginationService; _qrCodeService = qrCodeService; } @@ -40,6 +44,32 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } + public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId) + { + var ticketTypeResult = _ticketTypeRepository.GetTicketTypeById(ticketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + return GetNumberOfAvailableTicketsByType(ticketTypeResult.Value!); + } + + public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount) + { + var numberOfTicketsResult = GetNumberOfAvailableTicketsByTypeId(ticketTypeId); + + if (numberOfTicketsResult.IsError) + { + return Result.PropagateError(numberOfTicketsResult); + } + + var availableAmount = numberOfTicketsResult.Value!; + + return availableAmount >= amount ? Result.Success(true) : Result.Success(false); + } + public async Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize) { var eventTickets = _ticketRepository.GetTicketsByEventId(eventId); From 3edf2d6f6526b8f7460de382952b7dad83fc719b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 16 May 2025 20:53:48 +0200 Subject: [PATCH 11/32] relegated some of the ticket adding logic to the repository --- .../Abstractions/IShoppingCartRepository.cs | 1 + .../Repositories/ShoppingCartRepository.cs | 37 +++++++++++++++++++ .../Services/ShoppingCartService.cs | 35 ++++-------------- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index af2d2d8..afd8f2b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -8,4 +8,5 @@ public interface IShoppingCartRepository { public Task> GetShoppingCartByEmailAsync(string customerEmail); public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); + public Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 6cda1c9..aa41651 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -3,6 +3,7 @@ using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Models; +using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Repositories; @@ -54,6 +55,42 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping return Result.Success(); } + public async Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) + { + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); + + if (existingEntry != null) + { + existingEntry.Quantity += amount; + } + else + { + cart.NewTickets.Add(new ShoppingCartNewTicket + { + TicketTypeId = ticketTypeId, + Quantity = amount + }); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 47a67cd..d069cb9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -21,6 +21,11 @@ public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITick public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { + if (amount <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); + } + var availabilityResult = _ticketService.CheckTicketAvailabilityByTypeId(ticketTypeId, amount); if (availabilityResult.IsError) @@ -32,36 +37,12 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun { return Result.Failure(StatusCodes.Status400BadRequest, $"not enough available tickets of type {ticketTypeId}"); } - - var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); - - if (getShoppingCartResult.IsError) - { - return Result.PropagateError(getShoppingCartResult); - } - - var cart = getShoppingCartResult.Value!; - - var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); - - if (existingEntry != null) - { - existingEntry.Quantity += amount; - } - else - { - cart.NewTickets.Add(new ShoppingCartNewTicket - { - TicketTypeId = ticketTypeId, - Quantity = amount - }); - } - var updateShoppingCartResult = await _shoppingCartRepository.UpdateShoppingCartAsync(customerEmail, cart); + var addTicketToCartResult = await _shoppingCartRepository.AddNewTicketToCartAsync(customerEmail, ticketTypeId, amount); - if (updateShoppingCartResult.IsError) + if (addTicketToCartResult.IsError) { - return Result.PropagateError(updateShoppingCartResult); + return Result.PropagateError(addTicketToCartResult); } return Result.Success(); From 84dd97f923a77d5f8f1d8984cd4c686f2e4c74e5 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 16 May 2025 21:59:58 +0200 Subject: [PATCH 12/32] started updating some ticket retrieval logic --- ...gCartTicketsNewTicketDetailsResponseDto.cs | 9 ++++++ ...rtTicketsResellTicketDetailsResponseDto.cs | 9 ++++++ .../GetShoppingCartTicketsResponseDto.cs | 8 ++--- .../Mappers/ShoppingCartMapper.cs | 32 +++++++++++++++++++ .../Tickets/Models/ShoppingCartNewTicket.cs | 2 +- 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs new file mode 100644 index 0000000..408bb51 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsNewTicketDetailsResponseDto( + Guid TicketId, + string EventName, + string TicketType, + string OrganizerName, + uint Quantity +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs new file mode 100644 index 0000000..e014f96 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsResellTicketDetailsResponseDto( + Guid TicketId, + string EventName, + string TicketType, + string OrganizerName, + string OriginalOwnerEmail +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs index c4c3f96..3f553d6 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs @@ -1,8 +1,6 @@ -using TickAPI.Tickets.Models; - -namespace TickAPI.ShoppingCarts.DTOs.Response; +namespace TickAPI.ShoppingCarts.DTOs.Response; public record GetShoppingCartTicketsResponseDto( - List NewTickets, - List ResellTickets + List NewTickets, + List ResellTickets ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs new file mode 100644 index 0000000..95478d3 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -0,0 +1,32 @@ +using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.ShoppingCarts.Mappers; + +public static class ShoppingCartMapper +{ + public static GetShoppingCartTicketsNewTicketDetailsResponseDto + MapTicketTypeToGetShoppingCartTicketsNewTicketDetailsResponseDto(TicketType type, uint quantity) + { + return new GetShoppingCartTicketsNewTicketDetailsResponseDto( + type.Id, + type.Event.Name, + type.Description, + type.Event.Organizer.DisplayName, + quantity + ); + } + + public static GetShoppingCartTicketsResellTicketDetailsResponseDto + MapTicketToGetShoppingCartTicketsResellTicketDetailsResponseDto(Ticket ticket) + { + return new GetShoppingCartTicketsResellTicketDetailsResponseDto( + ticket.Id, + ticket.Type.Event.Name, + ticket.Type.Description, + ticket.Type.Event.Organizer.DisplayName, + ticket.Owner.Email + ); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs index 539c9ea..0ed97d9 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -3,5 +3,5 @@ public class ShoppingCartNewTicket { public Guid TicketTypeId { get; set; } - public uint? Quantity { get; set; } + public uint Quantity { get; set; } } \ No newline at end of file From ab228bedd5ea43e05eecc26d3a291544a943127e Mon Sep 17 00:00:00 2001 From: kubapoke Date: Tue, 20 May 2025 22:52:24 +0200 Subject: [PATCH 13/32] small aesthetic change in TicketRepository.cs --- .../TickAPI/Tickets/Repositories/TicketRepository.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index e5abb5e..162d9f1 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -41,9 +41,13 @@ public IQueryable GetTicketsByCustomerEmail(string email) public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email) { - var ticket = await _tickApiDbContext.Tickets.Include(t => t.Type).Include(t => t.Type.Event) - .Include(t => t.Type.Event.Organizer).Include(t => t.Type.Event.Address) - .Where(t => (t.Id == id && t.Owner.Email == email)).FirstOrDefaultAsync(); + var ticket = await _tickApiDbContext.Tickets + .Include(t => t.Type) + .Include(t => t.Type.Event) + .Include(t => t.Type.Event.Organizer) + .Include(t => t.Type.Event.Address) + .Where(t => (t.Id == id && t.Owner.Email == email)) + .FirstOrDefaultAsync(); if (ticket == null) { return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); From 0f0e843b05211417ad5c6ac48e98cbc638f2d296 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Tue, 20 May 2025 23:31:15 +0200 Subject: [PATCH 14/32] updated cart retrieval logic to return more data --- ...gCartTicketsNewTicketDetailsResponseDto.cs | 2 +- .../Services/ShoppingCartService.cs | 24 +++++++++++++++++-- .../Abstractions/ITicketTypeRepository.cs | 2 +- .../Repositories/TicketTypeRepository.cs | 8 ++++--- .../Tickets/Abstractions/ITicketService.cs | 5 ++-- .../TickAPI/Tickets/Services/TicketService.cs | 20 ++++++++++++---- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs index 408bb51..e0c4e9b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -1,7 +1,7 @@ namespace TickAPI.ShoppingCarts.DTOs.Response; public record GetShoppingCartTicketsNewTicketDetailsResponseDto( - Guid TicketId, + Guid TicketTypeId, string EventName, string TicketType, string OrganizerName, diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index d069cb9..f83cfe1 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -3,6 +3,7 @@ using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.ShoppingCarts.Mappers; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; @@ -26,7 +27,7 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); } - var availabilityResult = _ticketService.CheckTicketAvailabilityByTypeId(ticketTypeId, amount); + var availabilityResult = await _ticketService.CheckTicketAvailabilityByTypeIdAsync(ticketTypeId, amount); if (availabilityResult.IsError) { @@ -58,7 +59,26 @@ public async Task> GetTicketsFromCartA } var cart = getShoppingCartResult.Value!; - var result = new GetShoppingCartTicketsResponseDto(cart.NewTickets, cart.ResellTickets); + + var newTickets = new List(); + + foreach (var ticket in cart.NewTickets) + { + var newTicketResult = await _ticketService.GetTicketTypeByIdAsync(ticket.TicketTypeId); + + if (newTicketResult.IsError) + { + return Result.PropagateError(newTicketResult); + } + + var newTicket = + ShoppingCartMapper.MapTicketTypeToGetShoppingCartTicketsNewTicketDetailsResponseDto( + newTicketResult.Value!, ticket.Quantity); + + newTickets.Add(newTicket); + } + + var result = new GetShoppingCartTicketsResponseDto(newTickets, []); return Result.Success(result); } diff --git a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs index 403d5d2..f9fba2f 100644 --- a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs +++ b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs @@ -6,5 +6,5 @@ namespace TickAPI.TicketTypes.Abstractions; public interface ITicketTypeRepository { - public Result GetTicketTypeById(Guid ticketTypeId); + public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs index 2395a62..929e471 100644 --- a/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs +++ b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs @@ -16,11 +16,13 @@ public TicketTypeRepository(TickApiDbContext tickApiDbContext) _tickApiDbContext = tickApiDbContext; } - public Result GetTicketTypeById(Guid ticketTypeId) + public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) { - var ticketType = + var ticketType = await _tickApiDbContext.TicketTypes - .FirstOrDefault(t => t.Id == ticketTypeId); + .Include(t => t.Event) + .Include(t => t.Event.Organizer) + .FirstOrDefaultAsync(t => t.Id == ticketTypeId); if (ticketType == null) { diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index d32de7a..c396a0d 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -10,8 +10,8 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); - public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId); - public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount); + public Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId); + public Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount); public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, @@ -19,4 +19,5 @@ public Task>> GetTicketsForCustome public Task ScanTicket(Guid ticketGuid); public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); + public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 50b9e34..b24b704 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -44,9 +44,9 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } - public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId) + public async Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId) { - var ticketTypeResult = _ticketTypeRepository.GetTicketTypeById(ticketTypeId); + var ticketTypeResult = await _ticketTypeRepository.GetTicketTypeByIdAsync(ticketTypeId); if (ticketTypeResult.IsError) { @@ -56,9 +56,9 @@ public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId) return GetNumberOfAvailableTicketsByType(ticketTypeResult.Value!); } - public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount) + public async Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount) { - var numberOfTicketsResult = GetNumberOfAvailableTicketsByTypeId(ticketTypeId); + var numberOfTicketsResult = await GetNumberOfAvailableTicketsByTypeIdAsync(ticketTypeId); if (numberOfTicketsResult.IsError) { @@ -135,6 +135,18 @@ public async Task> GetTicketDetailsAsync(Gui return Result.Success(ticketDetails); } + public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) + { + var ticketTypeResult = await _ticketTypeRepository.GetTicketTypeByIdAsync(ticketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + return Result.Success(ticketTypeResult.Value!); + } + public async Task ScanTicket(Guid ticketGuid) { var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); From 5e3b95f56e44f16458992fd1e6efa4e364970ff0 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Tue, 20 May 2025 23:58:45 +0200 Subject: [PATCH 15/32] added functionality for removal of tickets from the shopping cart --- .../Abstractions/IShoppingCartRepository.cs | 3 +- .../Abstractions/IShoppingCartService.cs | 2 +- .../Controllers/ShoppingCartsController.cs | 23 ++++++-- .../DTOs/Request/RemoveNewTicketDto.cs | 6 ++ .../Repositories/ShoppingCartRepository.cs | 55 ++++++++++++++++++- .../Services/ShoppingCartService.cs | 22 ++++---- 6 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index afd8f2b..9a1607d 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -8,5 +8,6 @@ public interface IShoppingCartRepository { public Task> GetShoppingCartByEmailAsync(string customerEmail); public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); - public Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); + public Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); + public Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 5360d18..aa48e4b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -8,6 +8,6 @@ public interface IShoppingCartService { public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); - public Task RemoveNewTicketsFromCartAsync(); + public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index ad73641..fb9c6fe 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -23,7 +23,7 @@ public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaims [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost] - public async Task AddTickets([FromBody] AddNewTicketDto addNewTicketDto) + public async Task AddTickets([FromBody] AddNewTicketDto addTicketDto) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -33,7 +33,7 @@ public async Task AddTickets([FromBody] AddNewTicketDto addNewTick var email = emailResult.Value!; var addTicketResult = - await _shoppingCartService.AddNewTicketsToCartAsync(addNewTicketDto.TicketTypeId, addNewTicketDto.Amount, + await _shoppingCartService.AddNewTicketsToCartAsync(addTicketDto.TicketTypeId, addTicketDto.Amount, email); if (addTicketResult.IsError) { @@ -65,9 +65,24 @@ public async Task> GetTickets() [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpDelete] - public async Task DeleteTicket() + public async Task RemoveTickets([FromBody] RemoveNewTicketDto removeTicketDto) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var addTicketResult = + await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketTypeId, removeTicketDto.Amount, + email); + if (addTicketResult.IsError) + { + return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); + } + + return Ok(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs new file mode 100644 index 0000000..012c376 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record RemoveNewTicketDto( + Guid TicketTypeId, + uint Amount +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index aa41651..7e8f46e 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -44,7 +44,7 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, DefaultExpiry); if (!res) { - return Result.Failure(StatusCodes.Status500InternalServerError, "The shopping cart could not be updated."); + return Result.Failure(StatusCodes.Status500InternalServerError, "the shopping cart could not be updated"); } } catch (Exception e) @@ -55,8 +55,13 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping return Result.Success(); } - public async Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) + public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) { + if (amount <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); + } + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) @@ -91,6 +96,52 @@ public async Task AddNewTicketToCartAsync(string customerEmail, Guid tic return Result.Success(); } + public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount) + { + if (amount <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, "amount of removed tickets must be greater than 0"); + } + + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); + + if (existingEntry is null) + { + return Result.Failure(StatusCodes.Status404NotFound, "the shopping cart does not contain a ticket of this type"); + } + + if (existingEntry.Quantity < amount) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"the shopping cart does not contain {amount} tickets of this type"); + } + + existingEntry.Quantity -= amount; + + if (existingEntry.Quantity == 0) + { + cart.NewTickets.Remove(existingEntry); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index f83cfe1..8e3f6f7 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -22,11 +22,6 @@ public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITick public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { - if (amount <= 0) - { - return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); - } - var availabilityResult = await _ticketService.CheckTicketAvailabilityByTypeIdAsync(ticketTypeId, amount); if (availabilityResult.IsError) @@ -39,11 +34,11 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun return Result.Failure(StatusCodes.Status400BadRequest, $"not enough available tickets of type {ticketTypeId}"); } - var addTicketToCartResult = await _shoppingCartRepository.AddNewTicketToCartAsync(customerEmail, ticketTypeId, amount); + var addTicketsToCartResult = await _shoppingCartRepository.AddNewTicketsToCartAsync(customerEmail, ticketTypeId, amount); - if (addTicketToCartResult.IsError) + if (addTicketsToCartResult.IsError) { - return Result.PropagateError(addTicketToCartResult); + return Result.PropagateError(addTicketsToCartResult); } return Result.Success(); @@ -83,9 +78,16 @@ public async Task> GetTicketsFromCartA return Result.Success(result); } - public Task RemoveNewTicketsFromCartAsync() + public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { - throw new NotImplementedException(); + var removeTicketsFromCartResult = await _shoppingCartRepository.RemoveNewTicketsFromCartAsync(customerEmail, ticketTypeId, amount); + + if (removeTicketsFromCartResult.IsError) + { + return Result.PropagateError(removeTicketsFromCartResult); + } + + return Result.Success(); } public Task CheckoutAsync() From e877eae225e0dd63a0ab13244a696d3fa90883c7 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 01:13:44 +0200 Subject: [PATCH 16/32] implemented getting cart due amount and part of the checkout process --- .../Abstractions/IShoppingCartService.cs | 8 +- .../Controllers/ShoppingCartsController.cs | 21 +++++ ...gCartTicketsNewTicketDetailsResponseDto.cs | 3 +- ...rtTicketsResellTicketDetailsResponseDto.cs | 3 +- .../Mappers/ShoppingCartMapper.cs | 6 +- .../Services/ShoppingCartService.cs | 77 ++++++++++++++++++- 6 files changed, 108 insertions(+), 10 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index aa48e4b..12f914c 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.DTOs.Response; @@ -9,5 +10,8 @@ public interface IShoppingCartService public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); - public Task CheckoutAsync(); + public Task> GetDueAmountAsync(string customerEmail, 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 fb9c6fe..c477810 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -84,6 +84,27 @@ await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketT return Ok(); } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("due/{currency}")] + public async Task> GetDueAmount([FromRoute] string currency) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email, currency); + + if (dueAmountResult.IsError) + { + return StatusCode(dueAmountResult.StatusCode, dueAmountResult.ErrorMsg); + } + + return Ok(dueAmountResult.Value); + } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs index e0c4e9b..f85e035 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -5,5 +5,6 @@ public record GetShoppingCartTicketsNewTicketDetailsResponseDto( string EventName, string TicketType, string OrganizerName, - uint Quantity + uint Quantity, + decimal UnitPrice ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs index e014f96..f2aa4d0 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -5,5 +5,6 @@ public record GetShoppingCartTicketsResellTicketDetailsResponseDto( string EventName, string TicketType, string OrganizerName, - string OriginalOwnerEmail + string OriginalOwnerEmail, + decimal Price ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs index 95478d3..ae3d653 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -14,7 +14,8 @@ public static GetShoppingCartTicketsNewTicketDetailsResponseDto type.Event.Name, type.Description, type.Event.Organizer.DisplayName, - quantity + quantity, + type.Price ); } @@ -26,7 +27,8 @@ public static GetShoppingCartTicketsResellTicketDetailsResponseDto ticket.Type.Event.Name, ticket.Type.Description, ticket.Type.Event.Organizer.DisplayName, - ticket.Owner.Email + ticket.Owner.Email, + ticket.Type.Price ); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 8e3f6f7..a53a9d1 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,4 +1,6 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Payment.Abstractions; +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; @@ -13,11 +15,14 @@ public class ShoppingCartService : IShoppingCartService { private readonly IShoppingCartRepository _shoppingCartRepository; private readonly ITicketService _ticketService; + private readonly IPaymentGatewayService _paymentGatewayService; - public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService) + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService, + IPaymentGatewayService paymentGatewayService) { _shoppingCartRepository = shoppingCartRepository; _ticketService = ticketService; + _paymentGatewayService = paymentGatewayService; } public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) @@ -73,6 +78,8 @@ public async Task> GetTicketsFromCartA newTickets.Add(newTicket); } + // TODO: Add resell ticket parsing + var result = new GetShoppingCartTicketsResponseDto(newTickets, []); return Result.Success(result); @@ -90,8 +97,70 @@ public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint return Result.Success(); } - public Task CheckoutAsync() + public async Task> GetDueAmountAsync(string customerEmail, string currency) { - throw new NotImplementedException(); + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + decimal total = 0; + + foreach (var newTicket in cart.NewTickets) + { + var ticketTypeResult = await _ticketService.GetTicketTypeByIdAsync(newTicket.TicketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + var ticketType = ticketTypeResult.Value!; + + if (ticketType.Currency == currency) + { + total += newTicket.Quantity * ticketType.Price; + } + } + + // TODO: Add resell tickets to the calculations + + return Result.Success(total); + } + + public async Task> CheckoutAsync(string customerEmail, decimal amount, string currency, + string cardNumber, string cardExpiry, string cvv) + { + var dueAmountResult = await GetDueAmountAsync(customerEmail, currency); + + if (dueAmountResult.IsError) + { + return Result.PropagateError(dueAmountResult); + } + + var dueAmount = dueAmountResult.Value; + + if (dueAmount != amount) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"the given amount {amount} {currency} is different than the expected amount of {dueAmount} {currency}"); + } + + var paymentResult = + await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currency, cardNumber, cardExpiry, + cvv, false)); + + if (paymentResult.IsError) + { + return Result.PropagateError(paymentResult); + } + + var payment = paymentResult.Value!; + + return Result.Success(payment); } } \ No newline at end of file From 68d9662eee0e21070df487eb283f4c57b0b3f8d2 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 01:23:32 +0200 Subject: [PATCH 17/32] added payment endpoint --- .../Controllers/ShoppingCartsController.cs | 21 +++++++++++++++++-- .../ShoppingCarts/DTOs/Request/CheckoutDto.cs | 9 ++++++++ .../DTOs/Response/CheckoutResponseDto.cs | 6 ++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index c477810..a869b6a 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -108,8 +108,25 @@ public async Task> GetDueAmount([FromRoute] string currenc [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] - public async Task Checkout() + public async Task> Checkout([FromBody] CheckoutDto checkoutDto) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var checkoutResult = await _shoppingCartService.CheckoutAsync(email, checkoutDto.Amount, checkoutDto.Currency, + checkoutDto.CardNumber, checkoutDto.CardExpiry, checkoutDto.Cvv); + + if (checkoutResult.IsError) + { + return StatusCode(checkoutResult.StatusCode, checkoutResult.ErrorMsg); + } + + var checkout = checkoutResult.Value!; + + return Ok(new CheckoutResponseDto(checkout.TransactionId, checkout.Status)); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs new file mode 100644 index 0000000..aae0e68 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record CheckoutDto( + decimal Amount, + string Currency, + string CardNumber, + string CardExpiry, + string Cvv +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs new file mode 100644 index 0000000..6a4b4f2 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record CheckoutResponseDto( + string TransactionId, + string Status +); \ No newline at end of file From 05da8e5eb3d1e3ab0875351290f4103bfef545a8 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 01:46:06 +0200 Subject: [PATCH 18/32] changed due amount calculations --- .../Abstractions/IShoppingCartService.cs | 2 +- .../Controllers/ShoppingCartsController.cs | 6 +-- .../Services/ShoppingCartService.cs | 45 ++++++++++++++----- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 12f914c..47add30 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -10,7 +10,7 @@ public interface IShoppingCartService public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); - public Task> GetDueAmountAsync(string customerEmail, string currency); + public Task>> GetDueAmountAsync(string customerEmail); public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv); diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index a869b6a..1780d8d 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -86,8 +86,8 @@ await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketT } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpGet("due/{currency}")] - public async Task> GetDueAmount([FromRoute] string currency) + [HttpGet("due")] + public async Task>> GetDueAmount() { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -96,7 +96,7 @@ public async Task> GetDueAmount([FromRoute] string currenc } var email = emailResult.Value!; - var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email, currency); + var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email); if (dueAmountResult.IsError) { diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index a53a9d1..c671d98 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Payment.Abstractions; +using Google.Apis.Auth.OAuth2.Web; +using TickAPI.Common.Payment.Abstractions; using TickAPI.Common.Payment.Models; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; @@ -97,18 +98,18 @@ public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint return Result.Success(); } - public async Task> GetDueAmountAsync(string customerEmail, string currency) + public async Task>> GetDueAmountAsync(string customerEmail) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) { - return Result.PropagateError(getShoppingCartResult); + return Result>.PropagateError(getShoppingCartResult); } var cart = getShoppingCartResult.Value!; - decimal total = 0; + Dictionary dueAmount = new Dictionary(); foreach (var newTicket in cart.NewTickets) { @@ -116,34 +117,44 @@ public async Task> GetDueAmountAsync(string customerEmail, strin if (ticketTypeResult.IsError) { - return Result.PropagateError(ticketTypeResult); + return Result>.PropagateError(ticketTypeResult); } var ticketType = ticketTypeResult.Value!; - - if (ticketType.Currency == currency) + + if(dueAmount.ContainsKey(ticketType.Currency)) + { + dueAmount[ticketType.Currency] += newTicket.Quantity * ticketType.Price; + } + else { - total += newTicket.Quantity * ticketType.Price; + dueAmount.Add(ticketType.Currency, newTicket.Quantity * ticketType.Price); } } // TODO: Add resell tickets to the calculations - return Result.Success(total); + return Result>.Success(dueAmount); } public async Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv) { - var dueAmountResult = await GetDueAmountAsync(customerEmail, currency); + var dueAmountResult = await GetDueAmountAsync(customerEmail); if (dueAmountResult.IsError) { return Result.PropagateError(dueAmountResult); } - var dueAmount = dueAmountResult.Value; + var currencyExists = dueAmountResult.Value!.TryGetValue(currency, out var dueAmount); + if (!currencyExists) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"no tickets paid in {currency} found in cart"); + } + if (dueAmount != amount) { return Result.Failure(StatusCodes.Status400BadRequest, @@ -158,9 +169,21 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc { return Result.PropagateError(paymentResult); } + + var generateTicketsResult = await GenerateBoughtTicketsAsync(customerEmail, currency); + if (generateTicketsResult.IsError) + { + return Result.PropagateError(generateTicketsResult); + } + var payment = paymentResult.Value!; return Result.Success(payment); } + + private static async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) + { + throw new NotImplementedException(); + } } \ No newline at end of file From 2338c8e760f80c95a76ab5de4b77ddc2a4747295 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 18:26:34 +0200 Subject: [PATCH 19/32] implemented adding created tickets to database --- .../Abstractions/IShoppingCartService.cs | 3 +- .../Services/ShoppingCartService.cs | 56 +++++++++++++++++-- .../Tickets/Abstractions/ITicketRepository.cs | 2 + .../Tickets/Abstractions/ITicketService.cs | 4 ++ .../Tickets/Repositories/TicketRepository.cs | 17 ++++++ .../TickAPI/Tickets/Services/TicketService.cs | 26 ++++++++- 6 files changed, 99 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 47add30..787e62e 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -12,6 +12,5 @@ public interface IShoppingCartService public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task>> GetDueAmountAsync(string customerEmail); public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, - string cardNumber, - string cardExpiry, string cvv); + string cardNumber, string cardExpiry, string cvv); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index c671d98..e459c54 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -3,25 +3,29 @@ using TickAPI.Common.Payment.Models; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Abstractions; using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; using TickAPI.ShoppingCarts.Mappers; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Abstractions; namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { private readonly IShoppingCartRepository _shoppingCartRepository; + private readonly ICustomerRepository _customerRepository; private readonly ITicketService _ticketService; private readonly IPaymentGatewayService _paymentGatewayService; - public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService, - IPaymentGatewayService paymentGatewayService) + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ICustomerRepository customerRepository, + ITicketService ticketService, IPaymentGatewayService paymentGatewayService) { _shoppingCartRepository = shoppingCartRepository; + _customerRepository = customerRepository; _ticketService = ticketService; _paymentGatewayService = paymentGatewayService; } @@ -182,8 +186,52 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc return Result.Success(payment); } - private static async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) + private async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) { - throw new NotImplementedException(); + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var getCustomerResult = await _customerRepository.GetCustomerByEmailAsync(customerEmail); + + if (getCustomerResult.IsError) + { + return Result.PropagateError(getCustomerResult); + } + + var owner = getCustomerResult.Value!; + + foreach (var ticket in cart.NewTickets) + { + var ticketTypeResult = await _ticketService.GetTicketTypeByIdAsync(ticket.TicketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + var type = ticketTypeResult.Value!; + + if (type.Currency == currency) + { + for (var i = 0; i < ticket.Quantity; i++) + { + // TODO: add seats/name on ticket setting + var createTicketResult = await _ticketService.CreateTicketAsync(type, owner); + + if (createTicketResult.IsError) + { + return Result.PropagateError(createTicketResult); + } + } + } + } + + return Result.Success(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 6290950..877b9cd 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -1,5 +1,6 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -12,4 +13,5 @@ public interface ITicketRepository public IQueryable GetTicketsByEventId(Guid eventId); public IQueryable GetTicketsByCustomerEmail(string email); public Task MarkTicketAsUsed(Guid id); + public Task AddTicketAsync(Ticket ticket); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index c396a0d..8b75bd3 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,6 +1,7 @@ using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; @@ -20,4 +21,7 @@ public Task>> GetTicketsForCustome public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); + + public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, + string? seats = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 162d9f1..57774e6 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -2,6 +2,7 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; +using TickAPI.Customers.Models; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -70,4 +71,20 @@ public async Task MarkTicketAsUsed(Guid id) await _tickApiDbContext.SaveChangesAsync(); return Result.Success(); } + + 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, + "The ticket you are trying to buy has already reached its max count"); + } + + _tickApiDbContext.Tickets.Add(ticket); + await _tickApiDbContext.SaveChangesAsync(); + + return Result.Success(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index b24b704..e141438 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -4,10 +4,13 @@ using TickAPI.Common.QR.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Filters; +using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; @@ -19,7 +22,8 @@ public class TicketService : ITicketService private readonly ITicketTypeRepository _ticketTypeRepository; private readonly IPaginationService _paginationService; private readonly IQRCodeService _qrCodeService; - public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, + + public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; @@ -147,11 +151,27 @@ public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) return Result.Success(ticketTypeResult.Value!); } + public async Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, + string? seats = null) + { + var ticket = new Ticket + { + Type = type, + Owner = owner, + NameOnTicket = nameOnTicket, + Seats = seats, + ForResell = false, + Used = false, + }; + + var addTicketResult = await _ticketRepository.AddTicketAsync(ticket); + + return addTicketResult; + } + public async Task ScanTicket(Guid ticketGuid) { var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); return res; } - - } \ No newline at end of file From b0703debf6489fc95226fdcd2f062cbde47bf79f Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 18:40:22 +0200 Subject: [PATCH 20/32] tickets removed from cart after purchase --- .../ShoppingCarts/Services/ShoppingCartService.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index e459c54..b4032e5 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -175,6 +175,7 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc } var generateTicketsResult = await GenerateBoughtTicketsAsync(customerEmail, currency); + // TODO: Add passing ownership of resell tickets if (generateTicketsResult.IsError) { @@ -205,6 +206,7 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri } var owner = getCustomerResult.Value!; + var removals = new List<(Guid id, uint amount)>(); foreach (var ticket in cart.NewTickets) { @@ -219,6 +221,8 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri if (type.Currency == currency) { + removals.Add((ticket.TicketTypeId, ticket.Quantity)); + for (var i = 0; i < ticket.Quantity; i++) { // TODO: add seats/name on ticket setting @@ -232,6 +236,16 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri } } + foreach (var (id, amount) in removals) + { + var removalResult = await RemoveNewTicketsFromCartAsync(id, amount, customerEmail); + + if (removalResult.IsError) + { + return Result.PropagateError(removalResult); + } + } + return Result.Success(); } } \ No newline at end of file From fe4cf05d84dd5b7bb5afd9366e9bab895c8b0a86 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 19:05:49 +0200 Subject: [PATCH 21/32] added methods for manipulating ticket type counters --- .../Abstractions/IShoppingCartRepository.cs | 5 + .../Repositories/ShoppingCartRepository.cs | 100 ++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index 9a1607d..d8be591 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -10,4 +10,9 @@ public interface IShoppingCartRepository public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); public Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); public Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount); + public Task> GetAmountOfTicketTypeAsync(Guid ticketTypeId); + public Task SetAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); + public Task> IncrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); + public Task> DecrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); + public Task RemoveAmountOfTicketTypeAsync(Guid ticketTypeId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 7e8f46e..2b15778 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -142,8 +142,108 @@ public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Gu return Result.Success(); } + public async Task> GetAmountOfTicketTypeAsync(Guid ticketTypeId) + { + long? amount; + + try + { + amount = await _redisService.GetLongValueAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (amount is null) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be retrieved"); + } + + return Result.Success(amount.Value); + } + + public async Task SetAmountOfTicketTypeAsync(Guid ticketTypeId, long amount) + { + bool success; + + try + { + success = await _redisService.SetLongValueAsync(GetAmountKey(ticketTypeId), amount); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (!success) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be updated"); + } + + return Result.Success(); + } + + public async Task> IncrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount) + { + long? newAmount; + + try + { + newAmount = await _redisService.IncrementValueAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(newAmount.Value); + } + + public async Task> DecrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount) + { + long? newAmount; + + try + { + newAmount = await _redisService.DecrementValueAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(newAmount.Value); + } + + public async Task RemoveAmountOfTicketTypeAsync(Guid ticketTypeId) + { + bool success; + + try + { + success = await _redisService.DeleteKeyAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (!success) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be updated"); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; } + + private static string GetAmountKey(Guid ticketTypeId) + { + return $"amount:{ticketTypeId}"; + } } \ No newline at end of file From c1097b366b7721c3be2f814f4b83d51cc1e4e4aa Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 20:25:43 +0200 Subject: [PATCH 22/32] added reserved ticket counting --- .../Events/Services/EventServiceTests.cs | 2 +- .../Tickets/Services/TicketServiceTests.cs | 53 +++++++++++++------ .../TickAPI/Events/Services/EventService.cs | 22 ++++++-- .../Repositories/ShoppingCartRepository.cs | 14 +++++ .../Tickets/Abstractions/ITicketService.cs | 2 +- .../TickAPI/Tickets/Services/TicketService.cs | 19 +++++-- 6 files changed, 87 insertions(+), 25 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 34b23ca..a4ac30b 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -631,7 +631,7 @@ public async Task GetEventDetailsAsync_WhenSuccessful_ShouldReturnEventDetails() .ReturnsAsync(Result.Success(@event)); ticketServiceMock - .Setup(m => m.GetNumberOfAvailableTicketsByType(It.IsAny())) + .Setup(m => m.GetNumberOfAvailableTicketsByTypeAsync(It.IsAny())) .Returns((TicketType input) => Result.Success((uint)(input.Price / 10)) ); diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index aeaae9a..1c100de 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -10,6 +10,7 @@ using TickAPI.Customers.Models; using TickAPI.Events.Models; using TickAPI.Organizers.Models; +using TickAPI.ShoppingCarts.Abstractions; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; @@ -22,7 +23,7 @@ namespace TickAPI.Tests.Tickets.Services; public class TicketServiceTests { [Fact] - public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorrectNumberOfTickets() + public async Task GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorrectNumberOfTickets() { // Arrange var type = new TicketType { MaxCount = 30 }; @@ -30,6 +31,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var ticketRepositoryMock = new Mock(); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -37,10 +39,11 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var result = sut.GetNumberOfAvailableTicketsByType(type); + var result = await sut.GetNumberOfAvailableTicketsByTypeAsync(type); // Assert Assert.True(result.IsSuccess); @@ -48,7 +51,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr } [Fact] - public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_ShouldReturnError() + public async Task GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_ShouldReturnError() { // Arrange var type = new TicketType { MaxCount = 30 }; @@ -56,6 +59,7 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var ticketRepositoryMock = new Mock(); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -63,10 +67,11 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var result = sut.GetNumberOfAvailableTicketsByType(type); + var result = await sut.GetNumberOfAvailableTicketsByTypeAsync(type); // Assert Assert.True(result.IsError); @@ -115,6 +120,7 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() .Returns(allTickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginatedTickets = new PaginatedData( new List { ticket1, ticket2 }, @@ -161,7 +167,8 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -212,6 +219,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm .Returns(tickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginatedData = new PaginatedData( new List(), @@ -241,7 +249,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -281,6 +290,7 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr .Returns(tickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) @@ -288,7 +298,8 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -314,6 +325,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp .Returns(tickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginatedData = new PaginatedData( new List(), @@ -343,7 +355,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -395,6 +408,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT const string scanurl = "http://localhost"; var ticketRepositoryMock = new Mock(); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); @@ -404,7 +418,8 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT var qrServiceMock = new Mock(); qrServiceMock.Setup(m => m.GenerateQrCode(scanurl)).Returns([]); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -447,10 +462,12 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldR ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + "for this user")); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -527,6 +544,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(tickets.AsQueryable()); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(tickets.AsQueryable(), pageSize, page)) @@ -537,7 +555,8 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -571,6 +590,7 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(emptyTickets.AsQueryable()); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(emptyTickets.AsQueryable(), pageSize, page)).ReturnsAsync(paginatedResult); @@ -579,7 +599,8 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -597,9 +618,11 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success()); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var res = await sut.ScanTicket(guid); diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 39f7807..350a4e0 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -133,10 +133,24 @@ public async Task> GetEventDetailsAsync(Guid ? ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList() : new List(); - var ticketTypes = ev.TicketTypes.Count > 0 - ? ev.TicketTypes.Select((t) => new GetEventDetailsResponseTicketTypeDto(t.Id, t.Description, t.Price, - t.Currency, t.AvailableFrom, _ticketService.GetNumberOfAvailableTicketsByType(t).Value)).ToList() - : new List(); + var ticketTypes = new List(); + + if (ev.TicketTypes.Count > 0) + { + foreach (var t in ev.TicketTypes) + { + var availableCount = await _ticketService.GetNumberOfAvailableTicketsByTypeAsync(t); + + ticketTypes.Add(new GetEventDetailsResponseTicketTypeDto( + t.Id, + t.Description, + t.Price, + t.Currency, + t.AvailableFrom, + availableCount.Value + )); + } + } var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 2b15778..8699445 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -86,6 +86,13 @@ public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ti }); } + var incrementTicketAmountResult = await IncrementAmountOfTicketTypeAsync(ticketTypeId, amount); + + if (incrementTicketAmountResult.IsError) + { + return Result.PropagateError(incrementTicketAmountResult); + } + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); if (updateShoppingCartResult.IsError) @@ -131,6 +138,13 @@ public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Gu { cart.NewTickets.Remove(existingEntry); } + + var decrementTicketAmountResult = await DecrementAmountOfTicketTypeAsync(ticketTypeId, amount); + + if (decrementTicketAmountResult.IsError) + { + return Result.PropagateError(decrementTicketAmountResult); + } var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 8b75bd3..303a4ab 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -10,7 +10,7 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { - public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + public Task> GetNumberOfAvailableTicketsByTypeAsync(TicketType ticketType); public Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId); public Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount); public Task>> GetTicketsForResellAsync(Guid eventId, int page, diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index e141438..5d1cd5e 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -6,6 +6,7 @@ using TickAPI.Common.Results.Generic; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; +using TickAPI.ShoppingCarts.Abstractions; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; @@ -20,24 +21,34 @@ public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; private readonly ITicketTypeRepository _ticketTypeRepository; + private readonly IShoppingCartRepository _shoppingCartRepository; private readonly IPaginationService _paginationService; private readonly IQRCodeService _qrCodeService; public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, - IPaginationService paginationService, IQRCodeService qrCodeService) + IShoppingCartRepository shoppingCartRepository, IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; _ticketTypeRepository = ticketTypeRepository; + _shoppingCartRepository = shoppingCartRepository; _paginationService = paginationService; _qrCodeService = qrCodeService; } // TODO: Update this method to also count tickets cached in Redis as unavailable - public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) + public async Task> GetNumberOfAvailableTicketsByTypeAsync(TicketType ticketType) { var unavailableTickets = _ticketRepository.GetAllTicketsByTicketType(ticketType); + var reservedTicketsAmountResult = await _shoppingCartRepository.GetAmountOfTicketTypeAsync(ticketType.Id); + + if (reservedTicketsAmountResult.IsError) + { + return Result.PropagateError(reservedTicketsAmountResult); + } + + var reservedTicketsAmount = reservedTicketsAmountResult.Value; - var availableCount = ticketType.MaxCount - unavailableTickets.Count(); + var availableCount = ticketType.MaxCount - unavailableTickets.Count() - reservedTicketsAmount; if (availableCount < 0) { @@ -57,7 +68,7 @@ public async Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ti return Result.PropagateError(ticketTypeResult); } - return GetNumberOfAvailableTicketsByType(ticketTypeResult.Value!); + return await GetNumberOfAvailableTicketsByTypeAsync(ticketTypeResult.Value!); } public async Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount) From ec35bc9834401eacf57b1b0755bc5d648bcb9874 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 23:17:00 +0200 Subject: [PATCH 23/32] fixed counting amount of reserved tickets --- TickAPI/TickAPI/Events/Services/EventService.cs | 9 +++++++-- .../ShoppingCarts/Repositories/ShoppingCartRepository.cs | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 350a4e0..91cdda2 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -139,7 +139,12 @@ public async Task> GetEventDetailsAsync(Guid { foreach (var t in ev.TicketTypes) { - var availableCount = await _ticketService.GetNumberOfAvailableTicketsByTypeAsync(t); + var availableCountResult = await _ticketService.GetNumberOfAvailableTicketsByTypeAsync(t); + + if (availableCountResult.IsError) + { + return Result.PropagateError(availableCountResult); + } ticketTypes.Add(new GetEventDetailsResponseTicketTypeDto( t.Id, @@ -147,7 +152,7 @@ public async Task> GetEventDetailsAsync(Guid t.Price, t.Currency, t.AvailableFrom, - availableCount.Value + availableCountResult.Value )); } } diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 8699445..a487c63 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -171,7 +171,7 @@ public async Task> GetAmountOfTicketTypeAsync(Guid ticketTypeId) if (amount is null) { - return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be retrieved"); + return Result.Success(0); } return Result.Success(amount.Value); @@ -204,7 +204,7 @@ public async Task> IncrementAmountOfTicketTypeAsync(Guid ticketType try { - newAmount = await _redisService.IncrementValueAsync(GetAmountKey(ticketTypeId)); + newAmount = await _redisService.IncrementValueAsync(GetAmountKey(ticketTypeId), amount); } catch (Exception e) { @@ -220,7 +220,7 @@ public async Task> DecrementAmountOfTicketTypeAsync(Guid ticketType try { - newAmount = await _redisService.DecrementValueAsync(GetAmountKey(ticketTypeId)); + newAmount = await _redisService.DecrementValueAsync(GetAmountKey(ticketTypeId), amount); } catch (Exception e) { From 98ddd7407c112000b7ad7d57a8b2178c168b49ed Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 23 May 2025 22:35:59 +0200 Subject: [PATCH 24/32] added redis service method for retrieving keys --- .../TickAPI/Common/Redis/Abstractions/IRedisService.cs | 1 + TickAPI/TickAPI/Common/Redis/Services/RedisService.cs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs index 194a7d5..2dd6674 100644 --- a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs @@ -13,4 +13,5 @@ public interface IRedisService public Task DecrementValueAsync(string key, long value = 1); public Task GetLongValueAsync(string key); public Task SetLongValueAsync(string key, long value, TimeSpan? expiry = null); + public Task> GetKeysByPatternAsync(string pattern); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index bba9b34..9c2b0fa 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -6,11 +6,13 @@ namespace TickAPI.Common.Redis.Services; public class RedisService : IRedisService { + private readonly IConnectionMultiplexer _connectionMultiplexer; private readonly IDatabase _database; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public RedisService(IConnectionMultiplexer connectionMultiplexer) { + _connectionMultiplexer = connectionMultiplexer; _database = connectionMultiplexer.GetDatabase(); } @@ -85,6 +87,12 @@ public async Task SetLongValueAsync(string key, long value, TimeSpan? expi return await RetryAsync(async () => await _database.StringSetAsync(key, value.ToString(), expiry)); } + public async Task> GetKeysByPatternAsync(string pattern) + { + var server = _connectionMultiplexer.GetServer(_connectionMultiplexer.GetEndPoints().First()); + return server.Keys(pattern: pattern).Select(k => k.ToString()); + } + private static async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) { var attempt = 0; From bc6d27aafa71ad74250c4f01722280895b78adb0 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 23 May 2025 23:26:03 +0200 Subject: [PATCH 25/32] added background service for syncing cart counters with their actual state --- TickAPI/TickAPI/Program.cs | 2 + .../ShoppingCartSyncBackgroundService.cs | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 7d05eb3..17ed802 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -45,6 +45,7 @@ using TickAPI.Common.QR.Abstractions; using TickAPI.Common.QR.Services; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Background; using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; using TickAPI.TicketTypes.Abstractions; @@ -126,6 +127,7 @@ // Add shopping cart services. builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddHostedService(); // Add ticket type services builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs new file mode 100644 index 0000000..fd66407 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs @@ -0,0 +1,80 @@ +using TickAPI.Common.Redis.Abstractions; +using TickAPI.ShoppingCarts.Models; + +namespace TickAPI.ShoppingCarts.Background; + +public class ShoppingCartSyncBackgroundService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private static readonly TimeSpan SyncInterval = TimeSpan.FromMinutes(5); + + public ShoppingCartSyncBackgroundService(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _serviceProvider.CreateScope(); + var redisService = scope.ServiceProvider.GetRequiredService(); + await SyncTicketTypeCountersAsync(redisService, stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while syncing shopping cart ticket counters"); + } + + await Task.Delay(SyncInterval, stoppingToken); + } + } + + private async Task SyncTicketTypeCountersAsync(IRedisService redisService, CancellationToken cancellationToken) + { + var cartKeys = await redisService.GetKeysByPatternAsync("cart:*"); + var ticketTypeCounts = new Dictionary(); + + foreach (var cartKey in cartKeys) + { + var cart = await redisService.GetObjectAsync(cartKey); + if (cart == null) continue; + + foreach (var ticket in cart.NewTickets) + { + if (ticketTypeCounts.ContainsKey(ticket.TicketTypeId)) + ticketTypeCounts[ticket.TicketTypeId] += ticket.Quantity; + else + ticketTypeCounts[ticket.TicketTypeId] = ticket.Quantity; + } + + if (cancellationToken.IsCancellationRequested) return; + } + + foreach (var kvp in ticketTypeCounts) + { + await redisService.SetLongValueAsync($"amount:{kvp.Key}", kvp.Value); + } + + var existingAmountKeys = await redisService.GetKeysByPatternAsync("amount:*"); + + foreach (var key in existingAmountKeys) + { + var typeIdStr = key.Split(":").Last(); + if (!Guid.TryParse(typeIdStr, out var ticketTypeId)) continue; + + if (!ticketTypeCounts.ContainsKey(ticketTypeId) || ticketTypeCounts[ticketTypeId] == 0) + { + await redisService.DeleteKeyAsync(key); + } + + if (cancellationToken.IsCancellationRequested) return; + } + + _logger.LogInformation("Synchronized ticket counters for {Count} ticket types", ticketTypeCounts.Count); + } +} From a9bc3278117a8beb19c5fdf4423823370be3b68e Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 23 May 2025 23:43:02 +0200 Subject: [PATCH 26/32] fixed tests to account for changed method logic --- .../TickAPI.Tests/Events/Services/EventServiceTests.cs | 2 +- .../TickAPI.Tests/Tickets/Services/TicketServiceTests.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index a4ac30b..464170b 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -632,7 +632,7 @@ public async Task GetEventDetailsAsync_WhenSuccessful_ShouldReturnEventDetails() ticketServiceMock .Setup(m => m.GetNumberOfAvailableTicketsByTypeAsync(It.IsAny())) - .Returns((TicketType input) => + .ReturnsAsync((TicketType input) => Result.Success((uint)(input.Price / 10)) ); diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 1c100de..b6f1058 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -39,6 +39,10 @@ public async Task GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldRetu .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); + shoppingCartRepositoryMock + .Setup(s => s.GetAmountOfTicketTypeAsync(type.Id)) + .ReturnsAsync(Result.Success(0)); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); @@ -66,6 +70,10 @@ public async Task GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCo ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); + + shoppingCartRepositoryMock + .Setup(s => s.GetAmountOfTicketTypeAsync(type.Id)) + .ReturnsAsync(Result.Success(0)); var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); From 1bccf0c71e7dec372cbb0b39f7e535967469fe64 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 21:51:57 +0200 Subject: [PATCH 27/32] changed ToObjectResult in Result.cs to return 200 on success instead of 204 --- TickAPI/TickAPI/Common/Results/Result.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index 2e7f4ec..8d850d7 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -57,7 +57,7 @@ public ObjectResult ToObjectResult(int successCode = StatusCodes.Status200OK) }; } - return new ObjectResult(null) + return new ObjectResult(string.Empty) { StatusCode = successCode }; From b649e813f6b4a6708dd9a4f43067178dae5be225 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 21:56:35 +0200 Subject: [PATCH 28/32] ShoppingCartsController.cs uses ToObjectResult --- .../Controllers/ShoppingCartsController.cs | 53 ++++++------------- .../DTOs/Response/CheckoutResponseDto.cs | 6 --- 2 files changed, 15 insertions(+), 44 deletions(-) delete mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 1780d8d..373d939 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -2,6 +2,7 @@ using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Payment.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Request; using TickAPI.ShoppingCarts.DTOs.Response; @@ -28,19 +29,15 @@ public async Task AddTickets([FromBody] AddNewTicketDto addTicketD var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var addTicketResult = await _shoppingCartService.AddNewTicketsToCartAsync(addTicketDto.TicketTypeId, addTicketDto.Amount, email); - if (addTicketResult.IsError) - { - return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); - } - - return Ok(); + + return addTicketResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -50,17 +47,13 @@ public async Task> GetTickets() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var getTicketsResult = await _shoppingCartService.GetTicketsFromCartAsync(email); - if (getTicketsResult.IsError) - { - return StatusCode(getTicketsResult.StatusCode, getTicketsResult.ErrorMsg); - } - - return Ok(getTicketsResult.Value); + + return getTicketsResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -70,19 +63,15 @@ public async Task RemoveTickets([FromBody] RemoveNewTicketDto remo var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; - var addTicketResult = + var removeTicketResult = await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketTypeId, removeTicketDto.Amount, email); - if (addTicketResult.IsError) - { - return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); - } - return Ok(); + return removeTicketResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -92,41 +81,29 @@ public async Task>> GetDueAmount() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email); - if (dueAmountResult.IsError) - { - return StatusCode(dueAmountResult.StatusCode, dueAmountResult.ErrorMsg); - } - - return Ok(dueAmountResult.Value); + return dueAmountResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] - public async Task> Checkout([FromBody] CheckoutDto checkoutDto) + public async Task> Checkout([FromBody] CheckoutDto checkoutDto) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var checkoutResult = await _shoppingCartService.CheckoutAsync(email, checkoutDto.Amount, checkoutDto.Currency, checkoutDto.CardNumber, checkoutDto.CardExpiry, checkoutDto.Cvv); - if (checkoutResult.IsError) - { - return StatusCode(checkoutResult.StatusCode, checkoutResult.ErrorMsg); - } - - var checkout = checkoutResult.Value!; - - return Ok(new CheckoutResponseDto(checkout.TransactionId, checkout.Status)); + return checkoutResult.ToObjectResult(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs deleted file mode 100644 index 6a4b4f2..0000000 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TickAPI.ShoppingCarts.DTOs.Response; - -public record CheckoutResponseDto( - string TransactionId, - string Status -); \ No newline at end of file From f88d48715aaa7d61124fc35dc93d8db5c6eb3bf3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 21:59:51 +0200 Subject: [PATCH 29/32] stylistic fixes --- .../ShoppingCarts/Repositories/ShoppingCartRepository.cs | 4 ++-- TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs | 1 - TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs | 1 - TickAPI/TickAPI/Tickets/Services/TicketService.cs | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index a487c63..e4f7845 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -57,7 +57,7 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) { - if (amount <= 0) + if (amount == 0) { return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); } @@ -105,7 +105,7 @@ public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ti public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount) { - if (amount <= 0) + if (amount == 0) { return Result.Failure(StatusCodes.Status400BadRequest, "amount of removed tickets must be greater than 0"); } diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 877b9cd..ff20841 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -1,6 +1,5 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; -using TickAPI.Customers.Models; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 303a4ab..d1fb208 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -21,7 +21,6 @@ public Task>> GetTicketsForCustome public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); - public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, string? seats = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 5d1cd5e..74b6dad 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -35,7 +35,6 @@ public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository t _qrCodeService = qrCodeService; } - // TODO: Update this method to also count tickets cached in Redis as unavailable public async Task> GetNumberOfAvailableTicketsByTypeAsync(TicketType ticketType) { var unavailableTickets = _ticketRepository.GetAllTicketsByTicketType(ticketType); From e1efea5aaa6d9428b613d070db432f17691c09db Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 22:00:34 +0200 Subject: [PATCH 30/32] added returning currency when getting cart items --- .../GetShoppingCartTicketsNewTicketDetailsResponseDto.cs | 3 ++- .../GetShoppingCartTicketsResellTicketDetailsResponseDto.cs | 3 ++- TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs index f85e035..579a865 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -6,5 +6,6 @@ public record GetShoppingCartTicketsNewTicketDetailsResponseDto( string TicketType, string OrganizerName, uint Quantity, - decimal UnitPrice + decimal UnitPrice, + string Currency ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs index f2aa4d0..a56fc13 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -6,5 +6,6 @@ public record GetShoppingCartTicketsResellTicketDetailsResponseDto( string TicketType, string OrganizerName, string OriginalOwnerEmail, - decimal Price + decimal Price, + string Currency ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs index ae3d653..9e75a25 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -15,7 +15,8 @@ public static GetShoppingCartTicketsNewTicketDetailsResponseDto type.Description, type.Event.Organizer.DisplayName, quantity, - type.Price + type.Price, + type.Currency ); } @@ -28,7 +29,8 @@ public static GetShoppingCartTicketsResellTicketDetailsResponseDto ticket.Type.Description, ticket.Type.Event.Organizer.DisplayName, ticket.Owner.Email, - ticket.Type.Price + ticket.Type.Price, + ticket.Type.Currency ); } } \ No newline at end of file From e8e3acc020c435b17898235c695beed0773940c3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 25 May 2025 01:28:46 +0200 Subject: [PATCH 31/32] program reads shopping cart lifetime and syncing interval from appsettings --- TickAPI/TickAPI/Program.cs | 3 +++ .../Background/ShoppingCartSyncBackgroundService.cs | 12 ++++++++---- .../ShoppingCarts/Options/ShoppingCartOptions.cs | 7 +++++++ .../Repositories/ShoppingCartRepository.cs | 13 ++++++++----- TickAPI/TickAPI/appsettings.example.json | 4 ++++ 5 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 17ed802..53441b3 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -46,6 +46,7 @@ using TickAPI.Common.QR.Services; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Background; +using TickAPI.ShoppingCarts.Options; using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; using TickAPI.TicketTypes.Abstractions; @@ -128,6 +129,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); +builder.Services.Configure( + builder.Configuration.GetSection("ShoppingCart")); // Add ticket type services builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs index fd66407..0e99449 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs @@ -1,5 +1,7 @@ -using TickAPI.Common.Redis.Abstractions; +using Microsoft.Extensions.Options; +using TickAPI.Common.Redis.Abstractions; using TickAPI.ShoppingCarts.Models; +using TickAPI.ShoppingCarts.Options; namespace TickAPI.ShoppingCarts.Background; @@ -7,12 +9,14 @@ public class ShoppingCartSyncBackgroundService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private static readonly TimeSpan SyncInterval = TimeSpan.FromMinutes(5); + private readonly TimeSpan _syncInterval; - public ShoppingCartSyncBackgroundService(IServiceProvider serviceProvider, ILogger logger) + public ShoppingCartSyncBackgroundService(IServiceProvider serviceProvider, + ILogger logger, IOptions options) { _serviceProvider = serviceProvider; _logger = logger; + _syncInterval = TimeSpan.FromMinutes(options.Value.SyncIntervalMinutes); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -30,7 +34,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogError(ex, "Error while syncing shopping cart ticket counters"); } - await Task.Delay(SyncInterval, stoppingToken); + await Task.Delay(_syncInterval, stoppingToken); } } diff --git a/TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs b/TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs new file mode 100644 index 0000000..c2f03a2 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs @@ -0,0 +1,7 @@ +namespace TickAPI.ShoppingCarts.Options; + +public class ShoppingCartOptions +{ + public int SyncIntervalMinutes { get; set; } + public int LifetimeMinutes { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index e4f7845..493afa5 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -1,8 +1,10 @@ -using TickAPI.Common.Redis.Abstractions; +using Microsoft.Extensions.Options; +using TickAPI.Common.Redis.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Models; +using TickAPI.ShoppingCarts.Options; using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Repositories; @@ -10,11 +12,12 @@ namespace TickAPI.ShoppingCarts.Repositories; public class ShoppingCartRepository : IShoppingCartRepository { private readonly IRedisService _redisService; - private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(15); + private readonly TimeSpan _defaultExpiry; - public ShoppingCartRepository(IRedisService redisService) + public ShoppingCartRepository(IRedisService redisService, IOptions options) { _redisService = redisService; + _defaultExpiry = TimeSpan.FromMinutes(options.Value.LifetimeMinutes); } public async Task> GetShoppingCartByEmailAsync(string customerEmail) @@ -25,7 +28,7 @@ public async Task> GetShoppingCartByEmailAsync(string custo try { cart = await _redisService.GetObjectAsync(cartKey); - await _redisService.KeyExpireAsync(cartKey, DefaultExpiry); + await _redisService.KeyExpireAsync(cartKey, _defaultExpiry); } catch (Exception e) { @@ -41,7 +44,7 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping try { - var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, DefaultExpiry); + var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, _defaultExpiry); if (!res) { return Result.Failure(StatusCodes.Status500InternalServerError, "the shopping cart could not be updated"); diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 420009a..c7e46ab 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -33,5 +33,9 @@ }, "PaymentGateway": { "Url": "http://localhost:7474" + }, + "ShoppingCart": { + "SyncIntervalMinutes": 5, + "LifetimeMinutes": 15 } } \ No newline at end of file From 5eb6440e5f162078afe0682d73dfba591c9bbe6a Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 25 May 2025 01:31:04 +0200 Subject: [PATCH 32/32] updated tests to account for ToObjectResult method change --- TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index 32ed274..828d4b3 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -122,7 +122,7 @@ public void ToObjectResult_WhenResultIsSuccess_ShouldReturnObjectResultWithDefau // Assert Assert.IsType(objectResult); Assert.Equal(StatusCodes.Status200OK, objectResult.StatusCode); - Assert.Null(objectResult.Value); + Assert.Equal(string.Empty, objectResult.Value); } [Fact] @@ -138,6 +138,6 @@ public void ToObjectResult_WhenResultIsSuccessWithCustomStatusCode_ShouldReturnO // Assert Assert.IsType(objectResult); Assert.Equal(customSuccessCode, objectResult.StatusCode); - Assert.Null(objectResult.Value); + Assert.Equal(string.Empty, objectResult.Value); } } \ No newline at end of file