diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 068a29f..dd1a7bf 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Http; using Moq; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; using TickAPI.Tickets.Services; using TickAPI.TicketTypes.Models; @@ -17,13 +20,14 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var type = new TicketType { MaxCount = 30 }; var ticketList = new List(new Ticket[10]); - Mock ticketRepositoryMock = new Mock(); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -40,13 +44,14 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var type = new TicketType { MaxCount = 30 }; var ticketList = new List(new Ticket[50]); - Mock ticketRepositoryMock = new Mock(); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -56,4 +61,269 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); Assert.Equal("The number of available tickets is negative.", result.ErrorMsg); } + + [Fact] + public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + + var ticket1 = new Ticket + { + Id = Guid.NewGuid(), + ForResell = true, + Type = new TicketType + { + Price = 50m, + Currency = "USD", + Description = "VIP" + }, + Seats = "A1" + }; + + var ticket2 = new Ticket + { + Id = Guid.NewGuid(), + ForResell = true, + Type = new TicketType + { + Price = 30m, + Currency = "USD", + Description = "Standard" + }, + Seats = "B2" + }; + + var allTickets = new List { ticket1, ticket2 }.AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(allTickets); + + var paginatedTickets = new PaginatedData( + new List { ticket1, ticket2 }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedTickets)); + + var mappedDto1 = new GetTicketForResellResponseDto( + ticket1.Id, + ticket1.Type.Price, + ticket1.Type.Currency, + ticket1.Type.Description, + ticket1.Seats + ); + + var mappedDto2 = new GetTicketForResellResponseDto( + ticket2.Id, + ticket2.Type.Price, + ticket2.Type.Currency, + ticket2.Type.Description, + ticket2.Seats + ); + + var mappedData = new PaginatedData( + new List { mappedDto1, mappedDto2 }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + paginationServiceMock.Setup(p => p.MapData( + paginatedTickets, + It.IsAny>())) + .Returns(mappedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal(mappedDto1, result.Value!.Data[0]); + Assert.Equal(mappedDto2, result.Value!.Data[1]); + } + + [Fact] + public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEmptyList() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + + var tickets = new List + { + new Ticket + { + Id = Guid.NewGuid(), + ForResell = false, + Type = new TicketType + { + Price = 50m, + Currency = "USD", + Description = "VIP" + } + }, + new Ticket + { + Id = Guid.NewGuid(), + ForResell = false, + Type = new TicketType + { + Price = 30m, + Currency = "USD", + Description = "Standard" + } + } + }.AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(tickets); + + var paginatedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var mappedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + paginationServiceMock.Setup(p => p.MapData( + paginatedData, + It.IsAny>())) + .Returns(mappedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.Value!.Data); + } + + [Fact] + public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateError() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + const string errorMsg = "Invalid pagination parameters"; + const int statusCode = 400; + + var tickets = new List + { + new Ticket + { + Id = Guid.NewGuid(), + ForResell = true, + Type = new TicketType + { + Price = 50m, + Currency = "USD", + Description = "VIP" + } + } + }.AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(tickets); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Failure(statusCode, errorMsg)); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsError); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmptyList() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + + var tickets = new List().AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(tickets); + + var paginatedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var mappedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + paginationServiceMock.Setup(p => p.MapData( + paginatedData, + It.IsAny>())) + .Returns(mappedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.Value!.Data); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index d3220d8..8ec26d6 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -6,4 +6,5 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketRepository { public IQueryable GetAllTicketsByTicketType(TicketType ticketType); + public IQueryable GetTicketsByEventId(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 85e9ac5..e6d0d55 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,9 +1,12 @@ -using TickAPI.Common.Results.Generic; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; +using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Abstractions; public interface ITicketService -{ - public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); +{ + Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index fcf4467..005ac16 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -1,4 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; namespace TickAPI.Tickets.Controllers; @@ -6,5 +9,21 @@ namespace TickAPI.Tickets.Controllers; [Route("api/[controller]")] public class TicketsController : ControllerBase { + private readonly ITicketService _ticketService; + + public TicketsController(ITicketService ticketService) + { + _ticketService = ticketService; + } + [HttpGet("/for-resell")] + public async Task>> GetTicketsForResell([FromQuery] Guid eventId, [FromQuery] int pageSize, [FromQuery] int page) + { + var result = await _ticketService.GetTicketsForResellAsync(eventId, page, pageSize); + if (result.IsError) + { + return StatusCode(result.StatusCode, result.ErrorMsg); + } + return result.Value!; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs new file mode 100644 index 0000000..831ad47 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.Tickets.DTOs.Response; + +public record GetTicketForResellResponseDto( + Guid Id, + decimal Price, + string Currency, + string Description, + string? Seats +); diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 42ade22..c85566d 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.TickApiDbContext; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.TickApiDbContext; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -18,4 +19,12 @@ public IQueryable GetAllTicketsByTicketType(TicketType ticketType) { return _tickApiDbContext.Tickets.Where(t => t.Type == ticketType); } + + public IQueryable GetTicketsByEventId(Guid eventId) + { + return _tickApiDbContext.Tickets + .Include(t => t.Type) + .Include(t => t.Type.Event) + .Where(t => t.Type.Event.Id == eventId); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index d8d7855..4b5e7b6 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -1,5 +1,8 @@ -using TickAPI.Common.Results.Generic; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -7,10 +10,12 @@ namespace TickAPI.Tickets.Services; public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; + private readonly IPaginationService _paginationService; - public TicketService(ITicketRepository ticketRepository) + public TicketService(ITicketRepository ticketRepository, IPaginationService paginationService) { _ticketRepository = ticketRepository; + _paginationService = paginationService; } // TODO: Update this method to also count tickets cached in Redis as unavailable @@ -28,4 +33,18 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } + + public async Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize) + { + var eventTickets = _ticketRepository.GetTicketsByEventId(eventId); + var ticketsForResell = eventTickets.Where(t => t.ForResell); + var paginatedTicketsResult = await _paginationService.PaginateAsync(ticketsForResell, pageSize, page); + if (paginatedTicketsResult.IsError) + { + return Result>.PropagateError(paginatedTicketsResult); + } + var paginatedResult = _paginationService.MapData(paginatedTicketsResult.Value!, + t => new GetTicketForResellResponseDto(t.Id, t.Type.Price, t.Type.Currency, t.Type.Description, t.Seats)); + return Result>.Success(paginatedResult); + } } \ No newline at end of file