diff --git a/src/main/java/alfio/controller/api/admin/AdminWaitingQueueApiController.java b/src/main/java/alfio/controller/api/admin/AdminWaitingQueueApiController.java
index 04cf9a7e1e..8f38721fc5 100644
--- a/src/main/java/alfio/controller/api/admin/AdminWaitingQueueApiController.java
+++ b/src/main/java/alfio/controller/api/admin/AdminWaitingQueueApiController.java
@@ -26,18 +26,17 @@
import alfio.util.ClockProvider;
import alfio.util.EventUtil;
import alfio.util.ExportUtils;
+import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
-import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.time.ZonedDateTime;
import java.util.*;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import static alfio.model.system.ConfigurationKeys.STOP_WAITING_QUEUE_SUBSCRIPTIONS;
@@ -69,8 +68,11 @@ private Map loadStatus(Event event) {
List stcList = eventManager.loadTicketCategories(event)
.stream()
.filter(tc -> !tc.isAccessRestricted())
- .map(tc -> new SaleableTicketCategory(tc, now, event, ticketReservationManager.countAvailableTickets(event, tc), tc.getMaxTickets(), null))
- .collect(Collectors.toList());
+ .map(tc -> {
+ var categoryAvailability = ticketReservationManager.countAvailableTickets(event, tc);
+ return new SaleableTicketCategory(tc, now, event, categoryAvailability, tc.getMaxTickets(), null);
+ })
+ .toList();
boolean active = EventUtil.checkWaitingQueuePreconditions(event, stcList, configurationManager, eventStatisticsManager.noSeatsAvailable());
boolean paused = active && configurationManager.getFor(STOP_WAITING_QUEUE_SUBSCRIPTIONS, event.getConfigurationLevel()).getValueAsBooleanOrDefault();
Map result = new HashMap<>();
diff --git a/src/main/java/alfio/controller/api/v2/model/ItemsByCategory.java b/src/main/java/alfio/controller/api/v2/model/ItemsByCategory.java
index 678c36bbce..fb97678485 100644
--- a/src/main/java/alfio/controller/api/v2/model/ItemsByCategory.java
+++ b/src/main/java/alfio/controller/api/v2/model/ItemsByCategory.java
@@ -16,28 +16,17 @@
*/
package alfio.controller.api.v2.model;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
import java.util.List;
-@AllArgsConstructor
-@Getter
-public class ItemsByCategory {
-
- private final List ticketCategories;
- private final List expiredCategories;
- private final List additionalServices;
-
- private final boolean waitingList;
- private final boolean preSales;
- private final List ticketCategoriesForWaitingList;
-
+public record ItemsByCategory(
+ List ticketCategories,
+ List expiredCategories,
+ List additionalServices,
+ boolean waitingList,
+ boolean preSales,
+ List ticketCategoriesForWaitingList
+) {
- @AllArgsConstructor
- @Getter
- public static class TicketCategoryForWaitingList {
- private final int id;
- private final String name;
+ public record TicketCategoryForWaitingList(int id, String name) {
}
}
diff --git a/src/main/java/alfio/controller/api/v2/model/TicketCategory.java b/src/main/java/alfio/controller/api/v2/model/TicketCategory.java
index 8ef45d5fff..478e1a114e 100644
--- a/src/main/java/alfio/controller/api/v2/model/TicketCategory.java
+++ b/src/main/java/alfio/controller/api/v2/model/TicketCategory.java
@@ -43,6 +43,7 @@ public class TicketCategory {
//
private final boolean expired;
private final boolean saleInFuture;
+ private final boolean containsPendingTickets;
private final Map formattedInception;
private final Map formattedExpiration;
private final boolean saleableAndLimitNotReached;
@@ -73,6 +74,7 @@ public TicketCategory(SaleableTicketCategory saleableTicketCategory,
//
this.expired = saleableTicketCategory.getExpired();
this.saleInFuture = saleableTicketCategory.getSaleInFuture();
+ this.containsPendingTickets = saleableTicketCategory.hasPendingTickets();
this.formattedInception = formattedInception;
this.formattedExpiration = formattedExpiration;
//
diff --git a/src/main/java/alfio/controller/decorator/SaleableTicketCategory.java b/src/main/java/alfio/controller/decorator/SaleableTicketCategory.java
index 6d3180ae08..cce622d4b7 100644
--- a/src/main/java/alfio/controller/decorator/SaleableTicketCategory.java
+++ b/src/main/java/alfio/controller/decorator/SaleableTicketCategory.java
@@ -16,10 +16,7 @@
*/
package alfio.controller.decorator;
-import alfio.model.Event;
-import alfio.model.PriceContainer;
-import alfio.model.PromoCodeDiscount;
-import alfio.model.TicketCategory;
+import alfio.model.*;
import alfio.util.MonetaryUtil;
import lombok.experimental.Delegate;
import org.apache.commons.lang3.StringUtils;
@@ -40,12 +37,13 @@ public class SaleableTicketCategory implements PriceContainer {
private final boolean inSale;
private final int availableTickets;
private final int maxTickets;
+ private final boolean hasPendingTickets;
private final PromoCodeDiscount promoCodeDiscount;
public SaleableTicketCategory(TicketCategory ticketCategory,
ZonedDateTime now,
Event event,
- int availableTickets,
+ CategoryAvailability categoryAvailability,
int maxTickets,
PromoCodeDiscount promoCodeDiscount) {
this.ticketCategory = ticketCategory;
@@ -53,9 +51,10 @@ public SaleableTicketCategory(TicketCategory ticketCategory,
this.zoneId = event.getZoneId();
this.event = event;
this.inSale = isCurrentlyInSale(now, ticketCategory, event.getZoneId());
- this.soldOut = inSale && availableTickets == 0;
- this.availableTickets = availableTickets;
+ this.soldOut = inSale && categoryAvailability.availableTickets() == 0;
+ this.availableTickets = categoryAvailability.availableTickets();
this.maxTickets = maxTickets;
+ this.hasPendingTickets = categoryAvailability.hasPendingTickets();
this.promoCodeDiscount = promoCodeDiscount;
}
@@ -126,6 +125,10 @@ public int getAvailableTickets() {
return availableTickets;
}
+ public boolean hasPendingTickets() {
+ return hasPendingTickets;
+ }
+
public String getDiscountedPrice() {
return MonetaryUtil.formatUnit(getFinalPriceToDisplay(getFinalPrice(), getVAT(), getVatStatus()), getCurrencyCode());
}
diff --git a/src/main/java/alfio/manager/AdminReservationManager.java b/src/main/java/alfio/manager/AdminReservationManager.java
index 9fa3121cc5..714fea4478 100644
--- a/src/main/java/alfio/manager/AdminReservationManager.java
+++ b/src/main/java/alfio/manager/AdminReservationManager.java
@@ -314,6 +314,8 @@ private Result performUpdate(String reservationId, PurchaseContext purc
var newPrice = ticketReservationManager.totalReservationCostWithVAT(r.withVatStatus(newVatStatus)).getLeft();
ticketReservationRepository.resetVat(reservationId, r.isInvoiceRequested(), newVatStatus, r.getSrcPriceCts(), newPrice.getPriceWithVAT(),
newPrice.getVAT(), Math.abs(newPrice.getDiscount()), r.getCurrencyCode());
+ // propagate status to tickets
+ ticketRepository.updateVatStatusForReservation(reservationId, newVatStatus);
}
}
diff --git a/src/main/java/alfio/manager/TicketReservationManager.java b/src/main/java/alfio/manager/TicketReservationManager.java
index 429c1b9ce5..344582bcae 100644
--- a/src/main/java/alfio/manager/TicketReservationManager.java
+++ b/src/main/java/alfio/manager/TicketReservationManager.java
@@ -1550,12 +1550,11 @@ public String getShortReservationID(Configurable event, String reservationId) {
return configurationManager.getShortReservationID(event, findById(reservationId).orElseThrow());
}
-
- public int countAvailableTickets(EventAndOrganizationId event, TicketCategory category) {
+ public CategoryAvailability countAvailableTickets(EventAndOrganizationId event, TicketCategory category) {
if(category.isBounded()) {
- return ticketRepository.countFreeTickets(event.getId(), category.getId());
+ return ticketRepository.getCategoryAvailability(event.getId(), category.getId());
}
- return ticketRepository.countFreeTicketsForUnbounded(event.getId());
+ return ticketRepository.getUnboundedCategoryAvailability(event.getId(), category.getId());
}
public void releaseTicket(Event event, TicketReservation ticketReservation, final Ticket ticket) {
diff --git a/src/main/java/alfio/model/CategoryAvailability.java b/src/main/java/alfio/model/CategoryAvailability.java
new file mode 100644
index 0000000000..f54cceac66
--- /dev/null
+++ b/src/main/java/alfio/model/CategoryAvailability.java
@@ -0,0 +1,26 @@
+/**
+ * This file is part of alf.io.
+ *
+ * alf.io is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * alf.io is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with alf.io. If not, see .
+ */
+package alfio.model;
+
+import ch.digitalfondue.npjt.ConstructorAnnotationRowMapper.Column;
+
+public record CategoryAvailability(@Column("available_tickets") int availableTickets,
+ @Column("pending_tickets") int pendingTickets) {
+ public boolean hasPendingTickets() {
+ return pendingTickets > 0;
+ }
+}
diff --git a/src/main/java/alfio/repository/TicketRepository.java b/src/main/java/alfio/repository/TicketRepository.java
index b1d7300cf0..68c2ddc606 100644
--- a/src/main/java/alfio/repository/TicketRepository.java
+++ b/src/main/java/alfio/repository/TicketRepository.java
@@ -102,6 +102,22 @@ default void bulkTicketUpdate(List ids, TicketCategory ticketCategory)
@Query("select count(*) from ticket where status = 'FREE' and category_id = :categoryId and event_id = :eventId")
Integer countFreeTickets(@Bind("eventId") int eventId, @Bind("categoryId") int categoryId);
+ @Query("""
+ select count(*) filter (where status = 'FREE') as available_tickets,
+ count(*) filter (where status = 'PENDING' or status = 'RELEASED') as pending_tickets
+ from ticket
+ where category_id = :categoryId and event_id = :eventId
+ """)
+ CategoryAvailability getCategoryAvailability(@Bind("eventId") int eventId, @Bind("categoryId") int categoryId);
+
+ @Query("""
+ select count(*) filter (where category_id is null and status = 'FREE') as available_tickets,
+ count(*) filter (where (category_id = :categoryId and status = 'PENDING') or status = 'RELEASED') as pending_tickets
+ from ticket
+ where event_id = :eventId
+ """)
+ CategoryAvailability getUnboundedCategoryAvailability(@Bind("eventId") int eventId, @Bind("categoryId") int categoryId);
+
@Query("select count(*) from ticket where status = 'FREE' and category_id is null and event_id = :eventId")
Integer countFreeTicketsForUnbounded(@Bind("eventId") int eventId);
diff --git a/src/main/java/alfio/util/ReservationUtil.java b/src/main/java/alfio/util/ReservationUtil.java
index 2159317a80..8106268e34 100644
--- a/src/main/java/alfio/util/ReservationUtil.java
+++ b/src/main/java/alfio/util/ReservationUtil.java
@@ -140,7 +140,8 @@ private static void validateCategory(Errors bindi
Event event, int maxAmountOfTicket, List res,
Optional specialCode, ZonedDateTime now, T r) {
TicketCategory tc = eventManager.getTicketCategoryById(r.getTicketCategoryId(), event.getId());
- SaleableTicketCategory ticketCategory = new SaleableTicketCategory(tc, now, event, tickReservationManager.countAvailableTickets(event, tc), maxAmountOfTicket, null);
+ var categoryAvailability = tickReservationManager.countAvailableTickets(event, tc);
+ SaleableTicketCategory ticketCategory = new SaleableTicketCategory(tc, now, event, categoryAvailability, maxAmountOfTicket, null);
if (!ticketCategory.getSaleable()) {
bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE);
diff --git a/src/main/resources/alfio/i18n/public.properties b/src/main/resources/alfio/i18n/public.properties
index faebf2de27..1226622875 100644
--- a/src/main/resources/alfio/i18n/public.properties
+++ b/src/main/resources/alfio/i18n/public.properties
@@ -14,6 +14,7 @@ common.event.date-format=EEE dd MMM yyyy
common.event.time-format=HH:mm
common.edit=Edit
common.confirm=Confirm
+common.refresh=Refresh
locale=en
event.get-your-ticket-for=Get your tickets for {0}
@@ -52,6 +53,8 @@ show-event.sales-not-started=Sales Start {0}
show-event.sales-end=Sales end {0}
show-event.sales-ended=Sales ended {0}
show-event.sold-out=All tickets have been sold
+show-event.all-tickets-reserved=All tickets reserved
+show-event.all-tickets-reserved.info=All tickets are currently reserved. If some reservations are not confirmed or paid within the allotted time, those tickets may become available again. Please check back periodically for any openings.
show-event.not-available=N/A
show-event.timezone.warn=Dates and times are in the local time zone of the event''s venue ({0})
show-event.promo-code-type.promotional=Promotional
@@ -66,6 +69,7 @@ show-event.add-to-calendar=Add to calendar
show-event.expired-categories=Expired categories:
show-event.mandatoryOneForTicket=Surcharge, 1 per ticket
show-event.mandatoryPercentage=Surcharge
+show-event.category-refresh.complete=Refresh successful!
#reservation-page.ms
reservation-page.header.title=Order summary for {0}
diff --git a/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java b/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java
index 9a530ef444..a2fd8b95fc 100644
--- a/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java
+++ b/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java
@@ -398,14 +398,14 @@ protected void testBasicFlow(Supplier contextSupplier) t
assertNotNull(items);
- assertEquals(1, items.getTicketCategories().size());
- var visibleCat = items.getTicketCategories().get(0);
+ assertEquals(1, items.ticketCategories().size());
+ var visibleCat = items.ticketCategories().get(0);
assertEquals("default", visibleCat.getName());
assertEquals("10.00", visibleCat.getFormattedFinalPrice());
assertFalse(visibleCat.isHasDiscount());
- assertEquals(1, items.getAdditionalServices().size());
- var additionalItem = items.getAdditionalServices().get(0);
+ assertEquals(1, items.additionalServices().size());
+ var additionalItem = items.additionalServices().get(0);
assertEquals("40.00", additionalItem.getFormattedFinalPrice());
assertEquals("1.00", additionalItem.getVatPercentage());
assertEquals(1, additionalItem.getTitle().size()); //TODO: check: if there are missing lang, we should at least copy them (?)
@@ -414,8 +414,8 @@ protected void testBasicFlow(Supplier contextSupplier) t
assertEquals("
additional desc
\n", additionalItem.getDescription().get("en"));
// check presence of reservation list
- assertFalse(items.isWaitingList());
- assertFalse(items.isPreSales());
+ assertFalse(items.waitingList());
+ assertFalse(items.preSales());
//
// fix dates to enable reservation list
@@ -424,8 +424,8 @@ protected void testBasicFlow(Supplier contextSupplier) t
//
items = eventApiV2Controller.getTicketCategories(context.event.getShortName(), null).getBody();
assertNotNull(items);
- assertTrue(items.isWaitingList());
- assertTrue(items.isPreSales());
+ assertTrue(items.waitingList());
+ assertTrue(items.preSales());
//
var subForm = new WaitingQueueSubscriptionForm();
@@ -501,9 +501,9 @@ protected void testBasicFlow(Supplier contextSupplier) t
var itemsRes2 = eventApiV2Controller.getTicketCategories(context.event.getShortName(), HIDDEN_CODE);
var items2 = itemsRes2.getBody();
assertNotNull(items2);
- assertEquals(2, items2.getTicketCategories().size());
+ assertEquals(2, items2.ticketCategories().size());
- var hiddenCat = items2.getTicketCategories().stream().filter(t -> t.isAccessRestricted()).findFirst().get();
+ var hiddenCat = items2.ticketCategories().stream().filter(t -> t.isAccessRestricted()).findFirst().get();
assertEquals(hiddenCategoryId, hiddenCat.getId());
assertEquals("hidden", hiddenCat.getName());
assertEquals("1.00", hiddenCat.getFormattedFinalPrice());
@@ -626,8 +626,8 @@ protected void testBasicFlow(Supplier contextSupplier) t
var items3 = itemsRes3.getBody();
assertNotNull(items3);
- assertEquals(1, items3.getTicketCategories().size());
- var visibleCat = items3.getTicketCategories().get(0);
+ assertEquals(1, items3.ticketCategories().size());
+ var visibleCat = items3.ticketCategories().get(0);
assertEquals("default", visibleCat.getName());
assertEquals("10.00", visibleCat.getFormattedFinalPrice());
assertTrue(visibleCat.isHasDiscount());
@@ -672,7 +672,7 @@ protected void testBasicFlow(Supplier contextSupplier) t
//check blacklist payment methods
{
var form = new ReservationForm();
- var categories = eventApiV2Controller.getTicketCategories(context.event.getShortName(), HIDDEN_CODE).getBody().getTicketCategories();
+ var categories = eventApiV2Controller.getTicketCategories(context.event.getShortName(), HIDDEN_CODE).getBody().ticketCategories();
var c1 = new TicketReservationModification();
c1.setQuantity(1);
@@ -1531,7 +1531,7 @@ protected void checkOrderSummary(ReservationInfo reservation, ReservationFlowCon
private List retrieveCategories(ReservationFlowContext context) {
var response = requireNonNull(eventApiV2Controller.getTicketCategories(context.event.getShortName(), null));
- return requireNonNull(response.getBody()).getTicketCategories();
+ return requireNonNull(response.getBody()).ticketCategories();
}
protected void performAdditionalTests(ReservationFlowContext reservationFlowContext) {}
@@ -1575,7 +1575,7 @@ protected void testAddSubscription(ReservationFlowContext context, int numberOfT
var categoriesResponse = eventApiV2Controller.getTicketCategories(context.event.getShortName(), null);
assertTrue(categoriesResponse.getStatusCode().is2xxSuccessful());
assertNotNull(categoriesResponse.getBody());
- var categoryId = categoriesResponse.getBody().getTicketCategories().stream()
+ var categoryId = categoriesResponse.getBody().ticketCategories().stream()
.filter(t -> t.getName().equals(categoryName))
.findFirst()
.orElseThrow()
diff --git a/src/test/java/alfio/manager/TicketReservationManagerTest.java b/src/test/java/alfio/manager/TicketReservationManagerTest.java
index 77e332dec9..4e9586ff3f 100644
--- a/src/test/java/alfio/manager/TicketReservationManagerTest.java
+++ b/src/test/java/alfio/manager/TicketReservationManagerTest.java
@@ -706,11 +706,11 @@ void countAvailableTickets() {
//count how many tickets yet available for a category
when(ticketCategory.isBounded()).thenReturn(true);
trm.countAvailableTickets(event, ticketCategory);
- verify(ticketRepository).countFreeTickets(eq(EVENT_ID), eq(TICKET_CATEGORY_ID));
+ verify(ticketRepository).getCategoryAvailability(eq(EVENT_ID), eq(TICKET_CATEGORY_ID));
//count how many tickets are available for unbounded categories
when(ticketCategory.isBounded()).thenReturn(false);
trm.countAvailableTickets(event, ticketCategory);
- verify(ticketRepository).countFreeTicketsForUnbounded(eq(EVENT_ID));
+ verify(ticketRepository).getUnboundedCategoryAvailability(eq(EVENT_ID), eq(TICKET_CATEGORY_ID));
}
private void initReleaseTicket() {