diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs index 6d8b112..ef3fad5 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs @@ -45,11 +45,11 @@ public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() new CreateEventTicketTypeDto("V.I.P", 10, 500.9m, "zł", new DateTime(2025, 5, 10)), ]; CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); - CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, categories, ticketTypes, eventStatus, createAddress); + CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, categories, ticketTypes, eventStatus, createAddress, null); var eventServiceMock = new Mock(); eventServiceMock - .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories , ticketTypes, eventStatus, email)) + .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories , ticketTypes, eventStatus, email, null)) .ReturnsAsync(Result.Success(new Event())); var claims = new List @@ -123,7 +123,7 @@ public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() }; // act - var res = await sut.CreateEvent(new CreateEventDto(name, description, startDate, endDate, minimumAge, categories, ticketTypes, eventStatus, createAddress)); + var res = await sut.CreateEvent(new CreateEventDto(name, description, startDate, endDate, minimumAge, categories, ticketTypes, eventStatus, createAddress, null)); // Assert var result = Assert.IsType>(res); diff --git a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs index 6256303..47ffd77 100644 --- a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs @@ -48,8 +48,7 @@ public void ApplyFilters_WithName_ShouldCallFilterByName() _eventFilterApplier.ApplyFilters(filters); // Assert - _mockEventFilter.Verify(ef => ef.FilterByName(filters.SearchQuery!), Times.Once); - _mockEventFilter.Verify(ef => ef.FilterByDescription(filters.SearchQuery!), Times.Once); + _mockEventFilter.Verify(ef => ef.FilterByNameOrDescription(filters.SearchQuery!), Times.Once); _mockEventFilter.Verify(ef => ef.GetEvents(), Times.Once); } @@ -660,8 +659,7 @@ public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() _eventFilterApplier.ApplyFilters(filters); // Assert - _mockEventFilter.Verify(ef => ef.FilterByName(filters.SearchQuery!), Times.Once); - _mockEventFilter.Verify(ef => ef.FilterByDescription(filters.SearchQuery!), Times.Once); + _mockEventFilter.Verify(ef => ef.FilterByNameOrDescription(filters.SearchQuery!), Times.Once); _mockEventFilter.Verify(ef => ef.FilterByStartDate(filters.StartDate!.Value), Times.Once); _mockEventFilter.Verify(ef => ef.FilterByMinPrice(filters.MinPrice!.Value), Times.Once); _mockEventFilter.Verify(ef => ef.FilterByMaxPrice(filters.MaxPrice!.Value), Times.Once); @@ -703,8 +701,7 @@ public void ApplyFilters_WithNoFilters_ShouldOnlyCallGetEvents() var result = _eventFilterApplier.ApplyFilters(filters); // Assert - _mockEventFilter.Verify(ef => ef.FilterByName(It.IsAny()), Times.Never); - _mockEventFilter.Verify(ef => ef.FilterByDescription(It.IsAny()), Times.Never); + _mockEventFilter.Verify(ef => ef.FilterByNameOrDescription(It.IsAny()), Times.Never); _mockEventFilter.Verify(ef => ef.FilterByStartDate(It.IsAny()), Times.Never); _mockEventFilter.Verify(ef => ef.FilterByMinStartDate(It.IsAny()), Times.Never); _mockEventFilter.Verify(ef => ef.FilterByMaxStartDate(It.IsAny()), Times.Never); diff --git a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterTests.cs b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterTests.cs index 6458b6e..90b4052 100644 --- a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterTests.cs @@ -102,7 +102,7 @@ public void FilterByName_ShouldReturnMatchingEvents() // Act var eventFilter = new EventFilter(events.AsQueryable()); - eventFilter.FilterByName("concert"); + eventFilter.FilterByNameOrDescription("concert"); var result = eventFilter.GetEvents().ToList(); // Assert @@ -119,7 +119,7 @@ public void FilterByDescription_ShouldReturnMatchingEvents() // Act var eventFilter = new EventFilter(events.AsQueryable()); - eventFilter.FilterByDescription("tech"); + eventFilter.FilterByNameOrDescription("tech"); var result = eventFilter.GetEvents().ToList(); // Assert diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 464170b..ad14287 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -9,12 +9,15 @@ using TickAPI.Categories.Abstractions; using TickAPI.Categories.DTOs.Request; using TickAPI.Categories.Models; +using TickAPI.Common.Blob.Abstractions; +using TickAPI.Common.Mail.Abstractions; using TickAPI.Common.Results; using TickAPI.Events.Models; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.Models; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; +using TickAPI.Customers.Abstractions; using TickAPI.Events.DTOs.Response; using TickAPI.Events.Services; using TickAPI.Tickets.Abstractions; @@ -111,10 +114,13 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); - - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act - var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail); + var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail, null); // Assert Assert.True(result.IsSuccess); @@ -184,11 +190,15 @@ public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnB var ticketServiceMock = new Mock(); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act - var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail); + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail, null); // Assert Assert.False(res.IsSuccess); @@ -239,10 +249,13 @@ public async Task CreateNewEventAsync_WhenTicketTypeAvailabilityIsAfterEventsEnd var ticketServiceMock = new Mock(); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act - var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail); + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail, null); // Assert Assert.False(res.IsSuccess); @@ -292,10 +305,14 @@ public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRe var ticketServiceMock = new Mock(); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act - var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail); + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, categories, ticketTypes, eventStatus, organizerEmail, null); // Assert Assert.False(res.IsSuccess); @@ -359,9 +376,12 @@ public async Task GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginate false, new PaginationDetails(1, 3) )); - - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.GetOrganizerEventsAsync(organizer, page, pageSize); @@ -411,8 +431,11 @@ public async Task GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() .Setup(p => p.PaginateAsync(organizerEvents, pageSize, page)) .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.GetOrganizerEventsAsync(organizer, page, pageSize); @@ -475,9 +498,12 @@ public async Task GetEventsAsync_WhenPaginationSucceeds_ShouldReturnPaginatedEve new PaginationDetails(1, 3) )); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); - + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); + // Act var result = await sut.GetEventsAsync(page, pageSize); @@ -521,8 +547,11 @@ public async Task GetEventsAsync_WhenPaginationFails_ShouldPropagateError() .Setup(p => p.PaginateAsync(eventsQueryable, pageSize, page)) .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.GetEventsAsync(page, pageSize); @@ -561,9 +590,12 @@ public async Task GetEventsPaginationDetailsAsync_WhenSuccessful_ShouldReturnPag .Setup(p => p.GetPaginationDetailsAsync(eventsQueryable, pageSize)) .ReturnsAsync(Result.Success(paginationDetails)); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); - + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); + // Act var result = await sut.GetEventsPaginationDetailsAsync(pageSize); @@ -599,9 +631,12 @@ public async Task GetEventsPaginationDetailsAsync_WhenFails_ShouldReturnError() .Setup(p => p.GetPaginationDetailsAsync(eventsQueryable, pageSize)) .ReturnsAsync(Result.Failure(StatusCodes.Status400BadRequest, "Invalid page size")); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); - + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.GetEventsPaginationDetailsAsync(pageSize); @@ -636,9 +671,12 @@ public async Task GetEventDetailsAsync_WhenSuccessful_ShouldReturnEventDetails() Result.Success((uint)(input.Price / 10)) ); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + var blobServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.GetEventDetailsAsync(@event.Id); @@ -673,9 +711,12 @@ public async Task GetEventDetailsAsync_WhenFails_ShouldReturnEventError() .Setup(m => m.GetEventByIdAsync(@event.Id)) .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"event with id {@event.Id} not found")); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, - dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); + + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.GetEventDetailsAsync(@event.Id); @@ -778,16 +819,12 @@ public async Task EditEventAsync_WhenDataValid_ShouldUpdateEvent() var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -852,16 +889,12 @@ public async Task EditEventAsync_WhenEventNotFound_ShouldReturnError() var categoryServiceMock = new Mock(); var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -929,16 +962,12 @@ public async Task EditEventAsync_WhenEndDateBeforeStartDate_ShouldReturnBadReque var categoryServiceMock = new Mock(); var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -1007,16 +1036,12 @@ public async Task EditEventAsync_WhenStartDateChangedAndInPast_ShouldReturnBadRe var categoryServiceMock = new Mock(); var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -1129,16 +1154,12 @@ public async Task EditEventAsync_StartDateNotChangedAndInPast_ShouldUpdateEvent( var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -1220,16 +1241,12 @@ public async Task EditEventAsync_WhenTicketTypeAvailableFromAfterEndDate_ShouldR var categoryServiceMock = new Mock(); var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -1301,16 +1318,12 @@ public async Task EditEventAsync_WhenAddressServiceFails_ShouldPropagateError() var categoryServiceMock = new Mock(); var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -1395,16 +1408,12 @@ public async Task EditEventAsync_WhenCategoryServiceFails_ShouldPropagateError() var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, @@ -1497,16 +1506,12 @@ public async Task EditEventAsync_WhenSaveEventFails_ShouldPropagateError() var paginationServiceMock = new Mock(); var ticketServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + var mailServiceMock = new Mock(); - var sut = new EventService( - eventRepositoryMock.Object, - organizerServiceMock.Object, - addressServiceMock.Object, - dateTimeServiceMock.Object, - paginationServiceMock.Object, - categoryServiceMock.Object, - ticketServiceMock.Object); + var blobServiceMock = new Mock(); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object, categoryServiceMock.Object, ticketServiceMock.Object, customerRepositoryMock.Object, mailServiceMock.Object, blobServiceMock.Object); // Act var result = await sut.EditEventAsync( organizer, diff --git a/TickAPI/TickAPI.Tests/Events/Utils.cs b/TickAPI/TickAPI.Tests/Events/Utils.cs index 1fa0c76..7980a47 100644 --- a/TickAPI/TickAPI.Tests/Events/Utils.cs +++ b/TickAPI/TickAPI.Tests/Events/Utils.cs @@ -51,7 +51,8 @@ public static GetEventResponseDto CreateSampleEventResponseDto(string name) new GetEventResponsePriceInfoDto(300, "PLN"), [new GetEventResponseCategoryDto("Test")], EventStatus.TicketsAvailable, - new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) + new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null), + null ); } @@ -69,7 +70,8 @@ [new GetEventResponseCategoryDto("Test")], new GetEventDetailsResponseTicketTypeDto(Guid.Parse("7ecfc61a-32d2-4124-a95c-cb5834a49990"), "Description #2", 300, "PLN", new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), 30), new GetEventDetailsResponseTicketTypeDto(Guid.Parse("7be2ae57-2394-4854-bf11-9567ce7e0ab6"), "Description #3", 200, "PLN", new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), 20)], EventStatus.TicketsAvailable, - new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) + new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null), + null ); } } diff --git a/TickAPI/TickAPI/Common/Blob/Abstractions/IBlobService.cs b/TickAPI/TickAPI/Common/Blob/Abstractions/IBlobService.cs new file mode 100644 index 0000000..f253b67 --- /dev/null +++ b/TickAPI/TickAPI/Common/Blob/Abstractions/IBlobService.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.Blob.Abstractions; + +public interface IBlobService +{ + public Task UploadToBlobContainerAsync(IFormFile image); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Blob/Services/BlobService.cs b/TickAPI/TickAPI/Common/Blob/Services/BlobService.cs new file mode 100644 index 0000000..b58a5c8 --- /dev/null +++ b/TickAPI/TickAPI/Common/Blob/Services/BlobService.cs @@ -0,0 +1,32 @@ +using Azure.Identity; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using TickAPI.Common.Blob.Abstractions; + +namespace TickAPI.Common.Blob.Services; + +public class BlobService : IBlobService +{ + private string _connectionString; + private string _containerName; + + public BlobService(IConfiguration configuration) + { + _connectionString = configuration["BlobStorage:ConnectionString"]; + _containerName = configuration["BlobStorage:ContainerName"]; + } + + public async Task UploadToBlobContainerAsync(IFormFile image) + { + var container = new BlobContainerClient(_connectionString, _containerName); + Guid id = Guid.NewGuid(); + string blobName = id.ToString(); + var blob = container.GetBlobClient(blobName); + var stream = new MemoryStream(); + await image.CopyToAsync(stream); + stream.Position = 0; + var response = await blob.UploadAsync(stream); + + return blob.Uri.ToString(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs b/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs index b7f3965..f4b3444 100644 --- a/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs +++ b/TickAPI/TickAPI/Common/Mail/Abstractions/IMailService.cs @@ -5,8 +5,7 @@ namespace TickAPI.Common.Mail.Abstractions; public interface IMailService { - public Task SendTicketAsync(string toEmail, string toLogin, string eventName, byte[] pdfData); + public Task SendTicketAsync(MailRecipient recipient, string eventName, byte[] pdfData); - public Task SendMailAsync(string toEmail, string toLogin, string subject, string content, - List? attachments); + public Task SendMailAsync(IEnumerable recipients, string subject, string content, List? attachments); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Mail/Models/MailAttachment.cs b/TickAPI/TickAPI/Common/Mail/Models/MailAttachment.cs index d59be62..c25dc4f 100644 --- a/TickAPI/TickAPI/Common/Mail/Models/MailAttachment.cs +++ b/TickAPI/TickAPI/Common/Mail/Models/MailAttachment.cs @@ -1,8 +1,7 @@ namespace TickAPI.Common.Mail.Models; -public class MailAttachment -{ - public string fileName { get; set; } - public string base64Content { get; set; } - public string fileType { get; set; } -} \ No newline at end of file +public record MailAttachment( + string FileName, + string Base64Content, + string FileType +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Mail/Models/MailRecipient.cs b/TickAPI/TickAPI/Common/Mail/Models/MailRecipient.cs new file mode 100644 index 0000000..dd0aa98 --- /dev/null +++ b/TickAPI/TickAPI/Common/Mail/Models/MailRecipient.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.Mail.Models; + +public record MailRecipient( + string Email, + string Login +); diff --git a/TickAPI/TickAPI/Common/Mail/Services/MailService.cs b/TickAPI/TickAPI/Common/Mail/Services/MailService.cs index 24456bb..b35b5c4 100644 --- a/TickAPI/TickAPI/Common/Mail/Services/MailService.cs +++ b/TickAPI/TickAPI/Common/Mail/Services/MailService.cs @@ -20,35 +20,29 @@ public MailService(IConfiguration configuration) _fromEmailAddress = new EmailAddress(fromEmail, fromName); } - public async Task SendTicketAsync(string toEmail, string toLogin, string eventName, byte[] pdfData) + public async Task SendTicketAsync(MailRecipient recipient, string eventName, byte[] pdfData) { var subject = $"Ticket for {eventName}"; var htmlContent = "Download your ticket from attachments"; var base64Content = Convert.ToBase64String(pdfData); - List attachments = - [ - new MailAttachment - { - base64Content = base64Content, - fileName = "ticket.pdf", - fileType = "application/pdf" - } + List attachments = [ + new MailAttachment("ticket.pdf", base64Content, "application/pdf") ]; - var res = await SendMailAsync(toEmail, toLogin, subject, htmlContent, attachments); + var res = await SendMailAsync([recipient], subject, htmlContent, attachments); return res; } - public async Task SendMailAsync(string toEmail, string toLogin, string subject, string content, + public async Task SendMailAsync(IEnumerable recipients, string subject, string content, List? attachments = null) { - var toEmailAddress = new EmailAddress(toEmail, toLogin); - var msg = MailHelper.CreateSingleEmail(_fromEmailAddress, toEmailAddress, subject, - null, content); + var toEmailAddresses = recipients.Select(r => new EmailAddress(r.Email, r.Login)).ToList(); + var msg = MailHelper.CreateSingleEmailToMultipleRecipients(_fromEmailAddress, toEmailAddresses, subject, null, content); + if (attachments != null) { foreach (var a in attachments) { - msg.AddAttachment(a.fileName, a.base64Content, a.fileType); + msg.AddAttachment(a.FileName, a.Base64Content, a.FileType); } } diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 4cd7bab..b41656a 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -7,4 +7,5 @@ public interface ICustomerRepository { Task> GetCustomerByEmailAsync(string customerEmail); Task AddNewCustomerAsync(Customer customer); + IQueryable GetCustomersWithTicketForEvent(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index ed131d5..f1d1c6e 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -32,4 +32,10 @@ public async Task AddNewCustomerAsync(Customer customer) _tickApiDbContext.Customers.Add(customer); await _tickApiDbContext.SaveChangesAsync(); } + + public IQueryable GetCustomersWithTicketForEvent(Guid eventId) + { + return _tickApiDbContext.Customers + .Where(c => c.Tickets.Any(t => t.Type.Event.Id == eventId)); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventFilter.cs b/TickAPI/TickAPI/Events/Abstractions/IEventFilter.cs index a4aa175..4681384 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventFilter.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventFilter.cs @@ -5,8 +5,7 @@ namespace TickAPI.Events.Abstractions; public interface IEventFilter { IQueryable GetEvents(); - void FilterByName(string name); - void FilterByDescription(string description); + void FilterByNameOrDescription(string name); void FilterByStartDate(DateTime startDate); void FilterByMinStartDate(DateTime startDate); void FilterByMaxStartDate(DateTime startDate); diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs index a7e4cb1..4a2103e 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs @@ -13,4 +13,6 @@ public interface IEventRepository public Task> GetEventByIdAsync(Guid eventId); public Task SaveEventAsync(Event ev); public Task> GetEventByIdAndOrganizerAsync(Guid eventId, Organizer organizer); + public Task GetEventRevenue(Guid eventId); + public Task GetEventSoldTicketsCount(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index b029b39..af2f26c 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -1,6 +1,7 @@ using TickAPI.Addresses.DTOs.Request; using TickAPI.Common.Pagination.Responses; using TickAPI.Categories.DTOs.Request; +using TickAPI.Common.Results; using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; using TickAPI.Events.DTOs.Request; @@ -14,11 +15,13 @@ public interface IEventService { public Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, List categories, - List ticketTypes,EventStatus eventStatus, string organizerEmail); + List ticketTypes,EventStatus eventStatus, string organizerEmail, IFormFile? images); public Task>> GetOrganizerEventsAsync(Organizer organizer, int page, int pageSize, EventFiltersDto? eventFilters = null); public Task> GetOrganizerEventsPaginationDetailsAsync(Organizer organizer, int pageSize); public Task>> GetEventsAsync(int page, int pageSize, EventFiltersDto? eventFilters = null); public Task> GetEventsPaginationDetailsAsync(int pageSize); public Task> GetEventDetailsAsync(Guid eventId); public Task> EditEventAsync(Organizer organizer, Guid eventId, string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto editAddress, List categories, EventStatus eventStatus); + public Task SendMessageToParticipants(Organizer organizer, Guid eventId, string subject, string message); + public Task> GetEventDetailsOrganizerAsync(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventsController.cs b/TickAPI/TickAPI/Events/Controllers/EventsController.cs index 95cbf68..9cdaa44 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventsController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventsController.cs @@ -28,7 +28,7 @@ public EventsController(IEventService eventService, IClaimsService claimsService [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPost] - public async Task> CreateEvent([FromBody] CreateEventDto request) + public async Task> CreateEvent([FromForm] CreateEventDto request) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -39,7 +39,7 @@ public async Task> CreateEvent([FromBody] C var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.CreateAddress, request.Categories - , request.TicketTypes ,request.EventStatus, email); + , request.TicketTypes ,request.EventStatus, email, request.Image); if (newEventResult.IsError) return newEventResult.ToObjectResult(); @@ -115,6 +115,14 @@ public async Task> GetEventDetails([Fro return eventDetailsResult.ToObjectResult(); } + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpGet("organizer/{id:guid}")] + public async Task> GetEventDetailsOrganizer([FromRoute] Guid id) + { + var eventDetailsResult = await _eventService.GetEventDetailsOrganizerAsync(id); + return eventDetailsResult.ToObjectResult(); + } + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPatch("{id:guid}")] public async Task> EditEvent([FromRoute] Guid id, [FromBody] EditEventDto request) @@ -141,4 +149,26 @@ public async Task> EditEvent([FromRoute] Guid return Ok("Event edited succesfully"); } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpPost("{id:guid}/message-to-participants")] + public async Task SendMessageToEventParticipants([FromRoute] Guid id, [FromBody] SendMessageToParticipantsDto request) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return emailResult.ToObjectResult(); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + { + return organizerResult.ToObjectResult(); + } + var organizer = organizerResult.Value!; + + var result = await _eventService.SendMessageToParticipants(organizer, id, request.Subject, request.Message); + return result.ToObjectResult(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs index 5b6f447..064ae42 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs @@ -14,5 +14,6 @@ public record CreateEventDto( List Categories, List TicketTypes, EventStatus EventStatus, - CreateAddressDto CreateAddress + CreateAddressDto CreateAddress, + IFormFile? Image ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/SendMessageToParticipantsDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/SendMessageToParticipantsDto.cs new file mode 100644 index 0000000..b3e617e --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Request/SendMessageToParticipantsDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Events.DTOs.Request; + +public record SendMessageToParticipantsDto( + string Subject, + string Message +); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsOrganizerResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsOrganizerResponseDto.cs new file mode 100644 index 0000000..94e0feb --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsOrganizerResponseDto.cs @@ -0,0 +1,22 @@ +using TickAPI.Events.Models; +using TickAPI.Events.DTOs.Response; + +namespace TickAPI.Events.DTOs.Response; + + +public record GetEventDetailsOrganizerResponseDto( + Guid Id, + string Name, + string Description, + DateTime StartDate, + DateTime EndDate, + uint? MinimumAge, + List Categories, + List TicketTypes, + EventStatus Status, + GetEventResponseAddressDto Address, + decimal Revenue, + int SoldTicketsCount + ); + + \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsResponseDto.cs index c5ea721..60e2862 100644 --- a/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsResponseDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventDetailsResponseDto.cs @@ -12,5 +12,6 @@ public record GetEventDetailsResponseDto( List Categories, List TicketTypes, EventStatus Status, - GetEventResponseAddressDto Address + GetEventResponseAddressDto Address, + string? ImageUrl ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs index a280251..6c15242 100644 --- a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs @@ -13,5 +13,6 @@ public record GetEventResponseDto( GetEventResponsePriceInfoDto MaximumPrice, List Categories, EventStatus Status, - GetEventResponseAddressDto Address + GetEventResponseAddressDto Address, + string? ImageUrl ); diff --git a/TickAPI/TickAPI/Events/Filters/EventFilter.cs b/TickAPI/TickAPI/Events/Filters/EventFilter.cs index 0e0f017..f7020a5 100644 --- a/TickAPI/TickAPI/Events/Filters/EventFilter.cs +++ b/TickAPI/TickAPI/Events/Filters/EventFilter.cs @@ -17,16 +17,11 @@ public IQueryable GetEvents() return _events; } - public void FilterByName(string name) + public void FilterByNameOrDescription(string searchQuery) { - _events = _events.Where(e => e.Name.ToLower().Contains(name.ToLower())); + _events = _events.Where(e => e.Name.ToLower().Contains(searchQuery.ToLower()) || e.Description.ToLower().Contains(searchQuery.ToLower())); } - - public void FilterByDescription(string description) - { - _events = _events.Where(e => e.Description.ToLower().Contains(description.ToLower())); - } - + public void FilterByStartDate(DateTime startDate) { _events = _events.Where(e => e.StartDate.Date == startDate.Date); diff --git a/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs b/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs index 7a3aa80..1720833 100644 --- a/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs +++ b/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs @@ -14,11 +14,7 @@ public EventFilterApplier(IEventFilter eventFilter) _eventFilter = eventFilter; _filterActions = new Dictionary, Action> { - { f => !string.IsNullOrEmpty(f.SearchQuery), f => - { - _eventFilter.FilterByName(f.SearchQuery!); - _eventFilter.FilterByDescription(f.SearchQuery!); - } + { f => !string.IsNullOrEmpty(f.SearchQuery), f => _eventFilter.FilterByNameOrDescription(f.SearchQuery!) }, { f => f.StartDate.HasValue, f => _eventFilter.FilterByStartDate(f.StartDate!.Value) }, { f => f.MinStartDate.HasValue, f => _eventFilter.FilterByMinStartDate(f.MinStartDate!.Value) }, diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index 50ca434..8bdb9cd 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -18,6 +18,7 @@ public class Event public ICollection TicketTypes { get; set; } public EventStatus EventStatus { get; set; } public Address Address { get; set; } + public string? ImageUrl { get; set; } } public enum EventStatus diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index bfd667e..60c95aa 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -75,4 +75,26 @@ public async Task> GetEventByIdAndOrganizerAsync(Guid eventId, Org } return Result.Success(ev); } + + public async Task GetEventRevenue(Guid eventId) + { + var query = from tickets in _tickApiDbContext.Tickets + join _ticketTypes in _tickApiDbContext.TicketTypes on tickets.Type.Id equals _ticketTypes.Id + join events in _tickApiDbContext.Events on _ticketTypes.Event.Id equals events.Id + where events.Id == eventId + select new { price = _ticketTypes.Price }; + var val = await query.SumAsync(x => x.price); + return val; + } + + public async Task GetEventSoldTicketsCount(Guid eventId) + { + var query = from tickets in _tickApiDbContext.Tickets + join _ticketTypes in _tickApiDbContext.TicketTypes on tickets.Type.Id equals _ticketTypes.Id + join events in _tickApiDbContext.Events on _ticketTypes.Event.Id equals events.Id + where events.Id == eventId + select new { id = tickets.Id }; + var val = await query.CountAsync(); + return val; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 91cdda2..546da79 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -1,14 +1,18 @@ -using TickAPI.Addresses.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.DTOs.Request; using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; using TickAPI.Categories.Abstractions; using TickAPI.Categories.DTOs.Request; +using TickAPI.Common.Mail.Abstractions; +using TickAPI.Common.Mail.Models; using TickAPI.Common.Results; using TickAPI.Common.Time.Abstractions; using TickAPI.Events.Abstractions; using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Abstractions; using TickAPI.Events.DTOs.Request; using TickAPI.Events.DTOs.Response; using TickAPI.Events.Filters; @@ -17,6 +21,7 @@ using TickAPI.Tickets.Abstractions; using TickAPI.TicketTypes.DTOs.Request; using TickAPI.TicketTypes.Models; +using TickAPI.Common.Blob.Abstractions; namespace TickAPI.Events.Services; @@ -29,8 +34,11 @@ public class EventService : IEventService private readonly IPaginationService _paginationService; private readonly ICategoryService _categoryService; private readonly ITicketService _ticketService; + private readonly ICustomerRepository _customerRepository; + private readonly IMailService _mailService; + private readonly IBlobService _blobService; - public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService, IDateTimeService dateTimeService, IPaginationService paginationService, ICategoryService categoryService, ITicketService ticketService) + public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService, IDateTimeService dateTimeService, IPaginationService paginationService, ICategoryService categoryService, ITicketService ticketService, ICustomerRepository customerRepository, IMailService mailService, IBlobService blobService) { _eventRepository = eventRepository; _organizerService = organizerService; @@ -39,11 +47,14 @@ public EventService(IEventRepository eventRepository, IOrganizerService organize _paginationService = paginationService; _categoryService = categoryService; _ticketService = ticketService; + _customerRepository = customerRepository; + _mailService = mailService; + _blobService = blobService; } public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, List categories, List ticketTypes, - EventStatus eventStatus, string organizerEmail) + EventStatus eventStatus, string organizerEmail, IFormFile? image) { var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); if (!organizerResult.IsSuccess) @@ -74,7 +85,19 @@ public async Task> CreateNewEventAsync(string name, string descri { return Result.PropagateError(categoriesByNameResult); } - + + string? imageUrl = null; + if (image != null) + { + try + { + imageUrl = await _blobService.UploadToBlobContainerAsync(image); + } + catch (Exception e) + { + return Result.Failure(statusCode:500, e.Message); + } + } var @event = new Event { Name = name, @@ -87,6 +110,7 @@ public async Task> CreateNewEventAsync(string name, string descri Organizer = organizerResult.Value!, EventStatus = eventStatus, TicketTypes = ticketTypesConverted, + ImageUrl = imageUrl }; await _eventRepository.AddNewEventAsync(@event); return Result.Success(@event); @@ -170,12 +194,40 @@ public async Task> GetEventDetailsAsync(Guid categories, ticketTypes, ev.EventStatus, - address + address, + ev.ImageUrl ); return Result.Success(details); } + public async Task> GetEventDetailsOrganizerAsync(Guid eventId) + { + var details = await GetEventDetailsAsync(eventId); + if (details.IsError) + { + return Result.PropagateError(details); + } + + var val = await _eventRepository.GetEventRevenue(eventId); + var count = await _eventRepository.GetEventSoldTicketsCount(eventId); + var ev = details.Value!; + + var ret = new GetEventDetailsOrganizerResponseDto( ev.Id, + ev.Name, + ev.Description, + ev.StartDate, + ev.EndDate, + ev.MinimumAge, + ev.Categories, + ev.TicketTypes, + ev.Status, + ev.Address, + val, + count); + return Result.Success(ret); + } + public async Task> EditEventAsync(Organizer organizer, Guid eventId, string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto editAddress, List categories, EventStatus eventStatus) { @@ -221,6 +273,21 @@ public async Task> EditEventAsync(Organizer organizer, Guid eventI return Result.Success(existingEvent); } + public async Task SendMessageToParticipants(Organizer organizer, Guid eventId, string subject, string message) + { + var eventResult = await _eventRepository.GetEventByIdAndOrganizerAsync(eventId, organizer); + if (eventResult.IsError) + { + return Result.PropagateError(eventResult); + } + var ev = eventResult.Value!; + + var eventParticipants = await _customerRepository.GetCustomersWithTicketForEvent(ev.Id).ToListAsync(); + var recipients = eventParticipants.Select(p => new MailRecipient(p.Email, $"{p.FirstName} {p.LastName}")); + + return await _mailService.SendMailAsync(recipients, subject, message, null); + } + private async Task>> GetPaginatedEventsAsync(IQueryable events, int page, int pageSize) { var paginatedEventsResult = await _paginationService.PaginateAsync(events, pageSize, page); @@ -259,7 +326,7 @@ private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) var maximumPrice = new GetEventResponsePriceInfoDto(ttMaximumPrice.Price, ttMaximumPrice.Currency); return new GetEventResponseDto(ev.Id, ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, - minimumPrice, maximumPrice, categories, ev.EventStatus, address); + minimumPrice, maximumPrice, categories, ev.EventStatus, address, ev.ImageUrl); } private Result CheckEventDates(DateTime startDate, DateTime endDate, IEnumerable ticketTypes, bool skipStartDateEvaluation = false) diff --git a/TickAPI/TickAPI/Migrations/20250613140026_blobmigration.Designer.cs b/TickAPI/TickAPI/Migrations/20250613140026_blobmigration.Designer.cs new file mode 100644 index 0000000..255440b --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250613140026_blobmigration.Designer.cs @@ -0,0 +1,400 @@ +// +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("20250613140026_blobmigration")] + partial class blobmigration + { + /// + 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("ImageUrl") + .HasColumnType("nvarchar(max)"); + + 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("ResellCurrency") + .HasColumnType("nvarchar(max)"); + + b.Property("ResellPrice") + .HasColumnType("decimal(18,2)"); + + 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/20250613140026_blobmigration.cs b/TickAPI/TickAPI/Migrations/20250613140026_blobmigration.cs new file mode 100644 index 0000000..d39274a --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250613140026_blobmigration.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class blobmigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageUrl", + table: "Events", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageUrl", + table: "Events"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 71d888e..18032ed 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -179,6 +179,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EventStatus") .HasColumnType("int"); + b.Property("ImageUrl") + .HasColumnType("nvarchar(max)"); + b.Property("MinimumAge") .HasColumnType("bigint"); diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 53441b3..5a983d6 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -33,6 +33,8 @@ using TickAPI.Categories.Abstractions; using TickAPI.Categories.Respositories; using TickAPI.Categories.Services; +using TickAPI.Common.Blob.Abstractions; +using TickAPI.Common.Blob.Services; using TickAPI.Common.Claims.Abstractions; using TickAPI.Common.Claims.Services; using TickAPI.Common.Redis.Abstractions; @@ -146,6 +148,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index d8be591..e93eea9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -15,4 +15,7 @@ public interface IShoppingCartRepository public Task> IncrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); public Task> DecrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); public Task RemoveAmountOfTicketTypeAsync(Guid ticketTypeId); + public Task AddResellTicketToCartAsync(string customerEmail, Guid ticketId); + public Task> CheckResellTicketAvailabilityAsync(Guid ticketId); + public Task RemoveResellTicketFromCartAsync(string customerEmail, Guid ticketId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 787e62e..609ff79 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -8,8 +8,10 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); + public Task AddResellTicketToCartAsync(Guid ticketId, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); + public Task RemoveResellTicketFromCartAsync(Guid ticketId, string customerEmail); public Task>> GetDueAmountAsync(string customerEmail); public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv); diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 373d939..88542c5 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -39,6 +39,22 @@ await _shoppingCartService.AddNewTicketsToCartAsync(addTicketDto.TicketTypeId, a return addTicketResult.ToObjectResult(); } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost("{ticketId:guid}")] + public async Task AddResellTicket([FromRoute] Guid ticketId) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return emailResult.ToObjectResult(); + } + var email = emailResult.Value!; + + var addTicketResult = await _shoppingCartService.AddResellTicketToCartAsync(ticketId, email); + + return addTicketResult.ToObjectResult(); + } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpGet] @@ -74,6 +90,22 @@ await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketT return removeTicketResult.ToObjectResult(); } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpDelete("{ticketId:guid}")] + public async Task RemoveResellTicket([FromRoute] Guid ticketId) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return emailResult.ToObjectResult(); + } + var email = emailResult.Value!; + + var removeTicketResult = await _shoppingCartService.RemoveResellTicketFromCartAsync(ticketId, email); + + return removeTicketResult.ToObjectResult(); + } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpGet("due")] public async Task>> GetDueAmount() diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs index 9e75a25..a63bb03 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -29,8 +29,8 @@ public static GetShoppingCartTicketsResellTicketDetailsResponseDto ticket.Type.Description, ticket.Type.Event.Organizer.DisplayName, ticket.Owner.Email, - ticket.Type.Price, - ticket.Type.Currency + ticket.ResellPrice ?? ticket.Type.Price, + ticket.ResellCurrency ?? ticket.Type.Currency ); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 493afa5..1fb7188 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -254,6 +254,131 @@ public async Task RemoveAmountOfTicketTypeAsync(Guid ticketTypeId) return Result.Success(); } + public async Task AddResellTicketToCartAsync(string customerEmail, Guid ticketId) + { + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + cart.ResellTickets.Add(new ShoppingCartResellTicket {TicketId = ticketId}); + + var setKeyResult = await SetTicketKeyAsync(ticketId); + + if (setKeyResult.IsError) + { + return Result.PropagateError(setKeyResult); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); + } + + public async Task> CheckResellTicketAvailabilityAsync(Guid ticketId) + { + bool exists; + + try + { + exists = await _redisService.KeyExistsAsync(GetResellTicketKey(ticketId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(!exists); + } + + public async Task RemoveResellTicketFromCartAsync(string customerEmail, Guid ticketId) + { + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var existingEntry = cart.ResellTickets.FirstOrDefault(t => t.TicketId == ticketId); + + if (existingEntry is null) + { + return Result.Failure(StatusCodes.Status404NotFound, "the shopping cart does not contain this ticket"); + } + + cart.ResellTickets.Remove(existingEntry); + + var deleteKeyResult = await DeleteTicketKeyAsync(ticketId); + + if (deleteKeyResult.IsError) + { + return Result.PropagateError(deleteKeyResult); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); + } + + private async Task SetTicketKeyAsync(Guid ticketId) + { + bool success; + + try + { + success = await _redisService.SetStringAsync(GetResellTicketKey(ticketId), string.Empty, _defaultExpiry); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (!success) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the ticket key could not be updated"); + } + + return Result.Success(); + } + + private async Task DeleteTicketKeyAsync(Guid ticketId) + { + bool success; + + try + { + success = await _redisService.DeleteKeyAsync(GetResellTicketKey(ticketId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (!success) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the ticket key could not be deleted"); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; @@ -263,4 +388,9 @@ private static string GetAmountKey(Guid ticketTypeId) { return $"amount:{ticketTypeId}"; } + + private static string GetResellTicketKey(Guid ticketId) + { + return $"resell:{ticketId}"; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index b4032e5..2f01bc6 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -4,10 +4,12 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; using TickAPI.ShoppingCarts.Mappers; +using TickAPI.ShoppingCarts.Models; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Abstractions; @@ -54,6 +56,49 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun return Result.Success(); } + public async Task AddResellTicketToCartAsync(Guid ticketId, string customerEmail) + { + var ticketResult = await _ticketService.GetTicketByIdAsync(ticketId); + + if (ticketResult.IsError) + { + return Result.PropagateError(ticketResult); + } + + var ticket = ticketResult.Value!; + + if (!ticket.ForResell) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"chosen ticket is not available for resell"); + } + + if (ticket.Owner.Email == customerEmail) + { + return Result.Failure(StatusCodes.Status403Forbidden, "you can't buy ticket sold from your account"); + } + + var availabilityResult = await _shoppingCartRepository.CheckResellTicketAvailabilityAsync(ticketId); + + if (availabilityResult.IsError) + { + return Result.PropagateError(availabilityResult); + } + + if (!availabilityResult.Value) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"the ticket you are trying to add isn't currently available"); + } + + var addTicketToCartResult = await _shoppingCartRepository.AddResellTicketToCartAsync(customerEmail, ticketId); + + if (addTicketToCartResult.IsError) + { + return Result.PropagateError(addTicketToCartResult); + } + + return Result.Success(); + } + public async Task> GetTicketsFromCartAsync(string customerEmail) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); @@ -66,6 +111,7 @@ public async Task> GetTicketsFromCartA var cart = getShoppingCartResult.Value!; var newTickets = new List(); + var resellTickets = new List(); foreach (var ticket in cart.NewTickets) { @@ -82,10 +128,24 @@ public async Task> GetTicketsFromCartA newTickets.Add(newTicket); } + + foreach (var ticket in cart.ResellTickets) + { + var resellTicketResult = await _ticketService.GetTicketByIdAsync(ticket.TicketId); + + if (resellTicketResult.IsError) + { + return Result.PropagateError(resellTicketResult); + } + + var resellTicket = + ShoppingCartMapper.MapTicketToGetShoppingCartTicketsResellTicketDetailsResponseDto(resellTicketResult + .Value!); + + resellTickets.Add(resellTicket); + } - // TODO: Add resell ticket parsing - - var result = new GetShoppingCartTicketsResponseDto(newTickets, []); + var result = new GetShoppingCartTicketsResponseDto(newTickets, resellTickets); return Result.Success(result); } @@ -102,6 +162,18 @@ public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint return Result.Success(); } + public async Task RemoveResellTicketFromCartAsync(Guid ticketId, string customerEmail) + { + var removeTicketFromCartResult = await _shoppingCartRepository.RemoveResellTicketFromCartAsync(customerEmail, ticketId); + + if (removeTicketFromCartResult.IsError) + { + return Result.PropagateError(removeTicketFromCartResult); + } + + return Result.Success(); + } + public async Task>> GetDueAmountAsync(string customerEmail) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); @@ -135,8 +207,41 @@ public async Task>> GetDueAmountAsync(string dueAmount.Add(ticketType.Currency, newTicket.Quantity * ticketType.Price); } } - - // TODO: Add resell tickets to the calculations + + foreach (var resellTicket in cart.ResellTickets) + { + var ticketResult = await _ticketService.GetTicketByIdAsync(resellTicket.TicketId); + + if (ticketResult.IsError) + { + return Result>.PropagateError(ticketResult); + } + + var ticket = ticketResult.Value!; + + if (ticket.ResellPrice is not null && ticket.ResellCurrency is not null) + { + if (dueAmount.ContainsKey(ticket.ResellCurrency)) + { + dueAmount[ticket.ResellCurrency] += ticket.ResellPrice.Value; + } + else + { + dueAmount.Add(ticket.ResellCurrency, ticket.ResellPrice.Value); + } + } + else + { + if (dueAmount.ContainsKey(ticket.Type.Currency)) + { + dueAmount[ticket.Type.Currency] += ticket.Type.Price; + } + else + { + dueAmount.Add(ticket.Type.Currency, ticket.Type.Price); + } + } + } return Result>.Success(dueAmount); } @@ -174,26 +279,11 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc 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); + return Result.PropagateError(getShoppingCartResult); } var cart = getShoppingCartResult.Value!; @@ -202,10 +292,32 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri if (getCustomerResult.IsError) { - return Result.PropagateError(getCustomerResult); + return Result.PropagateError(getCustomerResult); } var owner = getCustomerResult.Value!; + + var generateTicketsResult = await GenerateBoughtTicketsAsync(cart, owner, currency); + + if (generateTicketsResult.IsError) + { + return Result.PropagateError(generateTicketsResult); + } + + var passOwnershipResult = await PassTicketOwnershipAsync(cart, owner, currency); + + if (passOwnershipResult.IsError) + { + return Result.PropagateError(passOwnershipResult); + } + + var payment = paymentResult.Value!; + + return Result.Success(payment); + } + + private async Task GenerateBoughtTicketsAsync(ShoppingCart cart, Customer owner, string currency) + { var removals = new List<(Guid id, uint amount)>(); foreach (var ticket in cart.NewTickets) @@ -225,7 +337,6 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri 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) @@ -238,7 +349,48 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri foreach (var (id, amount) in removals) { - var removalResult = await RemoveNewTicketsFromCartAsync(id, amount, customerEmail); + var removalResult = await RemoveNewTicketsFromCartAsync(id, amount, owner.Email); + + if (removalResult.IsError) + { + return Result.PropagateError(removalResult); + } + } + + return Result.Success(); + } + + private async Task PassTicketOwnershipAsync(ShoppingCart cart, Customer newOwner, string currency) + { + var removals = new List(); + + foreach (var resellTicket in cart.ResellTickets) + { + var ticketResult = await _ticketService.GetTicketByIdAsync(resellTicket.TicketId); + + if (ticketResult.IsError) + { + return Result.PropagateError(ticketResult); + } + + var ticket = ticketResult.Value!; + + if ((ticket.ResellCurrency ?? ticket.Type.Currency) == currency) + { + removals.Add(ticket.Id); + + var createTicketResult = await _ticketService.ChangeTicketOwnershipViaResellAsync(ticket, newOwner); + + if (createTicketResult.IsError) + { + return Result.PropagateError(createTicketResult); + } + } + } + + foreach (var id in removals) + { + var removalResult = await RemoveResellTicketFromCartAsync(id, newOwner.Email); if (removalResult.IsError) { diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index fb79365..adc0f04 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -8,6 +8,7 @@ + diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 4add328..e441b09 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; @@ -14,4 +15,6 @@ public interface ITicketRepository public Task MarkTicketAsUsed(Guid id); public Task SetTicketForResell(Guid ticketId, decimal newPrice, string currency); public Task AddTicketAsync(Ticket ticket); + public Task> GetTicketWithDetailsByIdAsync(Guid id); + public Task ChangeTicketOwnershipAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 1fce588..30c46a2 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -4,6 +4,7 @@ using TickAPI.Customers.Models; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; +using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Abstractions; @@ -20,9 +21,10 @@ public Task>> GetTicketsForCustome public Task ScanTicket(Guid ticketGuid); public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); - public Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice, string resellCurrency); + public Task> GetTicketByIdAsync(Guid ticketId); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, string? seats = null); + public Task ChangeTicketOwnershipViaResellAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs index 17f4fc5..2a5ecc8 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs @@ -14,5 +14,6 @@ public record GetTicketDetailsResponseDto DateTime EndDate, GetTicketDetailsAddressDto Address, Guid eventId, - string qrcode + string qrcode, + bool Used ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index c3d3911..90d3244 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -88,6 +88,56 @@ public async Task AddTicketAsync(Ticket ticket) return Result.Success(); } + public async Task> GetTicketWithDetailsByIdAsync(Guid id) + { + 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) + .Include(t => t.Owner) + .Where(t => t.Id == id) + .FirstOrDefaultAsync(); + if (ticket == null) + { + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); + } + return Result.Success(ticket); + } + + public async Task ChangeTicketOwnershipAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null) + { + var ticketFromDb = await _tickApiDbContext.Tickets + .Include(t => t.Owner) // Include if needed + .FirstOrDefaultAsync(t => t.Id == ticket.Id); + + var newOwnerFromDb = await _tickApiDbContext.Customers + .FirstOrDefaultAsync(c => c.Id == newOwner.Id); + + if (ticketFromDb == null || newOwnerFromDb == null) + { + return Result.Failure(StatusCodes.Status404NotFound, "Ticket or new owner not found"); + } + if (!ticketFromDb.ForResell) + { + return Result.Failure(StatusCodes.Status400BadRequest, "This ticket can't have its ownership passed"); + } + if (ticketFromDb.Owner.Id == newOwnerFromDb.Id) + { + return Result.Failure(StatusCodes.Status400BadRequest, "You can't change the owner of the ticket to be the same"); + } + + ticketFromDb.Owner = newOwnerFromDb; + ticketFromDb.NameOnTicket = nameOnTicket ?? $"{newOwnerFromDb.FirstName} {newOwnerFromDb.LastName}"; + ticketFromDb.ForResell = false; + ticketFromDb.ResellCurrency = null; + ticketFromDb.ResellPrice = null; + + await _tickApiDbContext.SaveChangesAsync(); + + return Result.Success(); + } + public async Task SetTicketForResell(Guid ticketId, decimal newPrice, string currency) { var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == ticketId); diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index c643424..c9de357 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -93,10 +93,29 @@ public async Task>> GetTicke { 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)); + t => + { + decimal price; + string currency; + if (t.ResellPrice is not null && t.ResellCurrency is not null) + { + price = t.ResellPrice.Value; + currency = t.ResellCurrency; + } + else + { + price = t.Type.Price; + currency = t.Type.Currency; + } + + return new GetTicketForResellResponseDto(t.Id, price, currency, t.Type.Description, + t.Seats); + }); return Result>.Success(paginatedResult); } + public async Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize, TicketFiltersDto ? ticketFilters = null) { var customerTickets = _ticketRepository.GetTicketsByCustomerEmail(email); @@ -144,11 +163,19 @@ public async Task> GetTicketDetailsAsync(Gui ticket.Type.Event.EndDate, address, ticket.Type.Event.Id, - qrcode + qrcode, + ticket.Used ); return Result.Success(ticketDetails); } + public async Task> GetTicketByIdAsync(Guid ticketId) + { + var ticketResult = await _ticketRepository.GetTicketWithDetailsByIdAsync(ticketId); + + return ticketResult; + } + public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) { var ticketTypeResult = await _ticketTypeRepository.GetTicketTypeByIdAsync(ticketTypeId); @@ -168,7 +195,7 @@ public async Task CreateTicketAsync(TicketType type, Customer owner, str { Type = type, Owner = owner, - NameOnTicket = nameOnTicket, + NameOnTicket = nameOnTicket ?? owner.FirstName + " " + owner.LastName, Seats = seats, ForResell = false, Used = false, @@ -179,6 +206,13 @@ public async Task CreateTicketAsync(TicketType type, Customer owner, str return addTicketResult; } + public async Task ChangeTicketOwnershipViaResellAsync(Ticket ticket, Customer newOwner, string? nameOnTicket = null) + { + var updateTicketResult = await _ticketRepository.ChangeTicketOwnershipAsync(ticket, newOwner); + + return updateTicketResult; + } + public async Task ScanTicket(Guid ticketGuid) { var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index c7e46ab..cb999f1 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -37,5 +37,9 @@ "ShoppingCart": { "SyncIntervalMinutes": 5, "LifetimeMinutes": 15 + }, + "BlobStorage": { + "ConnectionString": "string", + "ContainerName": "resellio" } } \ No newline at end of file