diff --git a/frontend/projects/public/src/app/app.module.ts b/frontend/projects/public/src/app/app.module.ts index f4c9a0328e..18039a96ef 100644 --- a/frontend/projects/public/src/app/app.module.ts +++ b/frontend/projects/public/src/app/app.module.ts @@ -21,6 +21,7 @@ import { faAngleUp, faCheck, faCircle, + faCircleNotch, faCog, faCreditCard, faDownload, @@ -251,7 +252,7 @@ export class AppModule { library.addIcons(faInfoCircle, faGift, faTicketAlt, faCheck, faAddressCard, faFileAlt, faThumbsUp, faMoneyBill, faDownload, faSearchPlus, faExchangeAlt, faExclamationTriangle, faCreditCard, faCog, faEraser, faTimes, faFileInvoice, faGlobe, faAngleDown, faAngleUp, faCircle, faCheckCircle, faMoneyCheckAlt, faWifi, faTrash, faCopy, faExclamationCircle, faUserAstronaut, - faSignInAlt, faSignOutAlt, faExternalLinkAlt, faMapMarkerAlt, faRedoAlt); + faSignInAlt, faSignOutAlt, faExternalLinkAlt, faMapMarkerAlt, faRedoAlt, faCircleNotch); library.addIcons(faCalendarAlt, faCalendarPlus, faCompass, faClock, faEnvelope, faEdit, faClone, faHandshake, faBuilding); library.addIcons(faPaypal, faStripe, faIdeal, faApplePay, faFilePdf); } diff --git a/frontend/projects/public/src/app/event-display/event-display.component.html b/frontend/projects/public/src/app/event-display/event-display.component.html index 89e881dccd..761d34bd7d 100644 --- a/frontend/projects/public/src/app/event-display/event-display.component.html +++ b/frontend/projects/public/src/app/event-display/event-display.component.html @@ -117,8 +117,15 @@

+ (valueChange)="selectionChange()" + (refreshCommand)="handleRefreshCommand()"> + @if (!waitingList && !category.saleableAndLimitNotReached && category.containsPendingTickets) { +
+ + {{ 'show-event.all-tickets-reserved.info' | translate }} +
+ } @@ -206,7 +213,14 @@
-
+
+ +
diff --git a/frontend/projects/public/src/app/event-display/event-display.component.ts b/frontend/projects/public/src/app/event-display/event-display.component.ts index 1a30a992be..089f555fdf 100644 --- a/frontend/projects/public/src/app/event-display/event-display.component.ts +++ b/frontend/projects/public/src/app/event-display/event-display.component.ts @@ -1,4 +1,4 @@ -import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; +import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {EventService} from '../shared/event.service'; import {ActivatedRoute, Router} from '@angular/router'; import {UntypedFormArray, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; @@ -8,7 +8,7 @@ import {TranslateService} from '@ngx-translate/core'; import {TicketCategory} from '../model/ticket-category'; import {ReservationRequest} from '../model/reservation-request'; import {handleServerSideValidationError} from '../shared/validation-helper'; -import {zip} from 'rxjs'; +import {debounceTime, Subject, Subscription, zip} from 'rxjs'; import {AdditionalService} from '../model/additional-service'; import {I18nService} from '../shared/i18n.service'; import {WaitingListSubscriptionRequest} from '../model/waiting-list-subscription-request'; @@ -17,13 +17,14 @@ import {DynamicDiscount, EventCode} from '../model/event-code'; import {AnalyticsService} from '../shared/analytics.service'; import {ErrorDescriptor} from '../model/validated-response'; import {SearchParams} from '../model/search-params'; +import {FeedbackService} from "../shared/feedback/feedback.service"; @Component({ selector: 'app-event-display', templateUrl: './event-display.component.html', styleUrls: ['./event-display.component.scss'] }) -export class EventDisplayComponent implements OnInit { +export class EventDisplayComponent implements OnInit, OnDestroy { event: AlfioEvent; ticketCategories: TicketCategory[]; @@ -59,6 +60,9 @@ export class EventDisplayComponent implements OnInit { expiredCategoriesExpanded = false; private dynamicDiscount: DynamicDiscount; + private refreshDebouncer = new Subject(); + private subscription?: Subscription; + submitInProgress: boolean = false; // https://alligator.io/angular/reactive-forms-formarray-dynamic-fields/ @@ -70,10 +74,14 @@ export class EventDisplayComponent implements OnInit { private formBuilder: UntypedFormBuilder, public translate: TranslateService, private i18nService: I18nService, - private analytics: AnalyticsService) { } + private analytics: AnalyticsService, + private feedbackService: FeedbackService) { } ngOnInit(): void { + this.subscription = this.refreshDebouncer + .pipe(debounceTime(500)) + .subscribe(() => this.doRefreshCategories()); const code = this.route.snapshot.queryParams['code']; const errors = this.route.snapshot.queryParams['errors']; if (errors) { @@ -109,6 +117,10 @@ export class EventDisplayComponent implements OnInit { }); } + ngOnDestroy() { + this.subscription?.unsubscribe(); + } + private applyItemsByCat(itemsByCat: ItemsByCategory) { this.ticketCategories = itemsByCat.ticketCategories; this.expiredCategories = itemsByCat.expiredCategories || []; @@ -150,6 +162,11 @@ export class EventDisplayComponent implements OnInit { } submitForm(eventShortName: string, reservation: ReservationRequest) { + if (this.submitInProgress) { + // ignoring click, as there is a pending request + return; + } + this.submitInProgress = true; const request = reservation; if (reservation.additionalService != null && reservation.additionalService.length > 0) { request.additionalService = reservation.additionalService.filter(as => (as.amount != null && as.amount > 0) || (as.quantity != null && as.quantity > 0)); @@ -161,8 +178,10 @@ export class EventDisplayComponent implements OnInit { queryParams: SearchParams.transformParams(this.route.snapshot.queryParams, this.route.snapshot.params) }); } + this.submitInProgress = false; }, error: err => { + this.submitInProgress = false; this.globalErrors = handleServerSideValidationError(err, this.reservationForm); this.scrollToTickets(); } @@ -298,7 +317,18 @@ export class EventDisplayComponent implements OnInit { } get displayMap(): boolean { - return (this.event.mapUrl && this.event.mapUrl.length > 0) && !this.isEventOnline; + return (this.event.mapUrl?.length > 0) && !this.isEventOnline; } + handleRefreshCommand() { + this.refreshDebouncer.next(null); + } + + private doRefreshCategories() { + this.eventService.getEventTicketsInfo(this.event.shortName) + .subscribe(itemsByCat => { + this.applyItemsByCat(itemsByCat); + this.feedbackService.showSuccess('show-event.category-refresh.complete'); + }) + } } diff --git a/frontend/projects/public/src/app/item-card/item-card.html b/frontend/projects/public/src/app/item-card/item-card.html index 2d708b642e..0b17251f7a 100644 --- a/frontend/projects/public/src/app/item-card/item-card.html +++ b/frontend/projects/public/src/app/item-card/item-card.html @@ -19,5 +19,8 @@

+
+ +
diff --git a/frontend/projects/public/src/app/model/ticket-category.ts b/frontend/projects/public/src/app/model/ticket-category.ts index 93fe47fcae..10d48dea47 100644 --- a/frontend/projects/public/src/app/model/ticket-category.ts +++ b/frontend/projects/public/src/app/model/ticket-category.ts @@ -21,6 +21,7 @@ export interface TicketCategory { // saleableAndLimitNotReached: boolean; + containsPendingTickets: boolean; accessRestricted: boolean; soldOutOrLimitReached: boolean; diff --git a/frontend/projects/public/src/app/reservation/payment-method-selector/payment-method-selector.component.html b/frontend/projects/public/src/app/reservation/payment-method-selector/payment-method-selector.component.html index 3dbd79721e..cbc15cc2fe 100644 --- a/frontend/projects/public/src/app/reservation/payment-method-selector/payment-method-selector.component.html +++ b/frontend/projects/public/src/app/reservation/payment-method-selector/payment-method-selector.component.html @@ -2,7 +2,7 @@
+ [ngClass]="{ 'fw-bold border-top border-bottom': selectedPaymentMethod == method, 'text-body-secondary border-right-md': selectedPaymentMethod != null && selectedPaymentMethod != method }">
@@ -11,7 +11,7 @@
-
diff --git a/frontend/projects/public/src/app/subscription-display/subscription-display.component.html b/frontend/projects/public/src/app/subscription-display/subscription-display.component.html index 4508331ba1..66360b5b57 100644 --- a/frontend/projects/public/src/app/subscription-display/subscription-display.component.html +++ b/frontend/projects/public/src/app/subscription-display/subscription-display.component.html @@ -21,7 +21,16 @@


-
+
+ @if (subscription.numAvailable > 0) { + + } +
diff --git a/frontend/projects/public/src/app/subscription-display/subscription-display.component.ts b/frontend/projects/public/src/app/subscription-display/subscription-display.component.ts index fe978ad1cf..d640baf89d 100644 --- a/frontend/projects/public/src/app/subscription-display/subscription-display.component.ts +++ b/frontend/projects/public/src/app/subscription-display/subscription-display.component.ts @@ -22,6 +22,7 @@ export class SubscriptionDisplayComponent implements OnInit { submitError: ErrorDescriptor; subscription: SubscriptionInfo; subscriptionId: string; + submitInProgress: boolean = false; constructor(private route: ActivatedRoute, private router: Router, @@ -45,18 +46,28 @@ export class SubscriptionDisplayComponent implements OnInit { submitForm() { - this.subscriptionService.reserve(this.subscriptionId).subscribe(res => { - this.router.navigate(['subscription', this.subscriptionId, 'reservation', res.value, 'book']); - }, (err) => { - const errorObject = getErrorObject(err); - let errorCode: string; - if (errorObject != null) { - errorCode = errorObject.validationErrors[0].code; - } else { - errorCode = 'reservation-page-error-status.header.title'; + if (this.submitInProgress) { + // ignoring click, as there is a pending request + return; } - this.feedbackService.showError(errorCode); - }); + this.submitInProgress = true; + this.subscriptionService.reserve(this.subscriptionId).subscribe({ + next: res => { + this.submitInProgress = false; + this.router.navigate(['subscription', this.subscriptionId, 'reservation', res.value, 'book']); + }, + error: err => { + this.submitInProgress = false; + const errorObject = getErrorObject(err); + let errorCode: string; + if (errorObject != null) { + errorCode = errorObject.validationErrors[0].code; + } else { + errorCode = 'reservation-page-error-status.header.title'; + } + this.feedbackService.showError(errorCode); + } + }); } } diff --git a/frontend/projects/public/src/app/subscription-summary/subscription-summary.component.html b/frontend/projects/public/src/app/subscription-summary/subscription-summary.component.html index 2243c14363..2d416ce9a7 100644 --- a/frontend/projects/public/src/app/subscription-summary/subscription-summary.component.html +++ b/frontend/projects/public/src/app/subscription-summary/subscription-summary.component.html @@ -40,7 +40,7 @@
-
+
diff --git a/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.component.ts b/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.component.ts index b7d1bd977e..74ebeb498c 100644 --- a/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.component.ts +++ b/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.component.ts @@ -20,9 +20,16 @@ export class TicketQuantitySelectorComponent { @Output() valueChange = new EventEmitter(); + @Output() + refreshCommand = new EventEmitter(); + formGroup: UntypedFormGroup; selectionChanged(): void { this.valueChange.next(this.parentGroup.get('amount').value); } + + refreshCategories(): void { + this.refreshCommand.next(new Date().getTime()); + } } diff --git a/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.html b/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.html index f1f5536ac7..c39b77e358 100644 --- a/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.html +++ b/frontend/projects/public/src/app/ticket-quantity-selector/ticket-quantity-selector.html @@ -1,10 +1,26 @@
- + @if (category.saleableAndLimitNotReached) { - - - + } @else { +
+ @if (category.containsPendingTickets) { + + } @else if (category.soldOutOrLimitReached) { + + } @else { + + } + + @if (category.containsPendingTickets || !category.soldOutOrLimitReached) { + + } +
+ }
diff --git a/frontend/projects/public/src/app/update-ticket/update-ticket.component.html b/frontend/projects/public/src/app/update-ticket/update-ticket.component.html index ba7b462d7a..61676d61c8 100644 --- a/frontend/projects/public/src/app/update-ticket/update-ticket.component.html +++ b/frontend/projects/public/src/app/update-ticket/update-ticket.component.html @@ -6,7 +6,7 @@
-
+
{{'event.online.not-started' | translate:{ '0' : ticketOnlineCheckInDate } }} {{'event.online.started' | translate }}
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() {