/**
* 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;
import alfio.controller.decorator.EventDescriptor;
import alfio.controller.decorator.SaleableAdditionalService;
import alfio.controller.decorator.SaleableTicketCategory;
import alfio.controller.form.ReservationForm;
import alfio.controller.support.SessionUtil;
import alfio.manager.EventManager;
import alfio.manager.EventStatisticsManager;
import alfio.manager.TicketReservationManager;
import alfio.manager.i18n.I18nManager;
import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
import alfio.model.modification.support.LocationDescriptor;
import alfio.model.result.ValidationResult;
import alfio.model.system.Configuration;
import alfio.model.system.ConfigurationKeys;
import alfio.model.transaction.PaymentProxy;
import alfio.repository.*;
import alfio.repository.user.OrganizationRepository;
import alfio.util.ErrorsCode;
import alfio.util.EventUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static alfio.controller.support.SessionUtil.addToFlash;
import static alfio.util.OptionalWrapper.optionally;
@Controller
public class EventController {
private static final String REDIRECT = "redirect:";
private final EventRepository eventRepository;
private final EventDescriptionRepository eventDescriptionRepository;
private final I18nManager i18nManager;
private final TicketCategoryRepository ticketCategoryRepository;
private final TicketCategoryDescriptionRepository ticketCategoryDescriptionRepository;
private final ConfigurationManager configurationManager;
private final OrganizationRepository organizationRepository;
private final SpecialPriceRepository specialPriceRepository;
private final PromoCodeDiscountRepository promoCodeRepository;
private final EventManager eventManager;
private final TicketReservationManager ticketReservationManager;
private final EventStatisticsManager eventStatisticsManager;
private final AdditionalServiceRepository additionalServiceRepository;
private final AdditionalServiceTextRepository additionalServiceTextRepository;
@Autowired
public EventController(ConfigurationManager configurationManager,
EventRepository eventRepository,
EventDescriptionRepository eventDescriptionRepository,
I18nManager i18nManager,
OrganizationRepository organizationRepository,
TicketCategoryRepository ticketCategoryRepository,
TicketCategoryDescriptionRepository ticketCategoryDescriptionRepository,
SpecialPriceRepository specialPriceRepository,
PromoCodeDiscountRepository promoCodeRepository,
EventManager eventManager,
TicketReservationManager ticketReservationManager,
EventStatisticsManager eventStatisticsManager,
AdditionalServiceRepository additionalServiceRepository,
AdditionalServiceTextRepository additionalServiceTextRepository) {
this.configurationManager = configurationManager;
this.eventRepository = eventRepository;
this.eventDescriptionRepository = eventDescriptionRepository;
this.i18nManager = i18nManager;
this.organizationRepository = organizationRepository;
this.ticketCategoryRepository = ticketCategoryRepository;
this.ticketCategoryDescriptionRepository = ticketCategoryDescriptionRepository;
this.specialPriceRepository = specialPriceRepository;
this.promoCodeRepository = promoCodeRepository;
this.eventManager = eventManager;
this.ticketReservationManager = ticketReservationManager;
this.eventStatisticsManager = eventStatisticsManager;
this.additionalServiceRepository = additionalServiceRepository;
this.additionalServiceTextRepository = additionalServiceTextRepository;
}
@RequestMapping(value = "/", method = RequestMethod.HEAD)
public ResponseEntity<String> replyToProxy() {
return ResponseEntity.ok("Up and running!");
}
@RequestMapping(value = {"/"}, method = RequestMethod.GET)
public String listEvents(Model model, Locale locale) {
List<Event> events = eventManager.getPublishedEvents();
if(events.size() == 1) {
return REDIRECT + "/event/" + events.get(0).getShortName() + "/";
} else {
model.addAttribute("events", events.stream().map(e -> {
String eventDescription = eventDescriptionRepository.findDescriptionByEventIdTypeAndLocale(e.getId(), EventDescription.EventDescriptionType.DESCRIPTION, locale.getLanguage()).orElse("");
return new EventDescriptor(e, eventDescription);
}).collect(Collectors.toList()));
model.addAttribute("pageTitle", "event-list.header.title");
model.addAttribute("event", null);
model.addAttribute("showAvailableLanguagesInPageTop", true);
model.addAttribute("availableLanguages", i18nManager.getSupportedLanguages());
return "/event/event-list";
}
}
@RequestMapping("/session-expired")
public String sessionExpired(Model model) {
model.addAttribute("pageTitle", "session-expired.header.title");
model.addAttribute("event", null);
return "/event/session-expired";
}
@RequestMapping(value = "/event/{eventName}/promoCode/{promoCode}", method = RequestMethod.POST)
@ResponseBody
public ValidationResult savePromoCode(@PathVariable("eventName") String eventName,
@PathVariable("promoCode") String promoCode,
Model model,
HttpServletRequest request) {
SessionUtil.removeSpecialPriceData(request);
Optional<Event> optional = eventRepository.findOptionalByShortName(eventName);
if(!optional.isPresent()) {
return ValidationResult.failed(new ValidationResult.ErrorDescriptor("event", ""));
}
Event event = optional.get();
ZonedDateTime now = ZonedDateTime.now(event.getZoneId());
Optional<String> maybeSpecialCode = Optional.ofNullable(StringUtils.trimToNull(promoCode));
Optional<SpecialPrice> specialCode = maybeSpecialCode.flatMap((trimmedCode) -> optionally(() -> specialPriceRepository.getByCode(trimmedCode)));
Optional<PromoCodeDiscount> promotionCodeDiscount = maybeSpecialCode.flatMap((trimmedCode) -> optionally(() -> promoCodeRepository.findPromoCodeInEventOrOrganization(event.getId(), trimmedCode)));
if(specialCode.isPresent()) {
if (!optionally(() -> eventManager.getTicketCategoryById(specialCode.get().getTicketCategoryId(), event.getId())).isPresent()) {
return ValidationResult.failed(new ValidationResult.ErrorDescriptor("promoCode", ""));
}
if (specialCode.get().getStatus() != SpecialPrice.Status.FREE) {
return ValidationResult.failed(new ValidationResult.ErrorDescriptor("promoCode", ""));
}
} else if (promotionCodeDiscount.isPresent() && !promotionCodeDiscount.get().isCurrentlyValid(event.getZoneId(), now)) {
return ValidationResult.failed(new ValidationResult.ErrorDescriptor("promoCode", ""));
} else if(!specialCode.isPresent() && !promotionCodeDiscount.isPresent()) {
return ValidationResult.failed(new ValidationResult.ErrorDescriptor("promoCode", ""));
}
if(maybeSpecialCode.isPresent() && !model.asMap().containsKey("hasErrors")) {
if(specialCode.isPresent()) {
SessionUtil.saveSpecialPriceCode(maybeSpecialCode.get(), request);
} else if (promotionCodeDiscount.isPresent()) {
SessionUtil.savePromotionCodeDiscount(maybeSpecialCode.get(), request);
}
return ValidationResult.success();
}
return ValidationResult.failed(new ValidationResult.ErrorDescriptor("promoCode", ""));
}
@RequestMapping(value = "/event/{eventName}", method = {RequestMethod.GET, RequestMethod.HEAD})
public String showEvent(@PathVariable("eventName") String eventName,
Model model, HttpServletRequest request, Locale locale) {
return eventRepository.findOptionalByShortName(eventName).map(event -> {
Optional<String> maybeSpecialCode = SessionUtil.retrieveSpecialPriceCode(request);
Optional<SpecialPrice> specialCode = maybeSpecialCode.flatMap((trimmedCode) -> optionally(() -> specialPriceRepository.getByCode(trimmedCode)));
Optional<PromoCodeDiscount> promoCodeDiscount = SessionUtil.retrievePromotionCodeDiscount(request)
.flatMap((code) -> optionally(() -> promoCodeRepository.findPromoCodeInEventOrOrganization(event.getId(), code)));
final ZonedDateTime now = ZonedDateTime.now(event.getZoneId());
//hide access restricted ticket categories
List<SaleableTicketCategory> ticketCategories = ticketCategoryRepository.findAllTicketCategories(event.getId()).stream()
.filter((c) -> !c.isAccessRestricted() || (specialCode.filter(sc -> sc.getTicketCategoryId() == c.getId()).isPresent()))
.map((m) -> new SaleableTicketCategory(m, ticketCategoryDescriptionRepository.findByTicketCategoryIdAndLocale(m.getId(), locale.getLanguage()).orElse(""),
now, event, ticketReservationManager.countAvailableTickets(event, m), configurationManager.getIntConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), m.getId(), ConfigurationKeys.MAX_AMOUNT_OF_TICKETS_BY_RESERVATION), 5),
promoCodeDiscount.filter(promoCode -> shouldApplyDiscount(promoCode, m)).orElse(null)))
.collect(Collectors.toList());
//
LocationDescriptor ld = LocationDescriptor.fromGeoData(event.getLatLong(), TimeZone.getTimeZone(event.getTimeZone()),
configurationManager.getStringConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.MAPS_CLIENT_API_KEY)));
final boolean hasAccessPromotions = ticketCategoryRepository.countAccessRestrictedRepositoryByEventId(event.getId()) > 0 ||
promoCodeRepository.countByEventId(event.getId()) > 0;
String eventDescription = eventDescriptionRepository.findDescriptionByEventIdTypeAndLocale(event.getId(), EventDescription.EventDescriptionType.DESCRIPTION, locale.getLanguage()).orElse("");
final EventDescriptor eventDescriptor = new EventDescriptor(event, eventDescription);
List<SaleableTicketCategory> expiredCategories = ticketCategories.stream().filter(SaleableTicketCategory::getExpired).collect(Collectors.toList());
List<SaleableTicketCategory> validCategories = ticketCategories.stream().filter(tc -> !tc.getExpired()).collect(Collectors.toList());
List<SaleableAdditionalService> additionalServices = additionalServiceRepository.loadAllForEvent(event.getId()).stream().map((as) -> getSaleableAdditionalService(event, locale, as, promoCodeDiscount.orElse(null))).collect(Collectors.toList());
Predicate<SaleableTicketCategory> waitingQueueTargetCategory = tc -> !tc.getExpired() && !tc.isBounded();
boolean validPaymentConfigured = isEventHasValidPaymentConfigurations(event, configurationManager);
model.addAttribute("event", eventDescriptor)//
.addAttribute("organization", organizationRepository.getById(event.getOrganizationId()))
.addAttribute("ticketCategories", validCategories)//
.addAttribute("expiredCategories", expiredCategories)//
.addAttribute("containsExpiredCategories", !expiredCategories.isEmpty())//
.addAttribute("showNoCategoriesWarning", validCategories.isEmpty())
.addAttribute("hasAccessPromotions", hasAccessPromotions)
.addAttribute("promoCode", specialCode.map(SpecialPrice::getCode).orElse(null))
.addAttribute("locationDescriptor", ld)
.addAttribute("pageTitle", "show-event.header.title")
.addAttribute("hasPromoCodeDiscount", promoCodeDiscount.isPresent())
.addAttribute("promoCodeDiscount", promoCodeDiscount.orElse(null))
.addAttribute("displayWaitingQueueForm", EventUtil.displayWaitingQueueForm(event, ticketCategories, configurationManager, eventStatisticsManager.noSeatsAvailable()))
.addAttribute("displayCategorySelectionForWaitingQueue", ticketCategories.stream().filter(waitingQueueTargetCategory).count() > 1)
.addAttribute("unboundedCategories", ticketCategories.stream().filter(waitingQueueTargetCategory).collect(Collectors.toList()))
.addAttribute("preSales", EventUtil.isPreSales(event, ticketCategories))
.addAttribute("userLanguage", locale.getLanguage())
.addAttribute("showAdditionalServices", !additionalServices.isEmpty())
.addAttribute("enabledAdditionalServices", additionalServices.stream().filter(SaleableAdditionalService::isNotExpired).collect(Collectors.toList()))
.addAttribute("disabledAdditionalServices", additionalServices.stream().filter(SaleableAdditionalService::isExpired).collect(Collectors.toList()))
.addAttribute("forwardButtonDisabled", (ticketCategories.stream().noneMatch(SaleableTicketCategory::getSaleable)) || !validPaymentConfigured)
.addAttribute("useFirstAndLastName", event.mustUseFirstAndLastName())
.addAttribute("validPaymentMethodAvailable", validPaymentConfigured);
model.asMap().putIfAbsent("hasErrors", false);//
return "/event/show-event";
}).orElse(REDIRECT + "/");
}
@RequestMapping(value = "/event/{eventName}/calendar/locale/{locale}", method = {RequestMethod.GET, RequestMethod.HEAD})
public void calendar(@PathVariable("eventName") String eventName, @PathVariable("locale") String locale, @RequestParam(value = "type", required = false) String calendarType, HttpServletResponse response) throws IOException {
Optional<Event> event = eventRepository.findOptionalByShortName(eventName);
if (!event.isPresent()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//meh
Event ev = event.get();
String description = eventDescriptionRepository.findDescriptionByEventIdTypeAndLocale(ev.getId(), EventDescription.EventDescriptionType.DESCRIPTION, locale).orElse("");
if("google".equals(calendarType)) {
response.sendRedirect(ev.getGoogleCalendarUrl(description));
} else {
Optional<byte[]> ical = ev.getIcal(description);
//meh, checked exceptions don't work well with Function & co :(
if(ical.isPresent()) {
response.setContentType("text/calendar");
response.setHeader("Content-Disposition", "inline; filename=\"calendar.ics\"");
response.getOutputStream().write(ical.get());
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
}
@RequestMapping(value = "/event/{eventName}/reserve-tickets", method = { RequestMethod.POST, RequestMethod.GET, RequestMethod.HEAD })
public String reserveTicket(@PathVariable("eventName") String eventName,
@ModelAttribute ReservationForm reservation, BindingResult bindingResult, Model model,
ServletWebRequest request, RedirectAttributes redirectAttributes, Locale locale) {
return eventRepository.findOptionalByShortName(eventName).map(event -> {
final String redirectToEvent = "redirect:/event/" + eventName + "/";
if (request.getHttpMethod() == HttpMethod.GET) {
return redirectToEvent;
}
return reservation.validate(bindingResult, ticketReservationManager, ticketCategoryDescriptionRepository, additionalServiceRepository, eventManager, event, locale)
.map(selected -> {
Date expiration = DateUtils.addMinutes(new Date(), ticketReservationManager.getReservationTimeout(event));
try {
String reservationId = ticketReservationManager.createTicketReservation(event,
selected.getLeft(), selected.getRight(), expiration,
SessionUtil.retrieveSpecialPriceSessionId(request.getRequest()),
SessionUtil.retrievePromotionCodeDiscount(request.getRequest()),
locale, false);
return "redirect:/event/" + eventName + "/reservation/" + reservationId + "/book";
} catch (TicketReservationManager.NotEnoughTicketsException nete) {
bindingResult.reject(ErrorsCode.STEP_1_NOT_ENOUGH_TICKETS);
addToFlash(bindingResult, redirectAttributes);
return redirectToEvent;
} catch (TicketReservationManager.MissingSpecialPriceTokenException missing) {
bindingResult.reject(ErrorsCode.STEP_1_ACCESS_RESTRICTED);
addToFlash(bindingResult, redirectAttributes);
return redirectToEvent;
} catch (TicketReservationManager.InvalidSpecialPriceTokenException invalid) {
bindingResult.reject(ErrorsCode.STEP_1_CODE_NOT_FOUND);
addToFlash(bindingResult, redirectAttributes);
SessionUtil.removeSpecialPriceData(request.getRequest());
return redirectToEvent;
}
}).orElseGet(() -> {
addToFlash(bindingResult, redirectAttributes);
return redirectToEvent;
});
}).orElse("redirect:/");
}
private SaleableAdditionalService getSaleableAdditionalService(Event event, Locale locale, AdditionalService as, PromoCodeDiscount promoCodeDiscount) {
return new SaleableAdditionalService(event, as, additionalServiceTextRepository.findBestMatchByLocaleAndType(as.getId(), locale.getLanguage(), AdditionalServiceText.TextType.TITLE).getValue(),
additionalServiceTextRepository.findBestMatchByLocaleAndType(as.getId(), locale.getLanguage(), AdditionalServiceText.TextType.DESCRIPTION).getValue(), promoCodeDiscount);
}
private static boolean shouldApplyDiscount(PromoCodeDiscount promoCodeDiscount, TicketCategory ticketCategory) {
return promoCodeDiscount.getCategories().isEmpty() || promoCodeDiscount.getCategories().contains(ticketCategory.getId());
}
private boolean isEventHasValidPaymentConfigurations(Event event, ConfigurationManager configurationManager) {
if (event.isFreeOfCharge()) {
return true;
} else if (event.getAllowedPaymentProxies().size() == 0) {
return false;
} else {
//Check whether event already started and it has only PaymentProxy.OFFLINE as payment method
return !(event.getAllowedPaymentProxies().size() == 1 && event.getAllowedPaymentProxies().contains(PaymentProxy.OFFLINE) && !TicketReservationManager.hasValidOfflinePaymentWaitingPeriod(event, configurationManager));
}
}
}