diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index 36799c1..32ed274 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -37,6 +37,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() { @@ -55,6 +70,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.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 331853f..1c100de 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -10,10 +10,12 @@ 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; using TickAPI.Tickets.Services; +using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; namespace TickAPI.Tests.Tickets.Services; @@ -21,13 +23,15 @@ 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 }; var ticketList = new List(new Ticket[10]); var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -35,10 +39,11 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var result = sut.GetNumberOfAvailableTicketsByType(type); + var result = await sut.GetNumberOfAvailableTicketsByTypeAsync(type); // Assert Assert.True(result.IsSuccess); @@ -46,13 +51,15 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr } [Fact] - public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_ShouldReturnError() + public async Task GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_ShouldReturnError() { // Arrange var type = new TicketType { MaxCount = 30 }; var ticketList = new List(new Ticket[50]); var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -60,10 +67,11 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var result = sut.GetNumberOfAvailableTicketsByType(type); + var result = await sut.GetNumberOfAvailableTicketsByTypeAsync(type); // Assert Assert.True(result.IsError); @@ -111,6 +119,9 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(allTickets); + var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); + var paginatedTickets = new PaginatedData( new List { ticket1, ticket2 }, page, @@ -156,7 +167,8 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -206,6 +218,9 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); + var paginatedData = new PaginatedData( new List(), page, @@ -234,7 +249,8 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -272,6 +288,9 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + + var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) @@ -279,7 +298,8 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -304,6 +324,9 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); + var paginatedData = new PaginatedData( new List(), page, @@ -332,7 +355,8 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -380,9 +404,11 @@ 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 shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); @@ -392,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, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -426,18 +453,21 @@ 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 shoppingCartRepositoryMock = 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -513,6 +543,8 @@ 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)) @@ -523,7 +555,8 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -555,6 +588,9 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var ticketRepositoryMock = new Mock(); 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); @@ -563,7 +599,8 @@ 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -580,9 +617,12 @@ 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 shoppingCartRepositoryMock = 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, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var res = await sut.ScanTicket(guid); diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index 08bc55e..2e7f4ec 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -27,6 +27,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/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/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/Program.cs b/TickAPI/TickAPI/Program.cs index 8a0ac0e..7d05eb3 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -44,6 +44,11 @@ using TickAPI.Common.Payment.Services; using TickAPI.Common.QR.Abstractions; using TickAPI.Common.QR.Services; +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"; @@ -118,6 +123,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add shopping cart services. +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/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs new file mode 100644 index 0000000..d8be591 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -0,0 +1,18 @@ +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); + 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/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs new file mode 100644 index 0000000..787e62e --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -0,0 +1,16 @@ +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.DTOs.Response; + +namespace TickAPI.ShoppingCarts.Abstractions; + +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); + 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 new file mode 100644 index 0000000..1780d8d --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -0,0 +1,132 @@ +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; +using TickAPI.ShoppingCarts.DTOs.Response; + +namespace TickAPI.ShoppingCarts.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ShoppingCartsController : ControllerBase +{ + private readonly IShoppingCartService _shoppingCartService; + private readonly IClaimsService _claimsService; + + public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaimsService claimsService) + { + _shoppingCartService = shoppingCartService; + _claimsService = claimsService; + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost] + public async Task AddTickets([FromBody] AddNewTicketDto addTicketDto) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + 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(); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet] + public async Task> GetTickets() + { + 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)] + [HttpDelete] + public async Task RemoveTickets([FromBody] RemoveNewTicketDto removeTicketDto) + { + 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)] + [HttpGet("due")] + public async Task>> GetDueAmount() + { + 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); + + if (dueAmountResult.IsError) + { + return StatusCode(dueAmountResult.StatusCode, dueAmountResult.ErrorMsg); + } + + return Ok(dueAmountResult.Value); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost("checkout")] + public async Task> Checkout([FromBody] CheckoutDto checkoutDto) + { + 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/AddNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs new file mode 100644 index 0000000..48c3d8e --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record AddNewTicketDto( + Guid TicketTypeId, + uint Amount +); \ 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/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/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 diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs new file mode 100644 index 0000000..f85e035 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsNewTicketDetailsResponseDto( + Guid TicketTypeId, + string EventName, + string TicketType, + string OrganizerName, + 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 new file mode 100644 index 0000000..f2aa4d0 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsResellTicketDetailsResponseDto( + Guid TicketId, + string EventName, + string TicketType, + string OrganizerName, + string OriginalOwnerEmail, + decimal Price +); \ 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..3f553d6 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsResponseDto( + 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..ae3d653 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -0,0 +1,34 @@ +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, + type.Price + ); + } + + 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, + ticket.Type.Price + ); + } +} \ 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..4d480eb --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -0,0 +1,9 @@ +using TickAPI.Tickets.Models; + +namespace TickAPI.ShoppingCarts.Models; + +public class ShoppingCart +{ + public List NewTickets { 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..8699445 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -0,0 +1,263 @@ +using TickAPI.Common.Redis.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Models; +using TickAPI.Tickets.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(); + } + + 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) + { + 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 incrementTicketAmountResult = await IncrementAmountOfTicketTypeAsync(ticketTypeId, amount); + + if (incrementTicketAmountResult.IsError) + { + return Result.PropagateError(incrementTicketAmountResult); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + 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 decrementTicketAmountResult = await DecrementAmountOfTicketTypeAsync(ticketTypeId, amount); + + if (decrementTicketAmountResult.IsError) + { + return Result.PropagateError(decrementTicketAmountResult); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + 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 diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs new file mode 100644 index 0000000..b4032e5 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -0,0 +1,251 @@ +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; +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, ICustomerRepository customerRepository, + ITicketService ticketService, IPaymentGatewayService paymentGatewayService) + { + _shoppingCartRepository = shoppingCartRepository; + _customerRepository = customerRepository; + _ticketService = ticketService; + _paymentGatewayService = paymentGatewayService; + } + + public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) + { + var availabilityResult = await _ticketService.CheckTicketAvailabilityByTypeIdAsync(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 addTicketsToCartResult = await _shoppingCartRepository.AddNewTicketsToCartAsync(customerEmail, ticketTypeId, amount); + + if (addTicketsToCartResult.IsError) + { + return Result.PropagateError(addTicketsToCartResult); + } + + return Result.Success(); + } + + public async Task> GetTicketsFromCartAsync(string customerEmail) + { + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + 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); + } + + // TODO: Add resell ticket parsing + + var result = new GetShoppingCartTicketsResponseDto(newTickets, []); + + return Result.Success(result); + } + + public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail) + { + var removeTicketsFromCartResult = await _shoppingCartRepository.RemoveNewTicketsFromCartAsync(customerEmail, ticketTypeId, amount); + + if (removeTicketsFromCartResult.IsError) + { + return Result.PropagateError(removeTicketsFromCartResult); + } + + return Result.Success(); + } + + public async Task>> GetDueAmountAsync(string customerEmail) + { + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result>.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + Dictionary dueAmount = new Dictionary(); + + 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(dueAmount.ContainsKey(ticketType.Currency)) + { + dueAmount[ticketType.Currency] += newTicket.Quantity * ticketType.Price; + } + else + { + dueAmount.Add(ticketType.Currency, newTicket.Quantity * ticketType.Price); + } + } + + // TODO: Add resell tickets to the calculations + + 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); + + if (dueAmountResult.IsError) + { + return Result.PropagateError(dueAmountResult); + } + + 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, + $"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 generateTicketsResult = await GenerateBoughtTicketsAsync(customerEmail, currency); + // TODO: Add passing ownership of resell tickets + + if (generateTicketsResult.IsError) + { + return Result.PropagateError(generateTicketsResult); + } + + var payment = paymentResult.Value!; + + return Result.Success(payment); + } + + private async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) + { + 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!; + var removals = new List<(Guid id, uint amount)>(); + + 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) + { + removals.Add((ticket.TicketTypeId, ticket.Quantity)); + + 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); + } + } + } + } + + 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 diff --git a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs new file mode 100644 index 0000000..f9fba2f --- /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 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 new file mode 100644 index 0000000..929e471 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs @@ -0,0 +1,34 @@ +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 async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) + { + var ticketType = await + _tickApiDbContext.TicketTypes + .Include(t => t.Event) + .Include(t => t.Event.Organizer) + .FirstOrDefaultAsync(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/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 c722a8c..303a4ab 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; @@ -9,7 +10,9 @@ 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, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, @@ -17,4 +20,8 @@ public Task>> GetTicketsForCustome public Task ScanTicket(Guid ticketGuid); 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/Models/ShoppingCartNewTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs new file mode 100644 index 0000000..0ed97d9 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Tickets.Models; + +public class ShoppingCartNewTicket +{ + public Guid TicketTypeId { 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 new file mode 100644 index 0000000..9ddb9ac --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Tickets.Models; + +public class ShoppingCartResellTicket +{ + public Guid TicketId { get; set; } +} \ No newline at end of file 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; } diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index e5abb5e..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; @@ -41,9 +42,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"); @@ -66,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 27b60fd..5d1cd5e 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -4,10 +4,15 @@ using TickAPI.Common.QR.Abstractions; using TickAPI.Common.Results; 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; using TickAPI.Tickets.Filters; +using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -15,21 +20,35 @@ namespace TickAPI.Tickets.Services; 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, IPaginationService paginationService, IQRCodeService qrCodeService) + + public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, + 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) { @@ -40,6 +59,32 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } + public async Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId) + { + var ticketTypeResult = await _ticketTypeRepository.GetTicketTypeByIdAsync(ticketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + return await GetNumberOfAvailableTicketsByTypeAsync(ticketTypeResult.Value!); + } + + public async Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount) + { + var numberOfTicketsResult = await GetNumberOfAvailableTicketsByTypeIdAsync(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); @@ -105,11 +150,39 @@ 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 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