alfio.controller.api.admin.EventApiController.java Source code

Java tutorial

Introduction

Here is the source code for alfio.controller.api.admin.EventApiController.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.api.admin;

import alfio.controller.api.support.DescriptionsLoader;
import alfio.controller.api.support.EventListItem;
import alfio.controller.api.support.PageAndContent;
import alfio.controller.api.support.TicketHelper;
import alfio.controller.support.TemplateProcessor;
import alfio.manager.*;
import alfio.manager.i18n.I18nManager;
import alfio.manager.user.UserManager;
import alfio.model.*;
import alfio.model.modification.*;
import alfio.model.result.ValidationResult;
import alfio.model.user.Organization;
import alfio.model.user.Role;
import alfio.repository.DynamicFieldTemplateRepository;
import alfio.repository.SponsorScanRepository;
import alfio.repository.TicketCategoryDescriptionRepository;
import alfio.repository.TicketFieldRepository;
import alfio.util.Json;
import alfio.util.MonetaryUtil;
import alfio.util.TemplateManager;
import alfio.util.Validator;
import com.opencsv.CSVReader;
import com.opencsv.CSVWriter;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.util.StreamUtils;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.security.Principal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static alfio.util.OptionalWrapper.optionally;
import static alfio.util.Validator.*;
import static java.util.stream.Collectors.toList;
import static org.springframework.web.bind.annotation.RequestMethod.*;

@RestController
@RequestMapping("/admin/api")
@Log4j2
@AllArgsConstructor
public class EventApiController {

    private static final String OK = "OK";
    private static final String CUSTOM_FIELDS_PREFIX = "custom:";
    private final EventManager eventManager;
    private final EventStatisticsManager eventStatisticsManager;
    private final I18nManager i18nManager;
    private final TicketReservationManager ticketReservationManager;
    private final TicketCategoryDescriptionRepository ticketCategoryDescriptionRepository;
    private final TicketFieldRepository ticketFieldRepository;
    private final DescriptionsLoader descriptionsLoader;
    private final TicketHelper ticketHelper;
    private final DynamicFieldTemplateRepository dynamicFieldTemplateRepository;
    private final UserManager userManager;
    private final SponsorScanRepository sponsorScanRepository;
    private final PaymentManager paymentManager;
    private final TemplateManager templateManager;
    private final FileUploadManager fileUploadManager;

    @ExceptionHandler(DataAccessException.class)
    public String exception(DataAccessException e) {
        log.warn("unhandled exception", e);
        return "unexpected error. More info in the application log";
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String unhandledException(Exception e) {
        if (!IllegalArgumentException.class.isInstance(e)) {
            log.warn("unhandled exception", e);
        }
        return e.getMessage();
    }

    @RequestMapping(value = "/paymentProxies/{organizationId}", method = GET)
    @ResponseStatus(HttpStatus.OK)
    public List<PaymentManager.PaymentMethod> getPaymentProxies(@PathVariable("organizationId") int organizationId,
            Principal principal) {
        return userManager.findUserOrganizations(principal.getName()).stream()
                .filter(o -> o.getId() == organizationId).findFirst()
                .map(o -> paymentManager.getPaymentMethods(o.getId())).orElse(Collections.emptyList());
    }

    @RequestMapping(value = "/events", method = GET, headers = "Authorization")
    public List<EventListItem> getAllEventsForExternal(Principal principal, HttpServletRequest request) {
        List<Integer> userOrganizations = userManager.findUserOrganizations(principal.getName()).stream()
                .map(Organization::getId).collect(toList());
        return eventManager.getActiveEvents().stream()
                .filter(e -> userOrganizations.contains(e.getOrganizationId()))
                .sorted(Comparator.comparing(e -> e.getBegin().withZoneSameInstant(ZoneId.systemDefault())))
                .map(s -> new EventListItem(s, request.getContextPath(), descriptionsLoader.eventDescriptions()))
                .collect(toList());
    }

    @RequestMapping(value = "/events", method = GET)
    public List<EventStatistic> getAllEvents(Principal principal) {
        return eventStatisticsManager.getAllEventsWithStatistics(principal.getName());
    }

    @RequestMapping(value = "/active-events", method = GET)
    public List<EventStatistic> getAllActiveEvents(Principal principal) {
        return eventStatisticsManager.getAllEventsWithStatisticsFilteredBy(principal.getName(),
                event -> !event.expiredSince(14));
    }

    @RequestMapping(value = "/expired-events", method = GET)
    public List<EventStatistic> getAllExpiredEvents(Principal principal) {
        return eventStatisticsManager.getAllEventsWithStatisticsFilteredBy(principal.getName(),
                event -> event.expiredSince(14));
    }

    @AllArgsConstructor
    @Getter
    public static class EventAndOrganization {
        private final EventWithAdditionalInfo event;
        private final Organization organization;
    }

    @RequestMapping(value = "/events/{name}", method = GET)
    public ResponseEntity<EventAndOrganization> getSingleEvent(@PathVariable("name") String eventName,
            Principal principal) {
        final String username = principal.getName();
        return optionally(() -> eventStatisticsManager.getEventWithAdditionalInfo(eventName, username))
                .map(event -> {
                    EventAndOrganization out = new EventAndOrganization(event,
                            eventManager.loadOrganizer(event.getEvent(), username));
                    return ResponseEntity.ok(out);
                }).orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @RequestMapping(value = "/events/{eventId}", method = DELETE)
    public void deleteEvent(@PathVariable("eventId") int eventId, Principal principal) {
        eventManager.deleteEvent(eventId, principal.getName());
    }

    @RequestMapping(value = "/events/id/{eventId}", method = GET)
    public Event getSingleEventById(@PathVariable("eventId") int eventId, Principal principal) {
        return eventManager.getSingleEventById(eventId, principal.getName());
    }

    @RequestMapping(value = "/events/check", method = POST)
    public ValidationResult validateEvent(@RequestBody EventModification eventModification, Errors errors) {
        ValidationResult base = validateEventHeader(Optional.empty(), eventModification, errors)
                .or(validateEventPrices(Optional.empty(), eventModification, errors))
                .or(eventModification.getAdditionalServices().stream()
                        .map(as -> validateAdditionalService(as, eventModification, errors))
                        .reduce(ValidationResult::or).orElse(ValidationResult.success()));
        AtomicInteger counter = new AtomicInteger();
        return base.or(eventModification.getTicketCategories().stream()
                .map(c -> validateCategory(c, errors, "ticketCategories[" + counter.getAndIncrement() + "]."))
                .reduce(ValidationResult::or).orElse(ValidationResult.success()))
                .or(validateAdditionalTicketFields(eventModification.getTicketFields(), errors));
    }

    private ValidationResult validateAdditionalTicketFields(List<EventModification.AdditionalField> ticketFields,
            Errors errors) {
        //meh
        AtomicInteger cnt = new AtomicInteger();
        return Optional.ofNullable(ticketFields).orElseGet(Collections::emptyList).stream().map(field -> {
            String prefix = "ticketFields[" + cnt.getAndIncrement() + "]";
            if (StringUtils.isBlank(field.getName())) {
                errors.rejectValue(prefix + ".name", "error.required");
            }
            //TODO: check label value is present for all the locales
            //TODO: for select check option value+label

            return Validator.evaluateValidationResult(errors);
        }).reduce(ValidationResult::or).orElseGet(ValidationResult::success);
    }

    @RequestMapping(value = "/events/new", method = POST)
    public String insertEvent(@RequestBody EventModification eventModification) {
        eventManager.createEvent(eventModification);
        return OK;
    }

    @RequestMapping(value = "/events/{id}/status", method = PUT)
    public String activateEvent(@PathVariable("id") int id, @RequestParam("active") boolean active,
            Principal principal) {
        eventManager.toggleActiveFlag(id, principal.getName(), active);
        return OK;
    }

    @RequestMapping(value = "/events/{id}/header/update", method = POST)
    public ValidationResult updateHeader(@PathVariable("id") int id,
            @RequestBody EventModification eventModification, Errors errors, Principal principal) {
        Event event = eventManager.getSingleEventById(id, principal.getName());
        return validateEventHeader(Optional.of(event), eventModification, errors)
                .ifSuccess(() -> eventManager.updateEventHeader(event, eventModification, principal.getName()));
    }

    @RequestMapping(value = "/events/{id}/prices/update", method = POST)
    public ValidationResult updatePrices(@PathVariable("id") int id,
            @RequestBody EventModification eventModification, Errors errors, Principal principal) {
        Event event = eventManager.getSingleEventById(id, principal.getName());
        return validateEventPrices(Optional.of(event), eventModification, errors)
                .ifSuccess(() -> eventManager.updateEventPrices(event, eventModification, principal.getName()));
    }

    @RequestMapping(value = "/events/{eventId}/categories/{categoryId}/update", method = POST)
    public ValidationResult updateExistingCategory(@PathVariable("eventId") int eventId,
            @PathVariable("categoryId") int categoryId, @RequestBody TicketCategoryModification category,
            Errors errors, Principal principal) {
        return validateCategory(category, errors)
                .ifSuccess(() -> eventManager.updateCategory(categoryId, eventId, category, principal.getName()));
    }

    @RequestMapping(value = "/events/{eventId}/categories/new", method = POST)
    public ValidationResult createCategory(@PathVariable("eventId") int eventId,
            @RequestBody TicketCategoryModification category, Errors errors, Principal principal) {
        return validateCategory(category, errors)
                .ifSuccess(() -> eventManager.insertCategory(eventId, category, principal.getName()));
    }

    @RequestMapping(value = "/events/reallocate", method = PUT)
    public String reallocateTickets(@RequestBody TicketAllocationModification form) {
        eventManager.reallocateTickets(form.getSrcCategoryId(), form.getTargetCategoryId(), form.getEventId());
        return OK;
    }

    @RequestMapping(value = "/events/{eventName}/category/{categoryId}/unbind-tickets", method = PUT)
    public String unbindTickets(@PathVariable("eventName") String eventName,
            @PathVariable("categoryId") int categoryId, Principal principal) {
        eventManager.unbindTickets(eventName, categoryId, principal.getName());
        return OK;
    }

    private static final List<String> FIXED_FIELDS = Arrays.asList("ID", "Creation", "Category", "Event", "Status",
            "OriginalPrice", "PaidPrice", "Discount", "VAT", "ReservationID", "Full Name", "First Name",
            "Last Name", "E-Mail", "Locked", "Language", "Confirmation", "Billing Address");
    private static final int[] BOM_MARKERS = new int[] { 0xEF, 0xBB, 0xBF };

    @RequestMapping("/events/{eventName}/export.csv")
    public void downloadAllTicketsCSV(@PathVariable("eventName") String eventName, HttpServletRequest request,
            HttpServletResponse response, Principal principal) throws IOException {
        List<String> fields = Arrays
                .asList(Optional.ofNullable(request.getParameterValues("fields")).orElse(new String[] {}));
        Event event = loadEvent(eventName, principal);
        Map<Integer, TicketCategory> categoriesMap = eventManager.loadTicketCategories(event).stream()
                .collect(Collectors.toMap(TicketCategory::getId, Function.identity()));
        ZoneId eventZoneId = event.getZoneId();

        Predicate<String> contains = FIXED_FIELDS::contains;

        response.setContentType("text/csv;charset=UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=" + eventName + "-export.csv");

        try (ServletOutputStream out = response.getOutputStream();
                CSVWriter writer = new CSVWriter(new OutputStreamWriter(out))) {

            for (int marker : BOM_MARKERS) {//UGLY-MODE_ON: specify that the file is written in UTF-8 with BOM, thanks to alexr http://stackoverflow.com/a/4192897
                out.write(marker);
            }

            writer.writeNext(fields.stream().map(f -> {
                if (f.startsWith(CUSTOM_FIELDS_PREFIX)) {
                    return f.substring(CUSTOM_FIELDS_PREFIX.length());
                }
                return f;
            }).toArray(String[]::new));

            eventManager.findAllConfirmedTicketsForCSV(eventName, principal.getName()).stream().map(t -> {
                List<String> line = new ArrayList<>();
                if (fields.contains("ID")) {
                    line.add(t.getUuid());
                }
                if (fields.contains("Creation")) {
                    line.add(t.getCreation().withZoneSameInstant(eventZoneId).toString());
                }
                if (fields.contains("Category")) {
                    line.add(categoriesMap.get(t.getCategoryId()).getName());
                }
                if (fields.contains("Event")) {
                    line.add(eventName);
                }
                if (fields.contains("Status")) {
                    line.add(t.getStatus().toString());
                }
                if (fields.contains("OriginalPrice")) {
                    line.add(MonetaryUtil.centsToUnit(t.getSrcPriceCts()).toString());
                }
                if (fields.contains("PaidPrice")) {
                    line.add(MonetaryUtil.centsToUnit(t.getFinalPriceCts()).toString());
                }
                if (fields.contains("Discount")) {
                    line.add(MonetaryUtil.centsToUnit(t.getDiscountCts()).toString());
                }
                if (fields.contains("VAT")) {
                    line.add(MonetaryUtil.centsToUnit(t.getVatCts()).toString());
                }
                if (fields.contains("ReservationID")) {
                    line.add(t.getTicketsReservationId());
                }
                if (fields.contains("Full Name")) {
                    line.add(t.getFullName());
                }
                if (fields.contains("First Name")) {
                    line.add(t.getFirstName());
                }
                if (fields.contains("Last Name")) {
                    line.add(t.getLastName());
                }
                if (fields.contains("E-Mail")) {
                    line.add(t.getEmail());
                }
                if (fields.contains("Locked")) {
                    line.add(String.valueOf(t.getLockedAssignment()));
                }
                if (fields.contains("Language")) {
                    line.add(String.valueOf(t.getUserLanguage()));
                }
                if (fields.contains("Confirmation")) {
                    line.add(t.getTicketReservation().getConfirmationTimestamp().withZoneSameInstant(eventZoneId)
                            .toString());
                }
                if (fields.contains("Billing Address")) {
                    line.add(t.getTicketReservation().getBillingAddress());
                }

                //obviously not optimized
                Map<String, String> additionalValues = ticketFieldRepository.findAllValuesForTicketId(t.getId());

                fields.stream().filter(contains.negate()).filter(f -> f.startsWith(CUSTOM_FIELDS_PREFIX))
                        .forEachOrdered(field -> {
                            String customFieldName = field.substring(CUSTOM_FIELDS_PREFIX.length());
                            line.add(additionalValues.getOrDefault(customFieldName, "").replaceAll("\"", ""));
                        });

                return line.toArray(new String[line.size()]);
            }).forEachOrdered(writer::writeNext);
            writer.flush();
            out.flush();
        }
    }

    @RequestMapping("/events/{eventName}/sponsor-scan/export.csv")
    public void downloadSponsorScanExport(@PathVariable("eventName") String eventName, HttpServletResponse response,
            Principal principal) throws IOException {
        Event event = loadEvent(eventName, principal);
        List<TicketFieldConfiguration> fields = ticketFieldRepository.findAdditionalFieldsForEvent(event.getId());

        response.setContentType("text/csv;charset=UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=" + eventName + "-sponsor-scan.csv");

        try (ServletOutputStream out = response.getOutputStream();
                CSVWriter writer = new CSVWriter(new OutputStreamWriter(out))) {
            for (int marker : BOM_MARKERS) {
                out.write(marker);
            }

            List<String> header = new ArrayList<>();
            header.add("Username");
            header.add("Timestamp");
            header.add("Full name");
            header.add("Email");
            header.addAll(fields.stream().map(TicketFieldConfiguration::getName).collect(toList()));
            writer.writeNext(header.toArray(new String[header.size()]));
            userManager.findAllEnabledUsers(principal.getName()).stream()
                    .map(u -> Pair.of(u, userManager.getUserRole(u))).filter(p -> p.getRight() == Role.SPONSOR)
                    .flatMap(p -> sponsorScanRepository
                            .loadSponsorData(
                                    event.getId(), p.getKey().getId(), SponsorScanRepository.DEFAULT_TIMESTAMP)
                            .stream()
                            .map(v -> Pair.of(v,
                                    ticketFieldRepository.findAllValuesForTicketId(v.getTicket().getId()))))
                    .map(p -> {
                        DetailedScanData data = p.getLeft();
                        Map<String, String> descriptions = p.getRight();
                        return Pair.of(data, fields.stream().map(x -> descriptions.getOrDefault(x.getName(), ""))
                                .collect(toList()));
                    }).map(p -> {
                        List<String> line = new ArrayList<>();
                        Ticket ticket = p.getLeft().getTicket();
                        SponsorScan sponsorScan = p.getLeft().getSponsorScan();
                        line.add(userManager.findUser(sponsorScan.getUserId()).getUsername());
                        line.add(sponsorScan.getTimestamp().toString());
                        line.add(ticket.getFullName());
                        line.add(ticket.getEmail());
                        line.addAll(p.getRight());
                        return line.toArray(new String[line.size()]);
                    }).forEachOrdered(writer::writeNext);
            writer.flush();
            out.flush();
        }
    }

    @RequestMapping("/events/{eventName}/fields")
    public List<SerializablePair<String, String>> getAllFields(@PathVariable("eventName") String eventName) {
        List<SerializablePair<String, String>> fields = new ArrayList<>();
        fields.addAll(FIXED_FIELDS.stream().map(f -> SerializablePair.of(f, f)).collect(toList()));
        fields.addAll(ticketFieldRepository.findFieldsForEvent(eventName).stream()
                .map(f -> SerializablePair.of(CUSTOM_FIELDS_PREFIX + f, f)).collect(toList()));
        return fields;
    }

    @RequestMapping("/events/{eventName}/additional-field")
    public List<TicketFieldConfigurationAndAllDescriptions> getAllAdditionalField(
            @PathVariable("eventName") String eventName) {
        final Map<Integer, List<TicketFieldDescription>> descById = ticketFieldRepository
                .findDescriptions(eventName).stream()
                .collect(Collectors.groupingBy(TicketFieldDescription::getTicketFieldConfigurationId));
        return ticketFieldRepository.findAdditionalFieldsForEvent(eventName).stream()
                .map(field -> new TicketFieldConfigurationAndAllDescriptions(field,
                        descById.getOrDefault(field.getId(), Collections.emptyList())))
                .collect(toList());
    }

    @RequestMapping(value = "/event/additional-field/templates", method = GET)
    public List<DynamicFieldTemplate> loadTemplates() {
        return dynamicFieldTemplateRepository.loadAll();
    }

    @RequestMapping(value = "/events/{eventName}/additional-field/descriptions", method = POST)
    public void saveAdditionalFieldDescriptions(
            @RequestBody Map<String, TicketFieldDescriptionModification> descriptions) {
        eventManager.updateTicketFieldDescriptions(descriptions);
    }

    @RequestMapping(value = "/events/{eventName}/additional-field/new", method = POST)
    public ValidationResult addAdditionalField(@PathVariable("eventName") String eventName,
            @RequestBody EventModification.AdditionalField field, Principal principal, Errors errors) {
        Event event = eventManager.getSingleEvent(eventName, principal.getName());
        List<TicketFieldConfiguration> fields = ticketFieldRepository.findAdditionalFieldsForEvent(event.getId());
        return validateAdditionalFields(fields, field, errors)
                .ifSuccess(() -> eventManager.addAdditionalField(event, field));
    }

    @RequestMapping(value = "/events/{eventName}/additional-field/swap-position/{id1}/{id2}", method = POST)
    public void swapAdditionalFieldPosition(@PathVariable("eventName") String eventName,
            @PathVariable("id1") int id1, @PathVariable("id2") int id2, Principal principal) {
        Event event = eventManager.getSingleEvent(eventName, principal.getName());
        eventManager.swapAdditionalFieldPosition(event.getId(), id1, id2);
    }

    @RequestMapping(value = "/events/{eventName}/additional-field/{id}", method = DELETE)
    public void deleteAdditionalField(@PathVariable("eventName") String eventName, @PathVariable("id") int id,
            Principal principal) {
        eventManager.getSingleEvent(eventName, principal.getName());
        eventManager.deleteAdditionalField(id);
    }

    @RequestMapping(value = "/events/{eventName}/additional-field/{id}", method = POST)
    public void updateAdditionalField(@PathVariable("eventName") String eventName, @PathVariable("id") int id,
            @RequestBody EventModification.UpdateAdditionalField field, Principal principal) {
        eventManager.getSingleEvent(eventName, principal.getName());
        eventManager.updateAdditionalField(id, field);
    }

    @RequestMapping(value = "/events/{eventName}/pending-payments")
    public List<SerializablePair<TicketReservation, OrderSummary>> getPendingPayments(
            @PathVariable("eventName") String eventName, Principal principal) {
        Event event = eventManager.getSingleEvent(eventName, principal.getName());
        return ticketReservationManager.getPendingPayments(event).stream().map(SerializablePair::fromPair)
                .collect(toList());
    }

    @RequestMapping(value = "/events/{eventName}/pending-payments-count")
    public Integer getPendingPaymentsCount(@PathVariable("eventName") String eventName, Principal principal) {
        if (Optional.ofNullable(principal).map(Principal::getName).map(userManager::findUserByUsername)
                .map(userManager::isOwner).orElse(false)) {
            Event event = eventManager.getSingleEvent(eventName, principal.getName());
            return ticketReservationManager.getPendingPaymentsCount(event.getId());
        } else {
            return 0;
        }
    }

    @RequestMapping(value = "/events/{eventName}/pending-payments/{reservationId}/confirm", method = POST)
    public String confirmPayment(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, Principal principal, Model model,
            HttpServletRequest request) {
        ticketReservationManager.confirmOfflinePayment(loadEvent(eventName, principal), reservationId,
                principal.getName());
        ticketReservationManager.findById(reservationId).filter(TicketReservation::isDirectAssignmentRequested)
                .ifPresent(reservation -> ticketHelper.directTicketAssignment(eventName, reservationId,
                        reservation.getEmail(), reservation.getFullName(), reservation.getFirstName(),
                        reservation.getLastName(), reservation.getUserLanguage(), Optional.empty(), request,
                        model));
        return OK;
    }

    @RequestMapping(value = "/events/{eventName}/pending-payments/{reservationId}", method = DELETE)
    public String deletePendingPayment(@PathVariable("eventName") String eventName,
            @PathVariable("reservationId") String reservationId, Principal principal) {
        ticketReservationManager.deleteOfflinePayment(loadEvent(eventName, principal), reservationId, false);
        return OK;
    }

    @RequestMapping(value = "/events/{eventName}/pending-payments/bulk-confirmation", method = POST)
    public List<Triple<Boolean, String, String>> bulkConfirmation(@PathVariable("eventName") String eventName,
            Principal principal, @RequestBody UploadBase64FileModification file) throws IOException {

        try (InputStreamReader isr = new InputStreamReader(file.getInputStream());
                CSVReader reader = new CSVReader(isr)) {
            Event event = loadEvent(eventName, principal);
            return reader.readAll().stream().map(line -> {
                String reservationID = null;
                try {
                    Validate.isTrue(line.length >= 2);
                    reservationID = line[0];
                    ticketReservationManager.validateAndConfirmOfflinePayment(reservationID, event,
                            new BigDecimal(line[1]), principal.getName());
                    return Triple.of(Boolean.TRUE, reservationID, "");
                } catch (Exception e) {
                    return Triple.of(Boolean.FALSE, Optional.ofNullable(reservationID).orElse(""), e.getMessage());
                }
            }).collect(toList());
        }
    }

    @RequestMapping(value = "/events/{eventName}/categories/{categoryId}/tickets/{ticketId}/toggle-locking", method = PUT)
    public boolean toggleTicketLocking(@PathVariable("eventName") String eventName,
            @PathVariable("categoryId") int categoryId, @PathVariable("ticketId") int ticketId,
            Principal principal) {
        return eventManager.toggleTicketLocking(eventName, categoryId, ticketId, principal.getName());
    }

    @RequestMapping(value = "/events/{eventName}/languages", method = GET)
    public List<ContentLanguage> getAvailableLocales(@PathVariable("eventName") String eventName) {
        return i18nManager.getEventLanguages(eventName);
    }

    @RequestMapping(value = "/events/{eventName}/invoices/count", method = GET)
    public Integer countInvoicesForEvent(@PathVariable("eventName") String eventName, Principal principal) {
        return optionally(() -> eventManager.getSingleEvent(eventName, principal.getName()))
                .map(e -> ticketReservationManager.countInvoices(e.getId())).orElse(0);
    }

    @RequestMapping(value = "/events/{eventName}/all-invoices", method = GET)
    public void getAllInvoices(@PathVariable("eventName") String eventName, HttpServletResponse response,
            Principal principal) throws IOException {
        Event event = loadEvent(eventName, principal);

        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment; filename=" + eventName + "-invoices.zip");

        try (OutputStream os = response.getOutputStream(); ZipOutputStream zipOS = new ZipOutputStream(os)) {
            for (TicketReservation reservation : ticketReservationManager.findAllInvoices(event.getId())) {
                OrderSummary orderSummary = Json.fromJson(reservation.getInvoiceModel(), OrderSummary.class);
                Optional<String> vat = Optional.ofNullable(orderSummary.getVatPercentage());
                Map<String, Object> reservationModel = ticketReservationManager
                        .prepareModelForReservationEmail(event, reservation, vat, orderSummary);
                Optional<byte[]> pdf = TemplateProcessor.buildInvoicePdf(event, fileUploadManager,
                        new Locale(reservation.getUserLanguage()), templateManager, reservationModel);

                if (pdf.isPresent()) {
                    zipOS.putNextEntry(new ZipEntry("invoice-" + eventName + "-id-" + reservation.getId()
                            + "-invoice-nr-" + reservation.getInvoiceNumber() + ".pdf"));
                    StreamUtils.copy(pdf.get(), zipOS);
                }
            }
        }
    }

    @RequestMapping(value = "/events-all-languages", method = GET)
    public List<ContentLanguage> getAllLanguages() {
        return i18nManager.getAvailableLanguages();
    }

    @RequestMapping(value = "/events-supported-languages", method = GET)
    public List<ContentLanguage> getSupportedLanguages() {
        return i18nManager.getSupportedLanguages();
    }

    @RequestMapping(value = "/events/{eventName}/category/{categoryId}/ticket", method = GET)
    public PageAndContent<List<TicketWithStatistic>> getTicketsInCategory(
            @PathVariable("eventName") String eventName, @PathVariable("categoryId") int categoryId,
            @RequestParam(value = "page", required = false) Integer page,
            @RequestParam(value = "search", required = false) String search, Principal principal) {
        Event event = loadEvent(eventName, principal);
        return new PageAndContent<>(eventStatisticsManager.loadModifiedTickets(event.getId(), categoryId,
                page == null ? 0 : page, search),
                eventStatisticsManager.countModifiedTicket(event.getId(), categoryId, search));
    }

    @RequestMapping(value = "/events/{eventName}/ticket-sold-statistics", method = GET)
    public List<TicketSoldStatistic> getTicketSoldStatistics(@PathVariable("eventName") String eventName,
            @RequestParam(value = "from", required = false) String f,
            @RequestParam(value = "to", required = false) String t, Principal principal) throws ParseException {
        Event event = loadEvent(eventName, principal);
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        //TODO: cleanup
        Date from = DateUtils.truncate(f == null ? new Date(0) : format.parse(f), Calendar.HOUR);
        Date to = DateUtils
                .addMilliseconds(DateUtils.ceiling(t == null ? new Date() : format.parse(t), Calendar.DATE), -1);
        //

        return eventStatisticsManager.getTicketSoldStatistics(event.getId(), from, to);
    }

    private Event loadEvent(String eventName, Principal principal) {
        Optional<Event> singleEvent = optionally(() -> eventManager.getSingleEvent(eventName, principal.getName()));
        Validate.isTrue(singleEvent.isPresent(), "event not found");
        return singleEvent.get();
    }

}