/**
* 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 <http://www.gnu.org/licenses/>.
*/
package alfio.controller.form;
import alfio.controller.decorator.SaleableTicketCategory;
import alfio.manager.EventManager;
import alfio.manager.TicketReservationManager;
import alfio.model.AdditionalService;
import alfio.model.Event;
import alfio.model.SpecialPrice;
import alfio.model.TicketCategory;
import alfio.model.modification.ASReservationWithOptionalCodeModification;
import alfio.model.modification.AdditionalServiceReservationModification;
import alfio.model.modification.TicketReservationModification;
import alfio.model.modification.TicketReservationWithOptionalCodeModification;
import alfio.repository.AdditionalServiceRepository;
import alfio.repository.TicketCategoryDescriptionRepository;
import alfio.util.ErrorsCode;
import alfio.util.OptionalWrapper;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.validation.Errors;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
//step 1 : choose tickets
@Data
public class ReservationForm {
private String promoCode;
private List<TicketReservationModification> reservation;
private List<AdditionalServiceReservationModification> additionalService;
private List<TicketReservationModification> selected() {
return ofNullable(reservation)
.orElse(emptyList())
.stream()
.filter((e) -> e != null && e.getAmount() != null && e.getTicketCategoryId() != null
&& e.getAmount() > 0).collect(toList());
}
private List<AdditionalServiceReservationModification> selectedAdditionalServices() {
return ofNullable(additionalService)
.orElse(emptyList())
.stream()
.filter(e -> e != null && e.getQuantity() != null && e.getAdditionalServiceId() != null && e.getQuantity() > 0)
.collect(toList());
}
private int ticketSelectionCount() {
return selected().stream().mapToInt(TicketReservationModification::getAmount).sum();
}
private int additionalServicesSelectionCount(AdditionalServiceRepository additionalServiceRepository, int eventId) {
return (int) selectedAdditionalServices().stream()
.filter(as -> as.getAdditionalServiceId() != null && (additionalServiceRepository.getById(as.getAdditionalServiceId(), eventId).isFixPrice() || Optional.ofNullable(as.getAmount()).filter(a -> a.compareTo(BigDecimal.ZERO) > 0).isPresent()))
.count();
}
public Optional<Pair<List<TicketReservationWithOptionalCodeModification>, List<ASReservationWithOptionalCodeModification>>> validate(Errors bindingResult,
TicketReservationManager tickReservationManager,
TicketCategoryDescriptionRepository ticketCategoryDescriptionRepository,
AdditionalServiceRepository additionalServiceRepository,
EventManager eventManager,
Event event,
Locale locale) {
int selectionCount = ticketSelectionCount();
int additionalServicesCount = additionalServicesSelectionCount(additionalServiceRepository, event.getId());
if (selectionCount + additionalServicesCount <= 0) {
bindingResult.reject(ErrorsCode.STEP_1_SELECT_AT_LEAST_ONE);
return Optional.empty();
}
List<Pair<TicketReservationModification, Integer>> maxTicketsByTicketReservation = selected().stream()
.map(r -> Pair.of(r, tickReservationManager.maxAmountOfTicketsForCategory(event.getOrganizationId(), event.getId(), r.getTicketCategoryId())))
.collect(toList());
Optional<Pair<TicketReservationModification, Integer>> error = maxTicketsByTicketReservation.stream()
.filter(p -> p.getKey().getAmount() > p.getValue())
.findAny();
if(error.isPresent()) {
bindingResult.reject(ErrorsCode.STEP_1_OVER_MAXIMUM, new Object[] { error.get().getValue() }, null);
return Optional.empty();
}
final List<TicketReservationModification> categories = selected();
final List<AdditionalServiceReservationModification> additionalServices = selectedAdditionalServices();
final boolean validCategorySelection = categories.stream().allMatch(c -> {
TicketCategory tc = eventManager.getTicketCategoryById(c.getTicketCategoryId(), event.getId());
return OptionalWrapper.optionally(() -> eventManager.findEventByTicketCategory(tc)).isPresent();
});
final boolean validAdditionalServiceSelected = additionalServices.stream().allMatch(asm -> {
AdditionalService as = eventManager.getAdditionalServiceById(asm.getAdditionalServiceId(), event.getId());
ZonedDateTime now = ZonedDateTime.now(event.getZoneId());
return as.getInception(event.getZoneId()).isBefore(now) && as.getExpiration(event.getZoneId()).isAfter(now) &&
OptionalWrapper.optionally(() -> eventManager.findEventByAdditionalService(as)).isPresent();
});
if(!validCategorySelection || !validAdditionalServiceSelected) {
bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE);
return Optional.empty();
}
List<TicketReservationWithOptionalCodeModification> res = new ArrayList<>();
//
Optional<SpecialPrice> specialCode = Optional.ofNullable(StringUtils.trimToNull(promoCode)).flatMap(
(trimmedCode) -> OptionalWrapper.optionally(() -> tickReservationManager.getSpecialPriceByCode(trimmedCode)));
//
final ZonedDateTime now = ZonedDateTime.now(event.getZoneId());
maxTicketsByTicketReservation.forEach((pair) -> validateCategory(bindingResult, tickReservationManager, ticketCategoryDescriptionRepository, eventManager, event, pair.getRight(), res, specialCode, now, pair.getLeft(), locale));
return bindingResult.hasErrors() ? Optional.empty() : Optional.of(Pair.of(res, additionalServices.stream().map(as -> new ASReservationWithOptionalCodeModification(as, specialCode)).collect(Collectors.toList())));
}
private static void validateCategory(Errors bindingResult, TicketReservationManager tickReservationManager, TicketCategoryDescriptionRepository ticketCategoryDescriptionRepository, EventManager eventManager,
Event event, int maxAmountOfTicket, List<TicketReservationWithOptionalCodeModification> res,
Optional<SpecialPrice> specialCode, ZonedDateTime now, TicketReservationModification r,
Locale locale) {
TicketCategory tc = eventManager.getTicketCategoryById(r.getTicketCategoryId(), event.getId());
SaleableTicketCategory ticketCategory = new SaleableTicketCategory(tc, "", now, event, tickReservationManager.countAvailableTickets(event, tc), maxAmountOfTicket, null);
if (!ticketCategory.getSaleable()) {
bindingResult.reject(ErrorsCode.STEP_1_TICKET_CATEGORY_MUST_BE_SALEABLE);
return;
}
res.add(new TicketReservationWithOptionalCodeModification(r, ticketCategory.isAccessRestricted() ? specialCode : Optional.empty()));
}
}