Java tutorial
/** * 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.config.Initializer; import alfio.manager.plugin.PluginManager; import alfio.manager.support.CategoryEvaluator; import alfio.manager.system.ConfigurationManager; import alfio.manager.user.UserManager; import alfio.model.*; import alfio.model.PromoCodeDiscount.DiscountType; import alfio.model.Ticket.TicketStatus; import alfio.model.TicketFieldConfiguration.Context; import alfio.model.modification.EventModification; import alfio.model.modification.EventModification.AdditionalField; import alfio.model.modification.PromoCodeDiscountWithFormattedTime; import alfio.model.modification.TicketCategoryModification; import alfio.model.modification.TicketFieldDescriptionModification; import alfio.model.result.ErrorCode; import alfio.model.result.Result; import alfio.model.system.Configuration; import alfio.model.system.ConfigurationKeys; import alfio.model.transaction.PaymentProxy; import alfio.model.user.Organization; import alfio.repository.*; import alfio.repository.user.OrganizationRepository; import alfio.util.Json; import alfio.util.MonetaryUtil; import ch.digitalfondue.npjt.AffectedRowCountAndKey; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; import org.flywaydb.core.Flyway; import org.springframework.core.env.Environment; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import java.math.BigDecimal; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import static alfio.model.modification.DateTimeModification.toZonedDateTime; import static alfio.util.EventUtil.*; import static alfio.util.OptionalWrapper.optionally; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @Component @Transactional @Log4j2 @AllArgsConstructor public class EventManager { private static final Predicate<TicketCategory> IS_CATEGORY_BOUNDED = TicketCategory::isBounded; private final UserManager userManager; private final EventRepository eventRepository; private final EventDescriptionRepository eventDescriptionRepository; private final TicketCategoryRepository ticketCategoryRepository; private final TicketCategoryDescriptionRepository ticketCategoryDescriptionRepository; private final TicketRepository ticketRepository; private final SpecialPriceRepository specialPriceRepository; private final PromoCodeDiscountRepository promoCodeRepository; private final NamedParameterJdbcTemplate jdbc; private final ConfigurationManager configurationManager; private final PluginManager pluginManager; private final TicketFieldRepository ticketFieldRepository; private final EventDeleterRepository eventDeleterRepository; private final AdditionalServiceRepository additionalServiceRepository; private final AdditionalServiceTextRepository additionalServiceTextRepository; private final Flyway flyway; private final Environment environment; private final OrganizationRepository organizationRepository; private final AuditingRepository auditingRepository; private final ExtensionManager extensionManager; public Event getSingleEvent(String eventName, String username) { return getOptionalByName(eventName, username).orElseThrow(IllegalStateException::new); } public Optional<Event> getOptionalByName(String eventName, String username) { return eventRepository.findOptionalByShortName(eventName) .filter(checkOwnership(username, organizationRepository)); } public Event getSingleEventById(int eventId, String username) { return optionally(() -> eventRepository.findById(eventId)) .filter(checkOwnership(username, organizationRepository)).orElseThrow(IllegalStateException::new); } public void checkOwnership(Event event, String username, int organizationId) { Validate.isTrue(organizationId == event.getOrganizationId(), "invalid organizationId"); Validate.isTrue(checkOwnership(username, organizationRepository).test(event), "User is not authorized"); } public static Predicate<Event> checkOwnership(String username, OrganizationRepository organizationRepository) { return event -> organizationRepository.findOrganizationForUser(username, event.getOrganizationId()) .isPresent(); } public List<TicketCategory> loadTicketCategories(Event event) { return ticketCategoryRepository.findByEventId(event.getId()); } public Organization loadOrganizer(Event event, String username) { return userManager.findOrganizationById(event.getOrganizationId(), username); } /** * Internal method used by automated jobs * @return */ Organization loadOrganizerUsingSystemPrincipal(Event event) { return loadOrganizer(event, UserManager.ADMIN_USERNAME); } public Event findEventByTicketCategory(TicketCategory ticketCategory) { return eventRepository.findById(ticketCategory.getEventId()); } public Event findEventByAdditionalService(AdditionalService additionalService) { return eventRepository.findById(additionalService.getEventId()); } public void createEvent(EventModification em) { int eventId = insertEvent(em); Event event = eventRepository.findById(eventId); createOrUpdateEventDescription(eventId, em); createAllAdditionalServices(eventId, em.getAdditionalServices(), event.getZoneId()); createAdditionalFields(event, em); createCategoriesForEvent(em, event); createAllTicketsForEvent(event, em); initPlugins(event); extensionManager.handleEventCreation(event); } public void toggleActiveFlag(int id, String username, boolean activate) { Event event = eventRepository.findById(id); checkOwnership(event, username, event.getOrganizationId()); if (environment.acceptsProfiles(Initializer.PROFILE_DEMO)) { throw new IllegalStateException("demo mode"); } Event.Status status = activate ? Event.Status.PUBLIC : Event.Status.DRAFT; eventRepository.updateEventStatus(id, status); extensionManager.handleEventStatusChange(event, status); } private void createAllAdditionalServices(int eventId, List<EventModification.AdditionalService> additionalServices, ZoneId zoneId) { Optional.ofNullable(additionalServices).ifPresent(list -> list.forEach(as -> { AffectedRowCountAndKey<Integer> service = additionalServiceRepository.insert(eventId, Optional.ofNullable(as.getPrice()).map(MonetaryUtil::unitToCents).orElse(0), as.isFixPrice(), as.getOrdinal(), as.getAvailableQuantity(), as.getMaxQtyPerOrder(), as.getInception().toZonedDateTime(zoneId), as.getExpiration().toZonedDateTime(zoneId), as.getVat(), as.getVatType(), as.getType(), as.getSupplementPolicy()); as.getTitle().forEach(insertAdditionalServiceDescription(service.getKey())); as.getDescription().forEach(insertAdditionalServiceDescription(service.getKey())); })); } private Consumer<EventModification.AdditionalServiceText> insertAdditionalServiceDescription(int serviceId) { return t -> additionalServiceTextRepository.insert(serviceId, t.getLocale(), t.getType(), t.getValue()); } private void initPlugins(Event event) { pluginManager.installPlugins(event); } private void createOrUpdateEventDescription(int eventId, EventModification em) { eventDescriptionRepository.delete(eventId, EventDescription.EventDescriptionType.DESCRIPTION); Set<String> validLocales = ContentLanguage.findAllFor(em.getLocales()).stream() .map(ContentLanguage::getLanguage).collect(Collectors.toSet()); Optional.ofNullable(em.getDescription()) .ifPresent(descriptions -> descriptions.forEach((locale, description) -> { if (validLocales.contains(locale)) { eventDescriptionRepository.insert(eventId, locale, EventDescription.EventDescriptionType.DESCRIPTION, description); } })); } private void createAdditionalFields(Event event, EventModification em) { if (!CollectionUtils.isEmpty(em.getTicketFields())) { em.getTicketFields().forEach(f -> { insertAdditionalField(event, f, f.getOrder()); }); } } private static String toSerializedRestrictedValues(EventModification.WithRestrictedValues f) { List<String> restrictedValues = Optional.ofNullable(f.getRestrictedValuesAsString()) .orElseGet(Collections::emptyList); return "select".equals(f.getType()) ? Json.GSON.toJson(restrictedValues) : null; } private void insertAdditionalField(Event event, AdditionalField f, int order) { String serializedRestrictedValues = toSerializedRestrictedValues(f); Optional<EventModification.AdditionalService> linkedAdditionalService = Optional .ofNullable(f.getLinkedAdditionalService()); Integer additionalServiceId = linkedAdditionalService .map(as -> Optional.ofNullable(as.getId()).orElseGet(() -> findAdditionalService(event, as))) .orElse(-1); Context context = linkedAdditionalService.isPresent() ? Context.ADDITIONAL_SERVICE : Context.ATTENDEE; int configurationId = ticketFieldRepository .insertConfiguration(event.getId(), f.getName(), order, f.getType(), serializedRestrictedValues, f.getMaxLength(), f.getMinLength(), f.isRequired(), context, additionalServiceId) .getKey(); f.getDescription().forEach((locale, value) -> ticketFieldRepository.insertDescription(configurationId, locale, Json.GSON.toJson(value))); } public void updateAdditionalField(int id, EventModification.UpdateAdditionalField f) { String serializedRestrictedValues = toSerializedRestrictedValues(f); ticketFieldRepository.updateRequiredAndRestrictedValues(id, f.isRequired(), serializedRestrictedValues); f.getDescription().forEach((locale, value) -> { String val = Json.GSON.toJson(value.getDescription()); if (0 == ticketFieldRepository.updateDescription(id, locale, val)) { ticketFieldRepository.insertDescription(id, locale, val); } }); } private Integer findAdditionalService(Event event, EventModification.AdditionalService as) { ZoneId utc = ZoneId.of("UTC"); int eventId = event.getId(); String checksum = new AdditionalService(0, eventId, as.isFixPrice(), as.getOrdinal(), as.getAvailableQuantity(), as.getMaxQtyPerOrder(), as.getInception().toZonedDateTime(event.getZoneId()).withZoneSameInstant(utc), as.getExpiration().toZonedDateTime(event.getZoneId()).withZoneSameInstant(utc), as.getVat(), as.getVatType(), Optional.ofNullable(as.getPrice()).map(MonetaryUtil::unitToCents).orElse(0), as.getType(), as.getSupplementPolicy()).getChecksum(); return additionalServiceRepository.loadAllForEvent(eventId).stream() .filter(as1 -> as1.getChecksum().equals(checksum)).findFirst().map(AdditionalService::getId) .orElse(null); } public void updateEventHeader(Event original, EventModification em, String username) { checkOwnership(original, username, em.getOrganizationId()); int eventId = original.getId(); final ZoneId zoneId = ZoneId.of(em.getZoneId()); final ZonedDateTime begin = em.getBegin().toZonedDateTime(zoneId); final ZonedDateTime end = em.getEnd().toZonedDateTime(zoneId); eventRepository.updateHeader(eventId, em.getDisplayName(), em.getWebsiteUrl(), em.getExternalUrl(), em.getTermsAndConditionsUrl(), em.getImageUrl(), em.getFileBlobId(), em.getLocation(), em.getLatitude(), em.getLongitude(), begin, end, em.getZoneId(), em.getOrganizationId(), em.getLocales()); createOrUpdateEventDescription(eventId, em); if (!original.getBegin().equals(begin) || !original.getEnd().equals(end)) { fixOutOfRangeCategories(em, username, zoneId, end); } } public void updateEventPrices(Event original, EventModification em, String username) { checkOwnership(original, username, em.getOrganizationId()); int eventId = original.getId(); int seatsDifference = em.getAvailableSeats() - eventRepository.countExistingTickets(original.getId()); if (seatsDifference < 0) { int allocatedSeats = ticketCategoryRepository.findByEventId(original.getId()).stream() .filter(TicketCategory::isBounded).mapToInt(TicketCategory::getMaxTickets).sum(); if (em.getAvailableSeats() < allocatedSeats) { throw new IllegalArgumentException(format( "cannot reduce max tickets to %d. There are already %d tickets allocated. Try updating categories first.", em.getAvailableSeats(), allocatedSeats)); } } String paymentProxies = collectPaymentProxies(em); BigDecimal vat = em.isFreeOfCharge() ? BigDecimal.ZERO : em.getVatPercentage(); eventRepository.updatePrices(em.getCurrency(), em.getAvailableSeats(), em.isVatIncluded(), vat, paymentProxies, eventId, em.getVatStatus(), em.getPriceInCents()); if (seatsDifference != 0) { Event modified = eventRepository.findById(eventId); if (seatsDifference > 0) { final MapSqlParameterSource[] params = generateEmptyTickets(modified, Date.from(ZonedDateTime.now(modified.getZoneId()).toInstant()), seatsDifference, TicketStatus.RELEASED).toArray(MapSqlParameterSource[]::new); jdbc.batchUpdate(ticketRepository.bulkTicketInitialization(), params); } else { List<Integer> ids = ticketRepository.selectNotAllocatedTicketsForUpdate(eventId, Math.abs(seatsDifference), singletonList(TicketStatus.FREE.name())); Validate.isTrue(ids.size() == Math.abs(seatsDifference), "cannot lock enough tickets for deletion."); int invalidatedTickets = ticketRepository.invalidateTickets(ids); Validate.isTrue(ids.size() == invalidatedTickets, String.format( "error during ticket invalidation: expected %d, got %d", ids.size(), invalidatedTickets)); } } } /** * This method has been modified to use the new Result<T> mechanism. * It will be replaced by {@link #insertCategory(Event, TicketCategoryModification, String)} in the next releases */ public void insertCategory(int eventId, TicketCategoryModification tcm, String username) { final Event event = eventRepository.findById(eventId); Result<Integer> result = insertCategory(event, tcm, username); failIfError(result); } public Result<Integer> insertCategory(Event event, TicketCategoryModification tcm, String username) { return optionally(() -> { checkOwnership(event, username, event.getOrganizationId()); return true; }).map(b -> { int eventId = event.getId(); int sum = ticketCategoryRepository.getTicketAllocation(eventId); int notAllocated = ticketRepository.countNotAllocatedFreeAndReleasedTicket(eventId); int requestedTickets = tcm.isBounded() ? tcm.getMaxTickets() : 1; return new Result.Builder<Integer>() .checkPrecondition( () -> sum + requestedTickets <= eventRepository.countExistingTickets(eventId), ErrorCode.CategoryError.NOT_ENOUGH_SEATS) .checkPrecondition(() -> requestedTickets <= notAllocated, ErrorCode.CategoryError.ALL_TICKETS_ASSIGNED) .checkPrecondition( () -> tcm.getExpiration().toZonedDateTime(event.getZoneId()).isBefore(event.getEnd()), ErrorCode.CategoryError.EXPIRATION_AFTER_EVENT_END) .build(() -> insertCategory(tcm, event)); }).orElseGet(() -> Result.error(ErrorCode.EventError.ACCESS_DENIED)); } /** * This method has been modified to use the new Result<T> mechanism. * It will be replaced by {@link #updateCategory(int, Event, TicketCategoryModification, String)} in the next releases */ public void updateCategory(int categoryId, int eventId, TicketCategoryModification tcm, String username) { final Event event = eventRepository.findById(eventId); checkOwnership(event, username, event.getOrganizationId()); Result<TicketCategory> result = updateCategory(categoryId, event, tcm, username); failIfError(result); } private <T> void failIfError(Result<T> result) { if (!result.isSuccess()) { Optional<ErrorCode> firstError = result.getErrors().stream().findFirst(); if (firstError.isPresent()) { throw new IllegalArgumentException(firstError.get().getDescription()); } throw new IllegalArgumentException("unknown error"); } } Result<TicketCategory> updateCategory(int categoryId, Event event, TicketCategoryModification tcm, String username, boolean resetTicketsToFree) { checkOwnership(event, username, event.getOrganizationId()); int eventId = event.getId(); return Optional .of(ticketCategoryRepository.getById(categoryId)).filter(tc -> tc.getId() == categoryId).map( existing -> new Result.Builder<TicketCategory>() .checkPrecondition(() -> tcm.getExpiration().toZonedDateTime(event.getZoneId()) .isBefore(event.getEnd()), ErrorCode.CategoryError.EXPIRATION_AFTER_EVENT_END) .checkPrecondition( () -> tcm.getMaxTickets() - existing.getMaxTickets() + ticketRepository.countAllocatedTicketsForEvent( eventId) <= eventRepository.countExistingTickets(eventId), ErrorCode.CategoryError.NOT_ENOUGH_SEATS) .checkPrecondition( () -> tcm.isTokenGenerationRequested() == existing.isAccessRestricted() || ticketRepository.countConfirmedAndPendingTickets(eventId, categoryId) == 0, ErrorCode .custom("", "cannot update category: there are tickets already sold.")) .checkPrecondition(() -> tcm.isBounded() == existing .isBounded() || ticketRepository.countPendingOrReleasedForCategory(eventId, existing.getId()) == 0, ErrorCode.custom("", "It is not safe to change allocation strategy right now because there are pending reservations.")) .checkPrecondition( () -> !existing.isAccessRestricted() || tcm.isBounded() == existing.isAccessRestricted(), ErrorCode.custom("", "Dynamic allocation is not compatible with restricted access")) .checkPrecondition(() -> { // see https://github.com/exteso/alf.io/issues/335 // handle the case when the user try to shrink a category with tokens that are already sent // we should fail if there are not enough free token left int addedTicket = tcm.getMaxTickets() - existing.getMaxTickets(); if (addedTicket < 0 && existing.isAccessRestricted() && specialPriceRepository .countNotSentToken(categoryId) < Math.abs(addedTicket)) { return false; } else { return true; } }, ErrorCode.CategoryError.NOT_ENOUGH_FREE_TOKEN_FOR_SHRINK) .checkPrecondition(() -> { if (tcm.isBounded() && !existing.isBounded()) { int newSize = tcm.getMaxTickets(); int confirmed = ticketRepository.countConfirmedForCategory(eventId, existing.getId()); return newSize >= confirmed; } else { return true; } }, ErrorCode.custom("", "Not enough tickets")).build(() -> { updateCategory(tcm, event.isFreeOfCharge(), event.getZoneId(), event, resetTicketsToFree); return ticketCategoryRepository.getByIdAndActive(categoryId, eventId); })) .orElseGet(() -> Result.error(ErrorCode.CategoryError.NOT_FOUND)); } Result<TicketCategory> updateCategory(int categoryId, Event event, TicketCategoryModification tcm, String username) { return updateCategory(categoryId, event, tcm, username, false); } void fixOutOfRangeCategories(EventModification em, String username, ZoneId zoneId, ZonedDateTime end) { Event event = getSingleEvent(em.getShortName(), username); ticketCategoryRepository.findAllTicketCategories(event.getId()).stream() .map(tc -> Triple.of(tc, tc.getInception(zoneId), tc.getExpiration(zoneId))) .filter(t -> t.getRight().isAfter(end)) .forEach(t -> fixTicketCategoryDates(end, t.getLeft(), t.getMiddle(), t.getRight())); } private void fixTicketCategoryDates(ZonedDateTime end, TicketCategory tc, ZonedDateTime inception, ZonedDateTime expiration) { final ZonedDateTime newExpiration = ObjectUtils.min(end, expiration); Objects.requireNonNull(newExpiration); Validate.isTrue(inception.isBefore(newExpiration), format("Cannot fix dates for category \"%s\" (id: %d), try updating that category first.", tc.getName(), tc.getId())); ticketCategoryRepository.fixDates(tc.getId(), inception, newExpiration); } public void reallocateTickets(int srcCategoryId, int targetCategoryId, int eventId) { Event event = eventRepository.findById(eventId); reallocateTickets(ticketCategoryRepository.findStatisticWithId(srcCategoryId, eventId), Optional.of(ticketCategoryRepository.getByIdAndActive(targetCategoryId, event.getId())), event); } void reallocateTickets(TicketCategoryStatisticView src, Optional<TicketCategory> target, Event event) { int notSoldTickets = src.getNotSoldTicketsCount(); if (notSoldTickets == 0) { log.debug("since all the ticket have been sold, ticket moving is not needed anymore."); return; } List<Integer> lockedTickets = ticketRepository.selectTicketInCategoryForUpdate(event.getId(), src.getId(), notSoldTickets, singletonList(TicketStatus.FREE.name())); int locked = lockedTickets.size(); if (locked != notSoldTickets) { throw new IllegalStateException( String.format("Expected %d free tickets, got %d.", notSoldTickets, locked)); } ticketCategoryRepository.updateSeatsAvailability(src.getId(), src.getSoldTicketsCount()); if (target.isPresent()) { TicketCategory targetCategory = target.get(); ticketCategoryRepository.updateSeatsAvailability(targetCategory.getId(), targetCategory.getMaxTickets() + locked); ticketRepository.moveToAnotherCategory(lockedTickets, targetCategory.getId(), targetCategory.getSrcPriceCts()); if (targetCategory.isAccessRestricted()) { insertTokens(targetCategory, locked); } else { ticketRepository.resetTickets(lockedTickets); } } else { int result = ticketRepository.unbindTicketsFromCategory(event.getId(), src.getId(), lockedTickets); Validate.isTrue(result == locked, String.format("Expected %d modified tickets, got %d.", locked, result)); ticketRepository.resetTickets(lockedTickets); } specialPriceRepository.cancelExpiredTokens(src.getId()); } public void unbindTickets(String eventName, int categoryId, String username) { Event event = getSingleEvent(eventName, username); Validate.isTrue(ticketCategoryRepository.countUnboundedCategoriesByEventId(event.getId()) > 0, "cannot unbind tickets: there aren't any unbounded categories"); TicketCategoryStatisticView ticketCategory = ticketCategoryRepository.findStatisticWithId(categoryId, event.getId()); Validate.isTrue(ticketCategory.isBounded(), "cannot unbind tickets from an unbounded category!"); reallocateTickets(ticketCategory, Optional.empty(), event); } MapSqlParameterSource[] prepareTicketsBulkInsertParameters(ZonedDateTime creation, Event event, int requestedTickets, TicketStatus ticketStatus) { //FIXME: the date should be inserted as ZonedDateTime ! Date creationDate = Date.from(creation.toInstant()); List<TicketCategory> categories = ticketCategoryRepository.findByEventId(event.getId()); Stream<MapSqlParameterSource> boundedTickets = categories.stream().filter(IS_CATEGORY_BOUNDED) .flatMap(tc -> generateTicketsForCategory(tc, event, creationDate, 0)); int generatedTickets = categories.stream().filter(IS_CATEGORY_BOUNDED) .mapToInt(TicketCategory::getMaxTickets).sum(); if (generatedTickets >= requestedTickets) { return boundedTickets.toArray(MapSqlParameterSource[]::new); } return Stream.concat(boundedTickets, generateEmptyTickets(event, creationDate, requestedTickets - generatedTickets, ticketStatus)) .toArray(MapSqlParameterSource[]::new); } private Stream<MapSqlParameterSource> generateTicketsForCategory(TicketCategory tc, Event event, Date creationDate, int existing) { Optional<TicketCategory> filteredTC = Optional.of(tc).filter(TicketCategory::isBounded); int missingTickets = filteredTC.map(c -> Math.abs(c.getMaxTickets() - existing)) .orElseGet(() -> eventRepository.countExistingTickets(event.getId()) - existing); return generateStreamForTicketCreation(missingTickets) .map(ps -> buildTicketParams(event.getId(), creationDate, filteredTC, tc.getSrcPriceCts(), ps)); } private void createCategoriesForEvent(EventModification em, Event event) { boolean freeOfCharge = em.isFreeOfCharge(); ZoneId zoneId = TimeZone.getTimeZone(event.getTimeZone()).toZoneId(); int eventId = event.getId(); int requestedSeats = em.getTicketCategories().stream().filter(TicketCategoryModification::isBounded) .mapToInt(TicketCategoryModification::getMaxTickets).sum(); int notAssignedTickets = em.getAvailableSeats() - requestedSeats; Validate.isTrue(notAssignedTickets >= 0, "Total categories' seats cannot be more than the actual event seats"); Validate.isTrue( notAssignedTickets > 0 || em.getTicketCategories().stream().allMatch(TicketCategoryModification::isBounded), "Cannot add an unbounded category if there aren't any free tickets"); em.getTicketCategories().forEach(tc -> { final int price = evaluatePrice(tc.getPriceInCents(), freeOfCharge); final int maxTickets = tc.isBounded() ? tc.getMaxTickets() : 0; final AffectedRowCountAndKey<Integer> category = ticketCategoryRepository.insert( tc.getInception().toZonedDateTime(zoneId), tc.getExpiration().toZonedDateTime(zoneId), tc.getName(), maxTickets, tc.isTokenGenerationRequested(), eventId, tc.isBounded(), price, StringUtils.trimToNull(tc.getCode()), toZonedDateTime(tc.getValidCheckInFrom(), zoneId), toZonedDateTime(tc.getValidCheckInTo(), zoneId), toZonedDateTime(tc.getTicketValidityStart(), zoneId), toZonedDateTime(tc.getTicketValidityEnd(), zoneId)); insertOrUpdateTicketCategoryDescription(category.getKey(), tc, event); if (tc.isTokenGenerationRequested()) { final TicketCategory ticketCategory = ticketCategoryRepository.getByIdAndActive(category.getKey(), event.getId()); final MapSqlParameterSource[] args = prepareTokenBulkInsertParameters(ticketCategory, ticketCategory.getMaxTickets()); jdbc.batchUpdate(specialPriceRepository.bulkInsert(), args); } }); } private Integer insertCategory(TicketCategoryModification tc, Event event) { ZoneId zoneId = event.getZoneId(); int eventId = event.getId(); final int price = evaluatePrice(tc.getPriceInCents(), event.isFreeOfCharge()); final AffectedRowCountAndKey<Integer> category = ticketCategoryRepository.insert( tc.getInception().toZonedDateTime(zoneId), tc.getExpiration().toZonedDateTime(zoneId), tc.getName(), tc.isBounded() ? tc.getMaxTickets() : 0, tc.isTokenGenerationRequested(), eventId, tc.isBounded(), price, StringUtils.trimToNull(tc.getCode()), toZonedDateTime(tc.getValidCheckInFrom(), zoneId), toZonedDateTime(tc.getValidCheckInTo(), zoneId), toZonedDateTime(tc.getTicketValidityStart(), zoneId), toZonedDateTime(tc.getTicketValidityEnd(), zoneId)); TicketCategory ticketCategory = ticketCategoryRepository.getByIdAndActive(category.getKey(), eventId); if (tc.isBounded()) { List<Integer> lockedTickets = ticketRepository.selectNotAllocatedTicketsForUpdate(eventId, ticketCategory.getMaxTickets(), asList(TicketStatus.FREE.name(), TicketStatus.RELEASED.name())); jdbc.batchUpdate(ticketRepository.bulkTicketUpdate(), lockedTickets.stream() .map(id -> new MapSqlParameterSource("id", id).addValue("categoryId", ticketCategory.getId()) .addValue("srcPriceCts", ticketCategory.getSrcPriceCts())) .toArray(MapSqlParameterSource[]::new)); if (tc.isTokenGenerationRequested()) { insertTokens(ticketCategory); ticketRepository.revertToFree(eventId, ticketCategory.getId(), lockedTickets); } else { ticketRepository.resetTickets(lockedTickets);//reset to RELEASED } } insertOrUpdateTicketCategoryDescription(category.getKey(), tc, event); return category.getKey(); } private void insertTokens(TicketCategory ticketCategory) { insertTokens(ticketCategory, ticketCategory.getMaxTickets()); } private void insertTokens(TicketCategory ticketCategory, int requiredTokens) { final MapSqlParameterSource[] args = prepareTokenBulkInsertParameters(ticketCategory, requiredTokens); jdbc.batchUpdate(specialPriceRepository.bulkInsert(), args); } private void insertOrUpdateTicketCategoryDescription(int tcId, TicketCategoryModification tc, Event event) { ticketCategoryDescriptionRepository.delete(tcId); Set<String> eventLang = ContentLanguage.findAllFor(event.getLocales()).stream() .map(ContentLanguage::getLanguage).collect(Collectors.toSet()); Optional.ofNullable(tc.getDescription()).ifPresent(descriptions -> descriptions.forEach((locale, desc) -> { if (eventLang.contains(locale)) { ticketCategoryDescriptionRepository.insert(tcId, locale, desc); } })); } private void updateCategory(TicketCategoryModification tc, boolean freeOfCharge, ZoneId zoneId, Event event, boolean resetTicketsToFree) { int eventId = event.getId(); final int price = evaluatePrice(tc.getPriceInCents(), freeOfCharge); TicketCategory original = ticketCategoryRepository.getByIdAndActive(tc.getId(), eventId); ticketCategoryRepository.update(tc.getId(), tc.getName(), tc.getInception().toZonedDateTime(zoneId), tc.getExpiration().toZonedDateTime(zoneId), tc.getMaxTickets(), tc.isTokenGenerationRequested(), price, StringUtils.trimToNull(tc.getCode()), toZonedDateTime(tc.getValidCheckInFrom(), zoneId), toZonedDateTime(tc.getValidCheckInTo(), (zoneId)), toZonedDateTime(tc.getTicketValidityStart(), zoneId), toZonedDateTime(tc.getTicketValidityEnd(), zoneId)); TicketCategory updated = ticketCategoryRepository.getByIdAndActive(tc.getId(), eventId); int addedTickets = 0; if (original.isBounded() ^ tc.isBounded()) { handleTicketAllocationStrategyChange(event, original, tc); } else { addedTickets = updated.getMaxTickets() - original.getMaxTickets(); handleTicketNumberModification(event, original, updated, addedTickets, resetTicketsToFree); } handleTokenModification(original, updated, addedTickets); handlePriceChange(event, original, updated); insertOrUpdateTicketCategoryDescription(tc.getId(), tc, event); // auditingRepository.insertUpdateTicketInCategoryId(tc.getId()); } private void handleTicketAllocationStrategyChange(Event event, TicketCategory original, TicketCategoryModification updated) { if (updated.isBounded()) { //the ticket allocation strategy has been changed to "bounded", //therefore we have to link the tickets which have not yet been acquired to this category int eventId = event.getId(); int newSize = updated.getMaxTickets(); int confirmed = ticketRepository.countConfirmedForCategory(eventId, original.getId()); int addedTickets = newSize - confirmed; List<Integer> ids = ticketRepository.selectNotAllocatedTicketsForUpdate(eventId, addedTickets, singletonList(TicketStatus.FREE.name())); Validate.isTrue(ids.size() == addedTickets, "not enough tickets"); Validate.isTrue(ids.size() == 0 || ticketRepository.moveToAnotherCategory(ids, original.getId(), updated.getPriceInCents()) == ids.size(), "not enough tickets"); } else { reallocateTickets(ticketCategoryRepository.findStatisticWithId(original.getId(), event.getId()), Optional.empty(), event); } ticketCategoryRepository.updateBoundedFlag(original.getId(), updated.isBounded()); } void handlePriceChange(Event event, TicketCategory original, TicketCategory updated) { if (original.getSrcPriceCts() == updated.getSrcPriceCts() || !original.isBounded()) { return; } final List<Integer> ids = ticketRepository.selectTicketInCategoryForUpdate(event.getId(), updated.getId(), updated.getMaxTickets(), singletonList(TicketStatus.FREE.name())); if (ids.size() < updated.getMaxTickets()) { throw new IllegalStateException( "Tickets have already been sold (or are in the process of being sold) for this category. Therefore price update is not allowed."); } //there's no need to calculate final price, vat etc, since these values will be updated at the time of reservation ticketRepository.updateTicketPrice(updated.getId(), event.getId(), updated.getSrcPriceCts(), 0, 0, 0); } void handleTokenModification(TicketCategory original, TicketCategory updated, int addedTickets) { if (original.isAccessRestricted() ^ updated.isAccessRestricted()) { if (updated.isAccessRestricted()) { final MapSqlParameterSource[] args = prepareTokenBulkInsertParameters(updated, updated.getMaxTickets()); jdbc.batchUpdate(specialPriceRepository.bulkInsert(), args); } else { specialPriceRepository.cancelExpiredTokens(updated.getId()); } } else if (updated.isAccessRestricted() && addedTickets != 0) { if (addedTickets > 0) { jdbc.batchUpdate(specialPriceRepository.bulkInsert(), prepareTokenBulkInsertParameters(updated, addedTickets)); } else { int absDifference = Math.abs(addedTickets); final List<Integer> ids = specialPriceRepository.lockNotSentTokens(updated.getId(), absDifference); Validate.isTrue(ids.size() - absDifference == 0, "not enough tokens"); specialPriceRepository.cancelTokens(ids); } } } void handleTicketNumberModification(Event event, TicketCategory original, TicketCategory updated, int addedTickets, boolean resetToFree) { if (addedTickets == 0) { log.debug("ticket handling not required since the number of ticket wasn't modified"); return; } log.debug("modification detected in ticket number. The difference is: {}", addedTickets); if (addedTickets > 0) { //the updated category contains more tickets than the older one List<Integer> lockedTickets = ticketRepository.selectNotAllocatedTicketsForUpdate(event.getId(), addedTickets, asList(TicketStatus.FREE.name(), TicketStatus.RELEASED.name())); Validate.isTrue(addedTickets == lockedTickets.size(), "Cannot add %d tickets. There are only %d free tickets.", addedTickets, lockedTickets.size()); jdbc.batchUpdate(ticketRepository.bulkTicketUpdate(), lockedTickets.stream() .map(id -> new MapSqlParameterSource("id", id).addValue("categoryId", updated.getId()) .addValue("srcPriceCts", updated.getSrcPriceCts())) .toArray(MapSqlParameterSource[]::new)); if (updated.isAccessRestricted()) { //since the updated category is not public, the tickets shouldn't be distributed to waiting people. ticketRepository.revertToFree(event.getId(), updated.getId(), lockedTickets); } else if (!resetToFree) { ticketRepository.resetTickets(lockedTickets); } } else { int absDifference = Math.abs(addedTickets); final List<Integer> ids = ticketRepository.lockTicketsToInvalidate(event.getId(), updated.getId(), absDifference); int actualDifference = ids.size(); if (actualDifference < absDifference) { throw new IllegalStateException("Cannot invalidate " + absDifference + " tickets. There are only " + actualDifference + " free tickets"); } ticketRepository.invalidateTickets(ids); final MapSqlParameterSource[] params = generateEmptyTickets(event, Date.from(ZonedDateTime.now(event.getZoneId()).toInstant()), absDifference, TicketStatus.RELEASED).toArray(MapSqlParameterSource[]::new); jdbc.batchUpdate(ticketRepository.bulkTicketInitialization(), params); } } private MapSqlParameterSource[] prepareTokenBulkInsertParameters(TicketCategory tc, int limit) { return generateStreamForTicketCreation(limit).peek(ps -> { ps.addValue("code", UUID.randomUUID().toString()); ps.addValue("priceInCents", tc.getSrcPriceCts()); ps.addValue("ticketCategoryId", tc.getId()); ps.addValue("status", SpecialPrice.Status.WAITING.name()); }).toArray(MapSqlParameterSource[]::new); } private void createAllTicketsForEvent(Event event, EventModification em) { final MapSqlParameterSource[] params = prepareTicketsBulkInsertParameters( ZonedDateTime.now(event.getZoneId()), event, em.getAvailableSeats(), TicketStatus.FREE); jdbc.batchUpdate(ticketRepository.bulkTicketInitialization(), params); } private int insertEvent(EventModification em) { String paymentProxies = collectPaymentProxies(em); BigDecimal vat = !em.isInternal() || em.isFreeOfCharge() ? BigDecimal.ZERO : em.getVatPercentage(); String privateKey = UUID.randomUUID().toString(); ZoneId zoneId = ZoneId.of(em.getZoneId()); String currentVersion = flyway.info().current().getVersion().getVersion(); return eventRepository.insert(em.getShortName(), em.getEventType(), em.getDisplayName(), em.getWebsiteUrl(), em.getExternalUrl(), em.isInternal() ? em.getTermsAndConditionsUrl() : "", em.getImageUrl(), em.getFileBlobId(), em.getLocation(), em.getLatitude(), em.getLongitude(), em.getBegin().toZonedDateTime(zoneId), em.getEnd().toZonedDateTime(zoneId), em.getZoneId(), em.getCurrency(), em.getAvailableSeats(), em.isInternal() && em.isVatIncluded(), vat, paymentProxies, privateKey, em.getOrganizationId(), em.getLocales(), em.getVatStatus(), em.getPriceInCents(), currentVersion, Event.Status.DRAFT).getKey(); } private String collectPaymentProxies(EventModification em) { return em.getAllowedPaymentProxies().stream().map(PaymentProxy::name).collect(joining(",")); } public TicketCategory getTicketCategoryById(int id, int eventId) { return ticketCategoryRepository.getByIdAndActive(id, eventId); } public AdditionalService getAdditionalServiceById(int id, int eventId) { return additionalServiceRepository.getById(id, eventId); } public boolean toggleTicketLocking(String eventName, int categoryId, int ticketId, String username) { Event event = getSingleEvent(eventName, username); checkOwnership(event, username, event.getOrganizationId()); ticketCategoryRepository.findByEventId(event.getId()).stream().filter(tc -> tc.getId() == categoryId) .findFirst().orElseThrow(IllegalArgumentException::new); Ticket ticket = ticketRepository.findById(ticketId, categoryId); Validate.isTrue( ticketRepository.toggleTicketLocking(ticketId, categoryId, !ticket.getLockedAssignment()) == 1, "unwanted result from ticket locking"); return true; } public void addPromoCode(String promoCode, Integer eventId, Integer organizationId, ZonedDateTime start, ZonedDateTime end, int discountAmount, DiscountType discountType, List<Integer> categoriesId) { Validate.isTrue(promoCode.length() >= 7, "min length is 7 chars"); Validate.isTrue((eventId != null && organizationId == null) || (eventId == null && organizationId != null), "eventId or organizationId must be not null"); if (DiscountType.PERCENTAGE == discountType) { Validate.inclusiveBetween(0, 100, discountAmount, "percentage discount must be between 0 and 100"); } if (DiscountType.FIXED_AMOUNT == discountType) { Validate.isTrue(discountAmount >= 0, "fixed discount amount cannot be less than zero"); } // categoriesId = Optional.ofNullable(categoriesId).orElse(Collections.emptyList()).stream() .filter(Objects::nonNull).collect(toList()); // promoCodeRepository.addPromoCode(promoCode, eventId, organizationId, start, end, discountAmount, discountType.toString(), Json.GSON.toJson(categoriesId)); } public void deletePromoCode(int promoCodeId) { promoCodeRepository.deletePromoCode(promoCodeId); } public void updatePromoCode(int promoCodeId, ZonedDateTime start, ZonedDateTime end) { promoCodeRepository.updateEventPromoCode(promoCodeId, start, end); } public List<PromoCodeDiscountWithFormattedTime> findPromoCodesInEvent(int eventId) { ZoneId zoneId = eventRepository.findById(eventId).getZoneId(); return promoCodeRepository.findAllInEvent(eventId).stream() .map((p) -> new PromoCodeDiscountWithFormattedTime(p, zoneId)).collect(toList()); } public List<PromoCodeDiscountWithFormattedTime> findPromoCodesInOrganization(int organizationId) { ZoneId zoneId = ZoneId.systemDefault(); return promoCodeRepository.findAllInOrganization(organizationId).stream() .map((p) -> new PromoCodeDiscountWithFormattedTime(p, zoneId)).collect(toList()); } public String getEventUrl(Event event) { return StringUtils.removeEnd( configurationManager.getRequiredValue( Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.BASE_URL)), "/") + "/event/" + event.getShortName() + "/"; } public List<TicketCSVInfo> findAllConfirmedTicketsForCSV(String eventName, String username) { Event event = getSingleEvent(eventName, username); checkOwnership(event, username, event.getOrganizationId()); return ticketRepository.findAllConfirmedForCSV(event.getId()); } public List<Event> getPublishedEvents() { return getActiveEventsStream().filter(e -> e.getStatus() == Event.Status.PUBLIC).collect(toList()); } public List<Event> getActiveEvents() { return getActiveEventsStream().collect(toList()); } private Stream<Event> getActiveEventsStream() { return eventRepository.findAll().stream().filter(e -> e.getEnd().truncatedTo(ChronoUnit.DAYS).plusDays(1) .isAfter(ZonedDateTime.now(e.getZoneId()).truncatedTo(ChronoUnit.DAYS))); } public Function<Ticket, Boolean> checkTicketCancellationPrerequisites() { return CategoryEvaluator.ticketCancellationAvailabilityChecker(ticketCategoryRepository); } void resetReleasedTickets(Event event) { int reverted = ticketRepository.revertToFree(event.getId()); if (reverted > 0) { log.debug("Reverted {} tickets to FREE for event {}", reverted, event.getId()); } } public void updateTicketFieldDescriptions(Map<String, TicketFieldDescriptionModification> descriptions) { descriptions.forEach((locale, value) -> { String description = Json.GSON.toJson(value.getDescription()); if (0 == ticketFieldRepository.updateDescription(value.getTicketFieldConfigurationId(), locale, description)) { ticketFieldRepository.insertDescription(value.getTicketFieldConfigurationId(), locale, description); } }); } public void addAdditionalField(Event event, AdditionalField field) { Integer order = ticketFieldRepository.findMaxOrderValue(event.getId()); insertAdditionalField(event, field, order == null ? 0 : order + 1); } public void deleteAdditionalField(int ticketFieldConfigurationId) { ticketFieldRepository.deleteValues(ticketFieldConfigurationId); ticketFieldRepository.deleteDescription(ticketFieldConfigurationId); ticketFieldRepository.deleteField(ticketFieldConfigurationId); } public void swapAdditionalFieldPosition(int eventId, int id1, int id2) { TicketFieldConfiguration field1 = ticketFieldRepository.findById(id1); TicketFieldConfiguration field2 = ticketFieldRepository.findById(id2); Assert.isTrue(eventId == field1.getEventId(), "eventId does not match field1.eventId"); Assert.isTrue(eventId == field2.getEventId(), "eventId does not match field2.eventId"); ticketFieldRepository.updateFieldOrder(id1, field2.getOrder()); ticketFieldRepository.updateFieldOrder(id2, field1.getOrder()); } public void deleteEvent(int eventId, String username) { final Event event = eventRepository.findById(eventId); checkOwnership(event, username, event.getOrganizationId()); eventDeleterRepository.deleteWaitingQueue(eventId); eventDeleterRepository.deletePluginLog(eventId); eventDeleterRepository.deletePluginConfiguration(eventId); eventDeleterRepository.deleteConfigurationEvent(eventId); eventDeleterRepository.deleteConfigurationTicketCategory(eventId); eventDeleterRepository.deleteEmailMessage(eventId); eventDeleterRepository.deleteTicketFieldValue(eventId); eventDeleterRepository.deleteFieldDescription(eventId); eventDeleterRepository.deleteAdditionalServiceFieldValue(eventId); eventDeleterRepository.deleteAdditionalServiceDescriptions(eventId); eventDeleterRepository.deleteAdditionalServiceItems(eventId); eventDeleterRepository.deleteTicketFieldConfiguration(eventId); eventDeleterRepository.deleteAdditionalServices(eventId); eventDeleterRepository.deleteEventMigration(eventId); eventDeleterRepository.deleteSponsorScan(eventId); eventDeleterRepository.deleteTicket(eventId); eventDeleterRepository.deleteTransactions(eventId); eventDeleterRepository.deleteReservation(eventId); eventDeleterRepository.deletePromoCode(eventId); eventDeleterRepository.deleteTicketCategoryText(eventId); eventDeleterRepository.deleteTicketCategory(eventId); eventDeleterRepository.deleteEventDescription(eventId); eventDeleterRepository.deleteResources(eventId); eventDeleterRepository.deleteScanAudit(eventId); eventDeleterRepository.deleteEvent(eventId); } public void disableEventsFromUsers(List<Integer> userIds) { if (!userIds.isEmpty()) { eventRepository.disableEventsForUsers(userIds); } } @Data private static final class GeolocationResult { private final Pair<String, String> coordinates; private final TimeZone tz; public String getLatitude() { return coordinates.getLeft(); } public String getLongitude() { return coordinates.getRight(); } public String getTimeZone() { return tz.getID(); } public ZoneId getZoneId() { return tz.toZoneId(); } } }