alfio.controller.ReservationController.java Source code

Java tutorial

Introduction

Here is the source code for alfio.controller.ReservationController.java

Source

/**
 * 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.api.support.TicketHelper;
import alfio.controller.form.PaymentForm;
import alfio.controller.form.UpdateTicketOwnerForm;
import alfio.controller.support.SessionUtil;
import alfio.controller.support.TicketDecorator;
import alfio.manager.*;
import alfio.manager.support.PaymentResult;
import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
import alfio.model.TicketReservation.TicketReservationStatus;
import alfio.model.result.ValidationResult;
import alfio.model.system.Configuration;
import alfio.model.system.ConfigurationKeys;
import alfio.model.transaction.PaymentProxy;
import alfio.model.user.Organization;
import alfio.repository.EventRepository;
import alfio.repository.TicketFieldRepository;
import alfio.repository.TicketRepository;
import alfio.repository.user.OrganizationRepository;
import alfio.util.ErrorsCode;
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static alfio.model.system.Configuration.getSystemConfiguration;
import static alfio.model.system.ConfigurationKeys.*;
import static java.util.stream.Collectors.toList;

@Controller
@Log4j2
@AllArgsConstructor
public class ReservationController {

    private final EventRepository eventRepository;
    private final EventManager eventManager;
    private final TicketReservationManager ticketReservationManager;
    private final OrganizationRepository organizationRepository;

    private final TemplateManager templateManager;
    private final MessageSource messageSource;
    private final ConfigurationManager configurationManager;
    private final NotificationManager notificationManager;
    private final TicketHelper ticketHelper;
    private final TicketFieldRepository ticketFieldRepository;
    private final PaymentManager paymentManager;
    private final TicketRepository ticketRepository;
    private final EuVatChecker vatChecker;
    private final MollieManager mollieManager;
    private final RecaptchaService recaptchaService;

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/book", method = RequestMethod.GET)
    public String showPaymentPage(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId,
            //paypal related parameters
            @RequestParam(value = "paymentId", required = false) String paypalPaymentId,
            @RequestParam(value = "PayerID", required = false) String paypalPayerID,
            @RequestParam(value = "paypal-success", required = false) Boolean isPaypalSuccess,
            @RequestParam(value = "paypal-error", required = false) Boolean isPaypalError,
            @RequestParam(value = "fullName", required = false) String fullName,
            @RequestParam(value = "firstName", required = false) String firstName,
            @RequestParam(value = "lastName", required = false) String lastName,
            @RequestParam(value = "email", required = false) String email,
            @RequestParam(value = "billingAddress", required = false) String billingAddress,
            @RequestParam(value = "hmac", required = false) String hmac,
            @RequestParam(value = "postponeAssignment", required = false) Boolean postponeAssignment, Model model,
            Locale locale) {

        return eventRepository.findOptionalByShortName(eventName)
                .map(event -> ticketReservationManager.findById(reservationId).map(reservation -> {

                    if (reservation.getStatus() != TicketReservationStatus.PENDING) {
                        return redirectReservation(Optional.of(reservation), eventName, reservationId);
                    }

                    List<Ticket> ticketsInReservation = ticketReservationManager
                            .findTicketsInReservation(reservationId);
                    if (Boolean.TRUE.equals(isPaypalSuccess) && paypalPayerID != null && paypalPaymentId != null) {
                        model.addAttribute("paypalPaymentId", paypalPaymentId)
                                .addAttribute("paypalPayerID", paypalPayerID)
                                .addAttribute("paypalCheckoutConfirmation", true).addAttribute("fullName", fullName)
                                .addAttribute("firstName", firstName).addAttribute("lastName", lastName)
                                .addAttribute("email", email).addAttribute("billingAddress", billingAddress)
                                .addAttribute("hmac", hmac)
                                .addAttribute("postponeAssignment", Boolean.TRUE.equals(postponeAssignment))
                                .addAttribute("showPostpone", Boolean.TRUE.equals(postponeAssignment));
                    } else {
                        model.addAttribute("paypalCheckoutConfirmation", false)
                                .addAttribute("postponeAssignment", false)
                                .addAttribute("showPostpone", ticketsInReservation.size() > 1);
                    }

                    try {
                        model.addAttribute("delayForOfflinePayment", Math.max(1, TicketReservationManager
                                .getOfflinePaymentWaitingPeriod(event, configurationManager)));
                    } catch (TicketReservationManager.OfflinePaymentException e) {
                        if (event.getAllowedPaymentProxies().contains(PaymentProxy.OFFLINE)) {
                            log.error("Already started event {} has been found with OFFLINE payment enabled",
                                    event.getDisplayName(), e);
                        }
                        model.addAttribute("delayForOfflinePayment", 0);
                    }

                    OrderSummary orderSummary = ticketReservationManager.orderSummaryForReservationId(reservationId,
                            event, locale);
                    List<PaymentProxy> activePaymentMethods = paymentManager
                            .getPaymentMethods(event.getOrganizationId()).stream()
                            .filter(p -> TicketReservationManager.isValidPaymentMethod(p, event,
                                    configurationManager))
                            .map(PaymentManager.PaymentMethod::getPaymentProxy).collect(toList());

                    if (orderSummary.getFree() || activePaymentMethods.stream()
                            .anyMatch(p -> p == PaymentProxy.OFFLINE || p == PaymentProxy.ON_SITE)) {
                        boolean captchaForOfflinePaymentEnabled = configurationManager
                                .isRecaptchaForOfflinePaymentEnabled(event);
                        model.addAttribute("captchaRequestedForOffline", captchaForOfflinePaymentEnabled)
                                .addAttribute("recaptchaApiKey",
                                        configurationManager.getStringConfigValue(
                                                Configuration.getSystemConfiguration(RECAPTCHA_API_KEY), null))
                                .addAttribute("captchaRequestedFreeOfCharge",
                                        orderSummary.getFree() && captchaForOfflinePaymentEnabled);
                    }

                    boolean invoiceAllowed = configurationManager.hasAllConfigurationsForInvoice(event)
                            || vatChecker.isVatCheckingEnabledFor(event.getOrganizationId());
                    PaymentForm paymentForm = PaymentForm.fromExistingReservation(reservation);
                    model.addAttribute("multiplePaymentMethods", activePaymentMethods.size() > 1)
                            .addAttribute("orderSummary", orderSummary).addAttribute("reservationId", reservationId)
                            .addAttribute("reservation", reservation)
                            .addAttribute("pageTitle", "reservation-page.header.title").addAttribute("event", event)
                            .addAttribute("activePaymentMethods", activePaymentMethods)
                            .addAttribute("expressCheckoutEnabled", isExpressCheckoutEnabled(event, orderSummary))
                            .addAttribute("useFirstAndLastName", event.mustUseFirstAndLastName())
                            .addAttribute("countries", TicketHelper.getLocalizedCountries(locale))
                            .addAttribute("euCountries",
                                    TicketHelper.getLocalizedEUCountries(locale,
                                            configurationManager.getRequiredValue(
                                                    getSystemConfiguration(ConfigurationKeys.EU_COUNTRIES_LIST))))
                            .addAttribute("euVatCheckingEnabled",
                                    vatChecker.isVatCheckingEnabledFor(event.getOrganizationId()))
                            .addAttribute("invoiceIsAllowed", invoiceAllowed)
                            .addAttribute("vatNrIsLinked",
                                    orderSummary.isVatExempt() || paymentForm.getHasVatCountryCode())
                            .addAttribute("billingAddressLabel", invoiceAllowed ? "reservation-page.billing-address"
                                    : "reservation-page.receipt-address");

                    boolean includeStripe = !orderSummary.getFree()
                            && activePaymentMethods.contains(PaymentProxy.STRIPE);
                    model.addAttribute("includeStripe", includeStripe);
                    if (includeStripe) {
                        model.addAttribute("stripe_p_key", paymentManager.getStripePublicKey(event));
                    }
                    Map<String, Object> modelMap = model.asMap();
                    modelMap.putIfAbsent("paymentForm", paymentForm);
                    modelMap.putIfAbsent("hasErrors", false);

                    boolean hasPaidSupplement = ticketReservationManager.hasPaidSupplements(reservationId);
                    model.addAttribute("ticketsByCategory", ticketsInReservation.stream()
                            .collect(Collectors.groupingBy(Ticket::getCategoryId)).entrySet().stream().map((e) -> {
                                TicketCategory category = eventManager.getTicketCategoryById(e.getKey(),
                                        event.getId());
                                List<TicketDecorator> decorators = TicketDecorator
                                        .decorate(e.getValue(),
                                                !hasPaidSupplement && configurationManager.getBooleanConfigValue(
                                                        Configuration.from(event.getOrganizationId(), event.getId(),
                                                                category.getId(), ALLOW_FREE_TICKETS_CANCELLATION),
                                                        false),
                                                eventManager.checkTicketCancellationPrerequisites(),
                                                ticket -> ticketHelper.findTicketFieldConfigurationAndValue(
                                                        event.getId(), ticket, locale),
                                                true, (t) -> "tickets['" + t.getUuid() + "'].");
                                return Pair.of(category, decorators);
                            }).collect(toList()));
                    return "/event/reservation-page";
                }).orElseGet(() -> redirectReservation(Optional.empty(), eventName, reservationId)))
                .orElse("redirect:/");
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/success", method = RequestMethod.GET)
    public String showConfirmationPage(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId,
            @RequestParam(value = "confirmation-email-sent", required = false, defaultValue = "false") boolean confirmationEmailSent,
            @RequestParam(value = "ticket-email-sent", required = false, defaultValue = "false") boolean ticketEmailSent,
            Model model, Locale locale, HttpServletRequest request) {

        return eventRepository.findOptionalByShortName(eventName).map(ev -> {
            Optional<TicketReservation> tr = ticketReservationManager.findById(reservationId);
            return tr.filter(r -> r.getStatus() == TicketReservationStatus.COMPLETE).map(reservation -> {
                SessionUtil.removeSpecialPriceData(request);
                model.addAttribute("reservationId", reservationId);
                model.addAttribute("reservation", reservation);
                model.addAttribute("confirmationEmailSent", confirmationEmailSent);
                model.addAttribute("ticketEmailSent", ticketEmailSent);

                List<Ticket> tickets = ticketReservationManager.findTicketsInReservation(reservationId);
                List<Triple<AdditionalService, List<AdditionalServiceText>, AdditionalServiceItem>> additionalServices = ticketReservationManager
                        .findAdditionalServicesInReservation(reservationId).stream()
                        .map(t -> Triple.of(
                                t.getLeft(), t.getMiddle().stream()
                                        .filter(d -> d.getLocale().equals(locale.getLanguage())).collect(toList()),
                                t.getRight()))
                        .collect(Collectors.toList());
                boolean hasPaidSupplement = ticketReservationManager.hasPaidSupplements(reservationId);
                model.addAttribute("ticketsByCategory", tickets.stream()
                        .collect(Collectors.groupingBy(Ticket::getCategoryId)).entrySet().stream().map((e) -> {
                            TicketCategory category = eventManager.getTicketCategoryById(e.getKey(), ev.getId());
                            List<TicketDecorator> decorators = TicketDecorator
                                    .decorate(e.getValue(),
                                            !hasPaidSupplement && configurationManager.getBooleanConfigValue(
                                                    Configuration.from(ev.getOrganizationId(), ev.getId(),
                                                            category.getId(), ALLOW_FREE_TICKETS_CANCELLATION),
                                                    false),
                                            eventManager.checkTicketCancellationPrerequisites(),
                                            ticket -> ticketHelper.findTicketFieldConfigurationAndValue(ev.getId(),
                                                    ticket, locale),
                                            tickets.size() == 1, TicketDecorator.EMPTY_PREFIX_GENERATOR);
                            return Pair.of(category, decorators);
                        }).collect(toList()));
                boolean ticketsAllAssigned = tickets.stream().allMatch(Ticket::getAssigned);
                model.addAttribute("ticketsAreAllAssigned", ticketsAllAssigned);
                model.addAttribute("collapseEnabled", tickets.size() > 1 && !ticketsAllAssigned);
                model.addAttribute("additionalServicesOnly", tickets.isEmpty() && !additionalServices.isEmpty());
                model.addAttribute("additionalServices", additionalServices);
                model.addAttribute("countries", TicketHelper.getLocalizedCountries(locale));
                model.addAttribute("pageTitle", "reservation-page-complete.header.title");
                model.addAttribute("event", ev);
                model.addAttribute("useFirstAndLastName", ev.mustUseFirstAndLastName());
                model.asMap().putIfAbsent("validationResult", ValidationResult.success());
                return "/event/reservation-page-complete";
            }).orElseGet(() -> redirectReservation(tr, eventName, reservationId));
        }).orElse("redirect:/");
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/failure", method = RequestMethod.GET)
    public String showFailurePage(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId,
            @RequestParam(value = "confirmation-email-sent", required = false, defaultValue = "false") boolean confirmationEmailSent,
            @RequestParam(value = "ticket-email-sent", required = false, defaultValue = "false") boolean ticketEmailSent,
            Model model) {
        Optional<Event> event = eventRepository.findOptionalByShortName(eventName);
        if (!event.isPresent()) {
            return "redirect:/";
        }

        Optional<TicketReservation> reservation = ticketReservationManager.findById(reservationId);
        Optional<TicketReservationStatus> status = reservation.map(TicketReservation::getStatus);

        if (!status.isPresent()) {
            return redirectReservation(reservation, eventName, reservationId);
        }

        TicketReservationStatus ticketReservationStatus = status.get();
        if (ticketReservationStatus == TicketReservationStatus.IN_PAYMENT
                || ticketReservationStatus == TicketReservationStatus.STUCK) {
            model.addAttribute("reservation", reservation.get());
            model.addAttribute("organizer", organizationRepository.getById(event.get().getOrganizationId()));
            model.addAttribute("pageTitle", "reservation-page-error-status.header.title");
            model.addAttribute("event", event.get());
            return "/event/reservation-page-error-status";
        }

        return redirectReservation(reservation, eventName, reservationId);
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}", method = RequestMethod.GET)
    public String showReservationPage(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, Model model) {
        Optional<Event> event = eventRepository.findOptionalByShortName(eventName);
        if (!event.isPresent()) {
            return "redirect:/";
        }

        return redirectReservation(ticketReservationManager.findById(reservationId), eventName, reservationId);
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/notfound", method = RequestMethod.GET)
    public String showNotFoundPage(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, Model model) {

        Optional<Event> event = eventRepository.findOptionalByShortName(eventName);
        if (!event.isPresent()) {
            return "redirect:/";
        }

        Optional<TicketReservation> reservation = ticketReservationManager.findById(reservationId);

        if (!reservation.isPresent()) {
            model.addAttribute("reservationId", reservationId);
            model.addAttribute("pageTitle", "reservation-page-not-found.header.title");
            model.addAttribute("event", event.get());
            return "/event/reservation-page-not-found";
        }

        return redirectReservation(reservation, eventName, reservationId);
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/waitingPayment", method = RequestMethod.GET)
    public String showWaitingPaymentPage(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, Model model, Locale locale) {

        Optional<Event> event = eventRepository.findOptionalByShortName(eventName);
        if (!event.isPresent()) {
            return "redirect:/";
        }

        Optional<TicketReservation> reservation = ticketReservationManager.findById(reservationId);
        TicketReservationStatus status = reservation.map(TicketReservation::getStatus)
                .orElse(TicketReservationStatus.PENDING);
        if (reservation.isPresent() && status == TicketReservationStatus.OFFLINE_PAYMENT) {
            Event ev = event.get();
            TicketReservation ticketReservation = reservation.get();
            OrderSummary orderSummary = ticketReservationManager.orderSummaryForReservationId(reservationId, ev,
                    locale);
            model.addAttribute("totalPrice", orderSummary.getTotalPrice());
            model.addAttribute("emailAddress", organizationRepository.getById(ev.getOrganizationId()).getEmail());
            model.addAttribute("reservation", ticketReservation);
            model.addAttribute("paymentReason",
                    ev.getShortName() + " " + ticketReservationManager.getShortReservationID(ev, reservationId));
            model.addAttribute("pageTitle", "reservation-page-waiting.header.title");
            model.addAttribute("bankAccount",
                    configurationManager
                            .getStringConfigValue(
                                    Configuration.from(ev.getOrganizationId(), ev.getId(), BANK_ACCOUNT_NR))
                            .orElse(""));

            Optional<String> maybeAccountOwner = configurationManager.getStringConfigValue(
                    Configuration.from(ev.getOrganizationId(), ev.getId(), BANK_ACCOUNT_OWNER));
            model.addAttribute("hasBankAccountOwnerSet", maybeAccountOwner.isPresent());
            model.addAttribute("bankAccountOwner", Arrays.asList(maybeAccountOwner.orElse("").split("\n")));

            model.addAttribute("expires",
                    ZonedDateTime.ofInstant(ticketReservation.getValidity().toInstant(), ev.getZoneId()));
            model.addAttribute("event", ev);
            return "/event/reservation-waiting-for-payment";
        }

        return redirectReservation(reservation, eventName, reservationId);
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/processing-payment", method = RequestMethod.GET)
    public String showProcessingPayment(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, Model model, Locale locale) {

        //FIXME
        return "/event/reservation-processing-payment";
    }

    private String redirectReservation(Optional<TicketReservation> ticketReservation, String eventName,
            String reservationId) {
        String baseUrl = "redirect:/event/" + eventName + "/reservation/" + reservationId;
        if (!ticketReservation.isPresent()) {
            return baseUrl + "/notfound";
        }
        TicketReservation reservation = ticketReservation.get();

        switch (reservation.getStatus()) {
        case PENDING:
            return baseUrl + "/book";
        case COMPLETE:
            return baseUrl + "/success";
        case OFFLINE_PAYMENT:
            return baseUrl + "/waitingPayment";
        case EXTERNAL_PROCESSING_PAYMENT:
            return baseUrl + "/processing-payment";
        case IN_PAYMENT:
        case STUCK:
            return baseUrl + "/failure";
        }

        return "redirect:/";
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}", method = RequestMethod.POST)
    public String handleReservation(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, PaymentForm paymentForm,
            BindingResult bindingResult, Model model, HttpServletRequest request, Locale locale,
            RedirectAttributes redirectAttributes) {

        Optional<Event> eventOptional = eventRepository.findOptionalByShortName(eventName);
        if (!eventOptional.isPresent()) {
            return "redirect:/";
        }
        Event event = eventOptional.get();
        Optional<TicketReservation> ticketReservation = ticketReservationManager.findById(reservationId);
        if (!ticketReservation.isPresent()) {
            return redirectReservation(ticketReservation, eventName, reservationId);
        }
        if (paymentForm.shouldCancelReservation()) {
            ticketReservationManager.cancelPendingReservation(reservationId, false);
            SessionUtil.removeSpecialPriceData(request);
            return "redirect:/event/" + eventName + "/";
        }
        if (!ticketReservation.get().getValidity().after(new Date())) {
            bindingResult.reject(ErrorsCode.STEP_2_ORDER_EXPIRED);
        }

        final TotalPrice reservationCost = ticketReservationManager.totalReservationCostWithVAT(reservationId);
        if (isCaptchaInvalid(reservationCost.getPriceWithVAT(), paymentForm.getPaymentMethod(), request, event)) {
            log.debug("captcha validation failed.");
            bindingResult.reject(ErrorsCode.STEP_2_CAPTCHA_VALIDATION_FAILED);
        }

        if (paymentForm.getPaymentMethod() != PaymentProxy.PAYPAL || !paymentForm.hasPaypalTokens()) {
            if (!paymentForm.isPostponeAssignment()
                    && !ticketRepository.checkTicketUUIDs(reservationId, paymentForm.getTickets().keySet())) {
                bindingResult.reject(ErrorsCode.STEP_2_MISSING_ATTENDEE_DATA);
            }
            paymentForm.validate(bindingResult, reservationCost, event,
                    ticketFieldRepository.findAdditionalFieldsForEvent(event.getId()));
            if (bindingResult.hasErrors()) {
                SessionUtil.addToFlash(bindingResult, redirectAttributes);
                return redirectReservation(ticketReservation, eventName, reservationId);
            }
        }

        CustomerName customerName = new CustomerName(paymentForm.getFullName(), paymentForm.getFirstName(),
                paymentForm.getLastName(), event);

        //handle paypal redirect!
        if (paymentForm.getPaymentMethod() == PaymentProxy.PAYPAL && !paymentForm.hasPaypalTokens()) {
            OrderSummary orderSummary = ticketReservationManager.orderSummaryForReservationId(reservationId, event,
                    locale);
            try {
                String checkoutUrl = paymentManager.createPaypalCheckoutRequest(event, reservationId, orderSummary,
                        customerName, paymentForm.getEmail(), paymentForm.getBillingAddress(), locale,
                        paymentForm.isPostponeAssignment());
                assignTickets(eventName, reservationId, paymentForm, bindingResult, request, true);
                return "redirect:" + checkoutUrl;
            } catch (Exception e) {
                bindingResult.reject(ErrorsCode.STEP_2_PAYMENT_REQUEST_CREATION);
                SessionUtil.addToFlash(bindingResult, redirectAttributes);
                return redirectReservation(ticketReservation, eventName, reservationId);
            }
        }

        //handle mollie redirect
        if (paymentForm.getPaymentMethod() == PaymentProxy.MOLLIE) {
            OrderSummary orderSummary = ticketReservationManager.orderSummaryForReservationId(reservationId, event,
                    locale);
            try {
                String checkoutUrl = mollieManager.createCheckoutRequest(event, reservationId, orderSummary,
                        customerName, paymentForm.getEmail(), paymentForm.getBillingAddress(), locale,
                        paymentForm.isInvoiceRequested(), paymentForm.getVatCountryCode(), paymentForm.getVatNr(),
                        ticketReservation.get().getVatStatus());
                assignTickets(eventName, reservationId, paymentForm, bindingResult, request, true);
                return "redirect:" + checkoutUrl;
            } catch (Exception e) {
                bindingResult.reject(ErrorsCode.STEP_2_PAYMENT_REQUEST_CREATION);
                SessionUtil.addToFlash(bindingResult, redirectAttributes);
                return redirectReservation(ticketReservation, eventName, reservationId);
            }
        }
        //

        final PaymentResult status = ticketReservationManager.confirm(paymentForm.getToken(),
                paymentForm.getPaypalPayerID(), event, reservationId, paymentForm.getEmail(), customerName, locale,
                paymentForm.getBillingAddress(), reservationCost,
                SessionUtil.retrieveSpecialPriceSessionId(request),
                Optional.ofNullable(paymentForm.getPaymentMethod()), paymentForm.isInvoiceRequested(),
                paymentForm.getVatCountryCode(), paymentForm.getVatNr(), ticketReservation.get().getVatStatus());

        if (!status.isSuccessful()) {
            String errorMessageCode = status.getErrorCode().get();
            MessageSourceResolvable message = new DefaultMessageSourceResolvable(
                    new String[] { errorMessageCode, StripeManager.STRIPE_UNEXPECTED });
            bindingResult.reject(ErrorsCode.STEP_2_PAYMENT_PROCESSING_ERROR,
                    new Object[] { messageSource.getMessage(message, locale) }, null);
            SessionUtil.addToFlash(bindingResult, redirectAttributes);
            return redirectReservation(ticketReservation, eventName, reservationId);
        }

        //
        TicketReservation reservation = ticketReservationManager.findById(reservationId)
                .orElseThrow(IllegalStateException::new);
        sendReservationCompleteEmail(request, event, reservation);
        sendReservationCompleteEmailToOrganizer(request, event, reservation);
        //

        if (paymentForm.getPaymentMethod() != PaymentProxy.PAYPAL) {
            assignTickets(eventName, reservationId, paymentForm, bindingResult, request,
                    paymentForm.getPaymentMethod() == PaymentProxy.OFFLINE);
        }

        return "redirect:/event/" + eventName + "/reservation/" + reservationId + "/success";
    }

    private boolean isCaptchaInvalid(int cost, PaymentProxy paymentMethod, HttpServletRequest request,
            Event event) {
        return (cost == 0 || paymentMethod == PaymentProxy.OFFLINE || paymentMethod == PaymentProxy.ON_SITE)
                && configurationManager.isRecaptchaForOfflinePaymentEnabled(event)
                && !recaptchaService.checkRecaptcha(request);
    }

    private void assignTickets(String eventName, String reservationId, PaymentForm paymentForm,
            BindingResult bindingResult, HttpServletRequest request, boolean preAssign) {
        if (!paymentForm.isPostponeAssignment()) {
            paymentForm.getTickets().forEach((ticketId, owner) -> {
                if (preAssign) {
                    ticketHelper.preAssignTicket(eventName, reservationId, ticketId, owner,
                            Optional.of(bindingResult), request, (tr) -> {
                            }, Optional.empty());
                } else {
                    ticketHelper.assignTicket(eventName, ticketId, owner, Optional.of(bindingResult), request,
                            (tr) -> {
                            }, Optional.empty());
                }
            });
        }
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/re-send-email", method = RequestMethod.POST)
    public String reSendReservationConfirmationEmail(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, HttpServletRequest request) {

        Optional<Event> event = eventRepository.findOptionalByShortName(eventName);
        if (!event.isPresent()) {
            return "redirect:/";
        }

        Optional<TicketReservation> ticketReservation = ticketReservationManager.findById(reservationId);
        if (!ticketReservation.isPresent()) {
            return "redirect:/event/" + eventName + "/";
        }

        sendReservationCompleteEmail(request, event.get(),
                ticketReservation.orElseThrow(IllegalStateException::new));
        return "redirect:/event/" + eventName + "/reservation/" + reservationId
                + "/success?confirmation-email-sent=true";
    }

    @RequestMapping(value = "/event/{eventName}/reservation/{reservationId}/ticket/{ticketIdentifier}/assign", method = RequestMethod.POST)
    public String assignTicketToPerson(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId,
            @PathVariable("ticketIdentifier") String ticketIdentifier, UpdateTicketOwnerForm updateTicketOwner,
            BindingResult bindingResult, HttpServletRequest request, Model model) throws Exception {

        Optional<Triple<ValidationResult, Event, Ticket>> result = ticketHelper.assignTicket(eventName,
                reservationId, ticketIdentifier, updateTicketOwner, Optional.of(bindingResult), request, model);
        return result.map(t -> "redirect:/event/" + t.getMiddle().getShortName() + "/reservation/"
                + t.getRight().getTicketsReservationId() + "/success").orElse("redirect:/");
    }

    private void sendReservationCompleteEmail(HttpServletRequest request, Event event,
            TicketReservation reservation) {
        Locale locale = RequestContextUtils.getLocale(request);
        ticketReservationManager.sendConfirmationEmail(event, reservation, locale);
    }

    private void sendReservationCompleteEmailToOrganizer(HttpServletRequest request, Event event,
            TicketReservation reservation) {

        Organization organization = organizationRepository.getById(event.getOrganizationId());
        List<String> cc = notificationManager.getCCForEventOrganizer(event);

        notificationManager.sendSimpleEmail(event, organization.getEmail(), cc,
                "Reservation complete " + reservation.getId(),
                () -> templateManager.renderTemplate(event, TemplateResource.CONFIRMATION_EMAIL_FOR_ORGANIZER,
                        ticketReservationManager.prepareModelForReservationEmail(event, reservation),
                        RequestContextUtils.getLocale(request)));
    }

    private boolean isExpressCheckoutEnabled(Event event, OrderSummary orderSummary) {
        return orderSummary.getTicketAmount() == 1
                && ticketFieldRepository.countRequiredAdditionalFieldsForEvent(event.getId()) == 0;
    }
}