alfio.manager.AdminReservationManager.java Source code

Java tutorial

Introduction

Here is the source code for alfio.manager.AdminReservationManager.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.manager;

import alfio.manager.support.DuplicateReferenceException;
import alfio.model.*;
import alfio.model.TicketReservation.TicketReservationStatus;
import alfio.model.decorator.TicketPriceContainer;
import alfio.model.modification.AdminReservationModification;
import alfio.model.modification.AdminReservationModification.Attendee;
import alfio.model.modification.AdminReservationModification.Category;
import alfio.model.modification.AdminReservationModification.TicketsInfo;
import alfio.model.modification.DateTimeModification;
import alfio.model.modification.TicketCategoryModification;
import alfio.model.result.ErrorCode;
import alfio.model.result.Result;
import alfio.model.result.Result.ResultStatus;
import alfio.model.transaction.PaymentProxy;
import alfio.model.user.Organization;
import alfio.repository.*;
import alfio.repository.user.UserRepository;
import alfio.util.Json;
import alfio.util.MonetaryUtil;
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.context.MessageSource;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;

import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static alfio.model.Audit.EntityType.TICKET;
import static alfio.model.Audit.EventType.CANCEL_TICKET;
import static alfio.model.modification.DateTimeModification.fromZonedDateTime;
import static alfio.util.EventUtil.generateEmptyTickets;
import static alfio.util.OptionalWrapper.optionally;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.*;
import static org.apache.commons.lang3.StringUtils.trimToNull;

@Component
@Log4j2
@RequiredArgsConstructor
public class AdminReservationManager {

    private final EventManager eventManager;
    private final TicketReservationManager ticketReservationManager;
    private final TicketCategoryRepository ticketCategoryRepository;
    private final TicketRepository ticketRepository;
    private final NamedParameterJdbcTemplate jdbc;
    private final SpecialPriceRepository specialPriceRepository;
    private final TicketReservationRepository ticketReservationRepository;
    private final EventRepository eventRepository;
    private final PlatformTransactionManager transactionManager;
    private final SpecialPriceTokenGenerator specialPriceTokenGenerator;
    private final TicketFieldRepository ticketFieldRepository;
    private final PaymentManager paymentManager;
    private final NotificationManager notificationManager;
    private final MessageSource messageSource;
    private final TemplateManager templateManager;
    private final AdditionalServiceItemRepository additionalServiceItemRepository;
    private final AuditingRepository auditingRepository;
    private final UserRepository userRepository;
    private final ExtensionManager extensionManager;

    //the following methods have an explicit transaction handling, therefore the @Transactional annotation is not helpful here
    public Result<Triple<TicketReservation, List<Ticket>, Event>> confirmReservation(String eventName,
            String reservationId, String username) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionTemplate template = new TransactionTemplate(transactionManager, definition);
        return template.execute(status -> {
            try {
                Result<Triple<TicketReservation, List<Ticket>, Event>> result = eventRepository
                        .findOptionalByShortName(eventName).flatMap(e -> optionally(() -> {
                            eventManager.checkOwnership(e, username, e.getOrganizationId());
                            return e;
                        }))
                        .map(event -> ticketReservationRepository.findOptionalReservationById(reservationId)
                                .filter(r -> r.getStatus() == TicketReservationStatus.PENDING
                                        || r.getStatus() == TicketReservationStatus.STUCK)
                                .map(r -> performConfirmation(reservationId, event, r))
                                .orElseGet(() -> Result.error(ErrorCode.ReservationError.UPDATE_FAILED)))
                        .orElseGet(() -> Result.error(ErrorCode.ReservationError.NOT_FOUND));
                if (!result.isSuccess()) {
                    log.debug("Reservation confirmation failed for eventName: {} reservationId: {}, username: {}",
                            eventName, reservationId, username);
                    status.setRollbackOnly();
                }
                return result;
            } catch (Exception e) {
                log.error("Error during confirmation of reservation eventName: {} reservationId: {}, username: {}",
                        eventName, reservationId, username);
                status.setRollbackOnly();
                return Result.error(singletonList(ErrorCode.custom("", e.getMessage())));
            }
        });
    }

    public Result<Boolean> updateReservation(String eventName, String reservationId,
            AdminReservationModification adminReservationModification, String username) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionTemplate template = new TransactionTemplate(transactionManager, definition);
        return template.execute(status -> {
            try {
                Result<Boolean> result = eventRepository.findOptionalByShortName(eventName)
                        .flatMap(e -> optionally(() -> {
                            eventManager.checkOwnership(e, username, e.getOrganizationId());
                            return e;
                        }))
                        .map(event -> ticketReservationRepository.findOptionalReservationById(reservationId)
                                .map(r -> performUpdate(reservationId, event, r, adminReservationModification))
                                .orElseGet(() -> Result.error(ErrorCode.ReservationError.UPDATE_FAILED)))
                        .orElseGet(() -> Result.error(ErrorCode.ReservationError.NOT_FOUND));
                if (!result.isSuccess()) {
                    log.debug(
                            "Application error detected eventName: {} reservationId: {}, username: {}, reservation: {}",
                            eventName, reservationId, username,
                            AdminReservationModification.summary(adminReservationModification));
                    status.setRollbackOnly();
                }
                return result;
            } catch (Exception e) {
                log.error(
                        "Error during update of reservation eventName: {} reservationId: {}, username: {}, reservation: {}",
                        eventName, reservationId, username,
                        AdminReservationModification.summary(adminReservationModification));
                status.setRollbackOnly();
                return Result.error(singletonList(ErrorCode.custom("", e.getMessage())));
            }
        });
    }

    public Result<Pair<TicketReservation, List<Ticket>>> createReservation(AdminReservationModification input,
            String eventName, String username) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionTemplate template = new TransactionTemplate(transactionManager, definition);
        return template.execute(status -> {
            try {
                Result<Pair<TicketReservation, List<Ticket>>> result = eventRepository
                        .findOptionalByShortNameForUpdate(eventName).map(e -> validateTickets(input, e))
                        .map(r -> r
                                .flatMap(p -> transactionalCreateReservation(p.getRight(), p.getLeft(), username)))
                        .orElse(Result.error(ErrorCode.EventError.NOT_FOUND));
                if (!result.isSuccess()) {
                    log.debug("Error during update of reservation eventName: {}, username: {}, reservation: {}",
                            eventName, username, AdminReservationModification.summary(input));
                    status.setRollbackOnly();
                }
                return result;
            } catch (Exception e) {
                log.error("Error during update of reservation eventName: {}, username: {}, reservation: {}",
                        eventName, username, AdminReservationModification.summary(input));
                status.setRollbackOnly();
                return Result.error(singletonList(ErrorCode.custom(
                        e instanceof DuplicateReferenceException ? "duplicate-reference" : "", e.getMessage())));
            }
        });
    }

    //end - the public / package protected methods below must be annotated with @Transactional

    @Transactional
    public Result<Boolean> notify(String eventName, String reservationId, AdminReservationModification arm,
            String username) {
        AdminReservationModification.Notification notification = arm.getNotification();
        return eventRepository.findOptionalByShortName(eventName).flatMap(e -> optionally(() -> {
            eventManager.checkOwnership(e, username, e.getOrganizationId());
            return e;
        }).flatMap(ev -> ticketReservationRepository.findOptionalReservationById(reservationId)
                .map(r -> Pair.of(e, r)))).map(pair -> {
                    Event event = pair.getLeft();
                    TicketReservation reservation = pair.getRight();
                    if (notification.isCustomer()) {
                        ticketReservationManager.sendConfirmationEmail(event, reservation,
                                Locale.forLanguageTag(reservation.getUserLanguage()));
                    }
                    if (notification.isAttendees()) {
                        ticketRepository.findTicketsInReservation(reservationId).stream()
                                .filter(Ticket::getAssigned).forEach(t -> {
                                    Locale locale = Locale.forLanguageTag(t.getUserLanguage());
                                    ticketReservationManager.sendTicketByEmail(t, locale, event,
                                            ticketReservationManager.getTicketEmailGenerator(event, reservation,
                                                    locale));
                                });
                    }
                    return Result.success(true);
                }).orElseGet(() -> Result.error(ErrorCode.EventError.NOT_FOUND));

    }

    private Result<Boolean> performUpdate(String reservationId, Event event, TicketReservation r,
            AdminReservationModification arm) {
        ticketReservationRepository.updateValidity(reservationId,
                Date.from(arm.getExpiration().toZonedDateTime(event.getZoneId()).toInstant()));
        if (arm.isUpdateContactData()) {
            AdminReservationModification.CustomerData customerData = arm.getCustomerData();
            ticketReservationRepository.updateTicketReservation(reservationId, r.getStatus().name(),
                    customerData.getEmailAddress(), customerData.getFullName(), customerData.getFirstName(),
                    customerData.getLastName(), customerData.getUserLanguage(), customerData.getBillingAddress(),
                    r.getConfirmationTimestamp(),
                    Optional.ofNullable(r.getPaymentMethod()).map(PaymentProxy::name).orElse(null));
        }
        arm.getTicketsInfo().stream().filter(TicketsInfo::isUpdateAttendees)
                .flatMap(ti -> ti.getAttendees().stream())
                .forEach(a -> ticketRepository.updateTicketOwnerById(a.getTicketId(),
                        trimToNull(a.getEmailAddress()), trimToNull(a.getFullName()), trimToNull(a.getFirstName()),
                        trimToNull(a.getLastName())));
        return Result.success(true);
    }

    @Transactional
    public Result<Triple<TicketReservation, List<Ticket>, Event>> loadReservation(String eventName,
            String reservationId, String username) {
        return eventRepository.findOptionalByShortName(eventName).flatMap(e -> optionally(() -> {
            eventManager.checkOwnership(e, username, e.getOrganizationId());
            return e;
        })).map(r -> loadReservation(reservationId))
                .orElseGet(() -> Result.error(ErrorCode.ReservationError.NOT_FOUND));
    }

    private Result<Triple<TicketReservation, List<Ticket>, Event>> loadReservation(String reservationId) {
        return ticketReservationRepository.findOptionalReservationById(reservationId)
                .map(r -> Triple.of(r, ticketRepository.findTicketsInReservation(reservationId),
                        eventRepository.findByReservationId(reservationId)))
                .map(Result::success).orElseGet(() -> Result.error(ErrorCode.ReservationError.NOT_FOUND));
    }

    private Result<Triple<TicketReservation, List<Ticket>, Event>> performConfirmation(String reservationId,
            Event event, TicketReservation original) {
        try {
            ticketReservationManager.completeReservation(event.getId(), reservationId, original.getEmail(),
                    new CustomerName(original.getFullName(), original.getFirstName(), original.getLastName(),
                            event),
                    Locale.forLanguageTag(original.getUserLanguage()), original.getBillingAddress(),
                    Optional.empty(), PaymentProxy.ADMIN);
            return loadReservation(reservationId);
        } catch (Exception e) {
            return Result.error(ErrorCode.ReservationError.UPDATE_FAILED);
        }
    }

    @Transactional
    Result<Pair<Event, AdminReservationModification>> validateTickets(AdminReservationModification input,
            Event event) {
        Set<String> keys = input.getTicketsInfo().stream().flatMap(ti -> ti.getAttendees().stream())
                .flatMap(a -> a.getAdditionalInfo().keySet().stream()).map(String::toLowerCase).distinct()
                .collect(toSet());

        if (keys.size() == 0) {
            return Result.success(Pair.of(event, input));
        }

        List<String> existing = ticketFieldRepository.getExistingFields(event.getId(), keys);
        if (existing.size() == keys.size()) {
            return Result.success(Pair.of(event, input));
        }

        return Result.error(keys.stream().filter(k -> !existing.contains(k))
                .map(k -> ErrorCode.custom("error.notfound." + k, k + " not found")).collect(toList()));
    }

    private Result<Pair<TicketReservation, List<Ticket>>> transactionalCreateReservation(
            AdminReservationModification input, Event event, String username) {
        return optionally(() -> {
            eventManager.checkOwnership(event, username, event.getOrganizationId());
            return event;
        }).map(e -> processReservation(input, username, e))
                .orElseGet(() -> Result.error(singletonList(ErrorCode.EventError.NOT_FOUND)));
    }

    private Result<Pair<TicketReservation, List<Ticket>>> processReservation(AdminReservationModification input,
            String username, Event event) {
        return input.getTicketsInfo().stream().map(ti -> checkCategoryCapacity(ti, event, input, username))
                .reduce((r1, r2) -> reduceResults(r1, r2, this::joinData))
                .map(r -> createReservation(r, event, input))
                .orElseGet(() -> Result.error(singletonList(ErrorCode.custom("", "something went wrong..."))));
    }

    private List<TicketsInfo> joinData(List<TicketsInfo> t1, List<TicketsInfo> t2) {
        List<TicketsInfo> join = new ArrayList<>();
        join.addAll(t1);
        join.addAll(t2);
        return join;
    }

    private Result<Pair<TicketReservation, List<Ticket>>> createReservation(Result<List<TicketsInfo>> input,
            Event event, AdminReservationModification arm) {
        final TicketsInfo empty = new TicketsInfo(null, null, false, false);
        return input.flatMap(t -> {
            String reservationId = UUID.randomUUID().toString();
            String specialPriceSessionId = UUID.randomUUID().toString();
            Date validity = Date.from(arm.getExpiration().toZonedDateTime(event.getZoneId()).toInstant());
            ticketReservationRepository.createNewReservation(reservationId, validity, null, arm.getLanguage(),
                    event.getId(), event.getVat(), event.isVatIncluded());
            AdminReservationModification.CustomerData customerData = arm.getCustomerData();
            ticketReservationRepository.updateTicketReservation(reservationId,
                    TicketReservationStatus.PENDING.name(), customerData.getEmailAddress(),
                    customerData.getFullName(), customerData.getFirstName(), customerData.getLastName(),
                    arm.getLanguage(), null, null, null);

            Result<List<Ticket>> result = flattenTicketsInfo(event, empty, t)
                    .map(pair -> reserveForTicketsInfo(event, arm, reservationId, specialPriceSessionId, pair))
                    .reduce(this::reduceReservationResults)
                    .orElseGet(() -> Result.error(ErrorCode.custom("", "unknown error")));

            OrderSummary orderSummary = ticketReservationManager.orderSummaryForReservationId(reservationId, event,
                    Locale.forLanguageTag(arm.getLanguage()));
            ticketReservationRepository.addReservationInvoiceOrReceiptModel(reservationId,
                    Json.toJson(orderSummary));

            return result
                    .map(list -> Pair.of(ticketReservationRepository.findReservationById(reservationId), list));
        });
    }

    private Result<List<Ticket>> reserveForTicketsInfo(Event event, AdminReservationModification arm,
            String reservationId, String specialPriceSessionId, Pair<TicketCategory, TicketsInfo> pair) {
        TicketCategory category = pair.getLeft();
        TicketsInfo ticketsInfo = pair.getRight();
        int categoryId = category.getId();
        List<Attendee> attendees = ticketsInfo.getAttendees();
        List<Integer> reservedForUpdate = ticketReservationManager.reserveTickets(event.getId(), categoryId,
                attendees.size(), singletonList(Ticket.TicketStatus.FREE));
        if (reservedForUpdate.size() != attendees.size()) {
            return Result.error(ErrorCode.CategoryError.NOT_ENOUGH_SEATS);
        }
        ticketRepository.reserveTickets(reservationId, reservedForUpdate, categoryId, arm.getLanguage(),
                category.getSrcPriceCts());
        Ticket ticket = ticketRepository.findById(reservedForUpdate.get(0), categoryId);
        TicketPriceContainer priceContainer = TicketPriceContainer.from(ticket, null, event, null);
        ticketRepository.updateTicketPrice(reservedForUpdate, categoryId, event.getId(), category.getSrcPriceCts(),
                MonetaryUtil.unitToCents(priceContainer.getFinalPrice()),
                MonetaryUtil.unitToCents(priceContainer.getVAT()),
                MonetaryUtil.unitToCents(priceContainer.getAppliedDiscount()));
        List<SpecialPrice> codes = category.isAccessRestricted()
                ? bindSpecialPriceTokens(specialPriceSessionId, categoryId, attendees)
                : Collections.emptyList();
        assignTickets(event, attendees, categoryId, reservedForUpdate, codes, reservationId, arm.getLanguage(),
                category.getSrcPriceCts());
        List<Ticket> tickets = reservedForUpdate.stream().map(id -> ticketRepository.findById(id, categoryId))
                .collect(toList());
        return Result.success(tickets);
    }

    private Result<List<Ticket>> reduceReservationResults(Result<List<Ticket>> r1, Result<List<Ticket>> r2) {
        return reduceResults(r1, r2, this::joinCreateReservationResults);
    }

    private List<Ticket> joinCreateReservationResults(List<Ticket> r1, List<Ticket> r2) {
        List<Ticket> data = new ArrayList<>(r1);
        data.addAll(r2);
        return data;
    }

    private <T> Result<T> reduceResults(Result<T> r1, Result<T> r2, BiFunction<T, T, T> processData) {
        boolean successful = r1.isSuccess() && r2.isSuccess();
        ResultStatus global = r1.isSuccess() ? r2.getStatus() : r1.getStatus();
        List<ErrorCode> errors = new ArrayList<>();
        if (!successful) {
            errors.addAll(r1.getErrors());
            errors.addAll(r2.getErrors());
            return new Result<>(global, null, errors);
        } else {
            return new Result<>(global, processData.apply(r1.getData(), r2.getData()), errors);
        }
    }

    private Stream<Pair<TicketCategory, TicketsInfo>> flattenTicketsInfo(Event event, TicketsInfo empty,
            List<TicketsInfo> t) {
        return t.stream().collect(groupingBy(ti -> ti.getCategory().getExistingCategoryId())).entrySet().stream()
                .map(entry -> {
                    TicketsInfo ticketsInfo = entry.getValue().stream().reduce((ti1, ti2) -> {
                        List<Attendee> attendees = new ArrayList<>(ti1.getAttendees());
                        attendees.addAll(ti2.getAttendees());
                        return new TicketsInfo(ti1.getCategory(), attendees,
                                ti1.isAddSeatsIfNotAvailable() && ti2.isAddSeatsIfNotAvailable(),
                                ti1.isUpdateAttendees() && ti2.isUpdateAttendees());
                    }).orElse(empty);
                    return Pair.of(ticketCategoryRepository.getByIdAndActive(entry.getKey(), event.getId()),
                            ticketsInfo);
                });
    }

    private List<SpecialPrice> bindSpecialPriceTokens(String specialPriceSessionId, int categoryId,
            List<Attendee> attendees) {
        specialPriceTokenGenerator.generatePendingCodesForCategory(categoryId);
        List<SpecialPrice> codes = specialPriceRepository.findActiveNotAssignedByCategoryId(categoryId).stream()
                .limit(attendees.size()).collect(toList());
        codes.forEach(c -> specialPriceRepository.updateStatus(c.getId(), SpecialPrice.Status.PENDING.toString(),
                specialPriceSessionId));
        return codes;
    }

    private void assignTickets(Event event, List<Attendee> attendees, int categoryId,
            List<Integer> reservedForUpdate, List<SpecialPrice> codes, String reservationId, String userLanguage,
            int srcPriceCts) {

        Optional<Iterator<SpecialPrice>> specialPriceIterator = Optional.of(codes).filter(c -> !c.isEmpty())
                .map(Collection::iterator);
        for (int i = 0; i < reservedForUpdate.size(); i++) {
            Attendee attendee = attendees.get(i);
            if (!attendee.isEmpty()) {
                Integer ticketId = reservedForUpdate.get(i);
                ticketRepository.updateTicketOwnerById(ticketId, attendee.getEmailAddress(), attendee.getFullName(),
                        attendee.getFirstName(), attendee.getLastName());
                if (StringUtils.isNotBlank(attendee.getReference()) || attendee.isReassignmentForbidden()) {
                    updateExtRefAndLocking(categoryId, attendee, ticketId);
                }
                if (!attendee.getAdditionalInfo().isEmpty()) {
                    ticketFieldRepository.updateOrInsert(attendee.getAdditionalInfo(), ticketId, event.getId());
                }
                specialPriceIterator.map(Iterator::next).ifPresent(code -> ticketRepository
                        .reserveTicket(reservationId, ticketId, code.getId(), userLanguage, srcPriceCts));
            }
        }
    }

    private void updateExtRefAndLocking(int categoryId, Attendee attendee, Integer ticketId) {
        try {
            ticketRepository.updateExternalReferenceAndLocking(ticketId, categoryId,
                    StringUtils.trimToNull(attendee.getReference()), attendee.isReassignmentForbidden());
        } catch (DataIntegrityViolationException ex) {
            log.warn("Duplicate found for external reference: " + attendee.getReference() + " and ticketID: "
                    + ticketId);
            throw new DuplicateReferenceException("Duplicated Reference: " + attendee.getReference(), ex);
        }
    }

    private Result<List<TicketsInfo>> checkCategoryCapacity(TicketsInfo ti, Event event,
            AdminReservationModification reservation, String username) {
        Result<TicketCategory> ticketCategoryResult = ti.getCategory().isExisting()
                ? checkExistingCategory(ti, event, username)
                : createCategory(ti, event, reservation, username);
        return ticketCategoryResult.map(tc -> Collections
                .singletonList(new TicketsInfo(new Category(tc.getId(), tc.getName(), tc.getPrice()),
                        ti.getAttendees(), ti.isAddSeatsIfNotAvailable(), ti.isUpdateAttendees())));
    }

    private Result<TicketCategory> createCategory(TicketsInfo ti, Event event,
            AdminReservationModification reservation, String username) {
        Category category = ti.getCategory();
        List<Attendee> attendees = ti.getAttendees();
        DateTimeModification inception = fromZonedDateTime(ZonedDateTime.now(event.getZoneId()));

        int tickets = attendees.size();
        TicketCategoryModification tcm = new TicketCategoryModification(category.getExistingCategoryId(),
                category.getName(), tickets, inception, reservation.getExpiration(), Collections.emptyMap(),
                category.getPrice(), true, "", true, null, null, null, null, null);
        int notAllocated = getNotAllocatedTickets(event);
        int missingTickets = Math.max(tickets - notAllocated, 0);
        Event modified = increaseSeatsIfNeeded(ti, event, missingTickets, event);
        return eventManager.insertCategory(modified, tcm, username)
                .map(id -> ticketCategoryRepository.getByIdAndActive(id, event.getId()));
    }

    private Event increaseSeatsIfNeeded(TicketsInfo ti, Event event, int missingTickets, Event modified) {
        if (missingTickets > 0 && ti.isAddSeatsIfNotAvailable()) {
            createMissingTickets(event, missingTickets);
            //update seats and reload event
            log.debug("adding {} extra seats to the event", missingTickets);
            eventRepository.updateAvailableSeats(event.getId(),
                    eventRepository.countExistingTickets(event.getId()) + missingTickets);
            modified = eventRepository.findById(event.getId());
        }
        return modified;
    }

    private int getNotAllocatedTickets(Event event) {
        return ticketRepository.countFreeTicketsForUnbounded(event.getId());
    }

    private Result<TicketCategory> checkExistingCategory(TicketsInfo ti, Event event, String username) {
        Category category = ti.getCategory();
        List<Attendee> attendees = ti.getAttendees();
        int tickets = attendees.size();
        int eventId = event.getId();
        TicketCategory existing = ticketCategoryRepository.getByIdAndActive(category.getExistingCategoryId(),
                eventId);
        int existingCategoryId = existing.getId();
        int freeTicketsInCategory = ticketRepository.countFreeTickets(eventId, existingCategoryId);
        int notAllocated = getNotAllocatedTickets(event);
        int missingTickets = Math.max(tickets - (freeTicketsInCategory + notAllocated), 0);
        Event modified = increaseSeatsIfNeeded(ti, event, missingTickets, event);
        if (freeTicketsInCategory < tickets && existing.isBounded()) {
            int maxTickets = existing.getMaxTickets() + (tickets - freeTicketsInCategory);
            TicketCategoryModification tcm = new TicketCategoryModification(existingCategoryId, existing.getName(),
                    maxTickets, fromZonedDateTime(existing.getInception(modified.getZoneId())),
                    fromZonedDateTime(existing.getExpiration(event.getZoneId())), Collections.emptyMap(),
                    existing.getPrice(), existing.isAccessRestricted(), "", true, existing.getCode(),
                    fromZonedDateTime(existing.getValidCheckInFrom(modified.getZoneId())),
                    fromZonedDateTime(existing.getValidCheckInTo(modified.getZoneId())),
                    fromZonedDateTime(existing.getTicketValidityStart(modified.getZoneId())),
                    fromZonedDateTime(existing.getTicketValidityEnd(modified.getZoneId())));
            return eventManager.updateCategory(existingCategoryId, modified, tcm, username, true);
        }
        return Result.success(existing);
    }

    private void createMissingTickets(Event event, int tickets) {
        final MapSqlParameterSource[] params = generateEmptyTickets(event,
                Date.from(ZonedDateTime.now(event.getZoneId()).toInstant()), tickets, Ticket.TicketStatus.FREE)
                        .toArray(MapSqlParameterSource[]::new);
        jdbc.batchUpdate(ticketRepository.bulkTicketInitialization(), params);
    }

    @Transactional
    public void removeTickets(String eventName, String reservationId, List<Integer> ticketIds,
            List<Integer> toRefund, boolean notify, String username) {
        loadReservation(eventName, reservationId, username).ifSuccess((res) -> {
            Event e = res.getRight();
            TicketReservation reservation = res.getLeft();
            List<Ticket> tickets = res.getMiddle();
            Map<Integer, Ticket> ticketsById = tickets.stream()
                    .collect(Collectors.toMap(Ticket::getId, Function.identity()));
            Set<Integer> ticketIdsInReservation = tickets.stream().map(Ticket::getId).collect(toSet());
            // ensure that all the tickets ids are present in tickets
            Assert.isTrue(ticketIdsInReservation.containsAll(ticketIds),
                    "Some ticket ids are not contained in the reservation");
            Assert.isTrue(ticketIdsInReservation.containsAll(toRefund),
                    "Some ticket ids to refund are not contained in the reservation");
            //

            removeTicketsFromReservation(reservationId, e, ticketIds, notify, username, false);
            //

            handleTicketsRefund(toRefund, e, reservation, ticketsById, username);

            if (tickets.size() - ticketIds.size() <= 0) {
                markAsCancelled(reservation);
                additionalServiceItemRepository.updateItemsStatusWithReservationUUID(reservation.getId(),
                        AdditionalServiceItem.AdditionalServiceItemStatus.CANCELLED);
            }
        });
    }

    @Transactional
    public Result<List<Audit>> getAudit(String eventName, String reservationId, String username) {
        return loadReservation(eventName, reservationId, username)
                .map((res) -> auditingRepository.findAllForReservation(reservationId));
    }

    @Transactional
    public Result<TransactionAndPaymentInfo> getPaymentInfo(String eventName, String reservationId,
            String username) {
        return loadReservation(eventName, reservationId, username)
                .map((res) -> paymentManager.getInfo(res.getLeft(), res.getRight()));
    }

    @Transactional
    public void removeReservation(String eventName, String reservationId, boolean refund, boolean notify,
            String username) {
        loadReservation(eventName, reservationId, username).ifSuccess((res) -> {
            Event e = res.getRight();
            TicketReservation reservation = res.getLeft();
            List<Ticket> tickets = res.getMiddle();

            removeTicketsFromReservation(reservationId, e, tickets.stream().map(Ticket::getId).collect(toList()),
                    notify, username, true);

            additionalServiceItemRepository.updateItemsStatusWithReservationUUID(reservation.getId(),
                    AdditionalServiceItem.AdditionalServiceItemStatus.CANCELLED);

            if (refund && reservation.getPaymentMethod() != null
                    && reservation.getPaymentMethod().isSupportRefund()) {
                //fully refund
                paymentManager.refund(reservation, e, Optional.empty(), username);
            }

            markAsCancelled(reservation);
        });
    }

    @Transactional
    public Result<Boolean> refund(String eventName, String reservationId, BigDecimal refundAmount,
            String username) {
        return loadReservation(eventName, reservationId, username).map((res) -> {
            Event e = res.getRight();
            TicketReservation reservation = res.getLeft();
            return reservation.getPaymentMethod() != null && reservation.getPaymentMethod().isSupportRefund()
                    && paymentManager.refund(reservation, e, Optional.of(MonetaryUtil.unitToCents(refundAmount)),
                            username);
        });
    }

    private void removeTicketsFromReservation(String reservationId, Event event, List<Integer> ticketIds,
            boolean notify, String username, boolean removeReservation) {
        if (notify && !ticketIds.isEmpty()) {
            Organization o = eventManager.loadOrganizer(event, username);
            ticketRepository.findByIds(ticketIds).forEach(t -> {
                if (StringUtils.isNotBlank(t.getEmail())) {
                    sendTicketHasBeenRemoved(event, o, t);
                }
            });
        }

        Integer userId = userRepository.findIdByUserName(username).orElse(null);
        Date date = new Date();

        ticketIds.forEach(id -> auditingRepository.insert(reservationId, userId, event.getId(), CANCEL_TICKET, date,
                TICKET, id.toString()));

        ticketRepository.resetCategoryIdForUnboundedCategoriesWithTicketIds(ticketIds);
        ticketFieldRepository.deleteAllValuesForTicketIds(ticketIds);
        MapSqlParameterSource[] args = ticketIds.stream()
                .map(id -> new MapSqlParameterSource("ticketId", id).addValue("reservationId", reservationId)
                        .addValue("eventId", event.getId()).addValue("newUuid", UUID.randomUUID().toString()))
                .toArray(MapSqlParameterSource[]::new);
        jdbc.batchUpdate(ticketRepository.batchReleaseTickets(), args);
        if (!removeReservation) {
            extensionManager.handleTicketCancelledForEvent(event, ticketRepository.findUUIDs(ticketIds));
        } else {
            extensionManager.handleReservationsCancelledForEvent(event,
                    ticketRepository.findReservationIds(ticketIds));
        }
    }

    private void sendTicketHasBeenRemoved(Event event, Organization organization, Ticket ticket) {
        Map<String, Object> model = TemplateResource.buildModelForTicketHasBeenCancelled(organization, event,
                ticket);
        Locale locale = Locale.forLanguageTag(Optional.ofNullable(ticket.getUserLanguage()).orElse("en"));
        notificationManager.sendSimpleEmail(event, ticket.getEmail(),
                messageSource.getMessage("email-ticket-released.subject", new Object[] { event.getDisplayName() },
                        locale),
                () -> templateManager.renderTemplate(event, TemplateResource.TICKET_HAS_BEEN_CANCELLED, model,
                        locale));
    }

    private void markAsCancelled(TicketReservation ticketReservation) {
        ticketReservationRepository.updateReservationStatus(ticketReservation.getId(),
                TicketReservationStatus.CANCELLED.toString());
    }

    private void handleTicketsRefund(List<Integer> toRefund, Event e, TicketReservation reservation,
            Map<Integer, Ticket> ticketsById, String username) {
        if (reservation.getPaymentMethod() == null || !reservation.getPaymentMethod().isSupportRefund()) {
            return;
        }
        // refund each selected ticket
        for (Integer toRefundId : toRefund) {
            int toBeRefunded = ticketsById.get(toRefundId).getFinalPriceCts();
            if (toBeRefunded > 0) {
                paymentManager.refund(reservation, e, Optional.of(toBeRefunded), username);
            }
        }
        //
    }
}