Skip to content

Commit

Permalink
use different UUIDs for reservation/UI and check-in (#1375)
Browse files Browse the repository at this point in the history
* use different UUIDs for reservation/UI and check-in

* fix ticket update URL

* handle online check-in

* fix imports; add test for checking that both unique constraints are applied

* fix imports after merge

* specify charset when parsing string
  • Loading branch information
cbellone authored Jul 14, 2024
1 parent 684eb56 commit f2d89c9
Show file tree
Hide file tree
Showing 48 changed files with 934 additions and 822 deletions.
22 changes: 11 additions & 11 deletions src/main/java/alfio/controller/OnlineCheckInController.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import alfio.manager.ExtensionManager;
import alfio.manager.TicketReservationManager;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -31,6 +30,7 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Optional;
import java.util.UUID;

import static alfio.manager.support.CheckInStatus.ALREADY_CHECK_IN;
import static alfio.manager.support.CheckInStatus.SUCCESS;
Expand All @@ -45,39 +45,39 @@ public class OnlineCheckInController {
private final CheckInManager checkInManager;
private final ExtensionManager extensionManager;

@GetMapping("/event/{shortName}/ticket/{ticketUUID}/check-in/{ticketCodeHash}")
@GetMapping("/event/{shortName}/ticket/{publicUUID}/check-in/{ticketCodeHash}")
public String performCheckIn(@PathVariable("shortName") String eventShortName,
@PathVariable String ticketUUID,
@PathVariable UUID publicUUID,
@PathVariable String ticketCodeHash) {

return ticketReservationManager.fetchCompleteAndAssignedForOnlineCheckIn(eventShortName, ticketUUID)
return ticketReservationManager.fetchCompleteAndAssignedForOnlineCheckIn(eventShortName, publicUUID)
.flatMap(data -> {
var ticket = data.getTicket();
var event = data.getEventWithCheckInInfo();
String ticketCode = ticket.ticketCode(event.getPrivateKey(), event.supportsQRCodeCaseInsensitive());
if(MessageDigest.isEqual(DigestUtils.sha256Hex(ticketCode).getBytes(StandardCharsets.UTF_8), ticketCodeHash.getBytes(StandardCharsets.UTF_8))) {
log.debug("code successfully validated for ticket {}", ticketUUID);
log.debug("code successfully validated for ticket {}", publicUUID);
// check-in can be done. Let's check if there is a redirection URL
var categoryConfiguration = data.getCategoryMetadata().getOnlineConfiguration();
var eventConfiguration = event.getMetadata().getOnlineConfiguration();
var match = findMatchingLink(event.getZoneId(), categoryConfiguration, eventConfiguration);
if(match.isPresent()) {
var checkInStatus = checkInManager.performCheckinForOnlineEvent(ticket, event, data.getTicketCategory());
log.info("check-in status {} for ticket {}", checkInStatus, ticketUUID);
log.info("check-in status {} for ticket {}", checkInStatus, publicUUID);
if(checkInStatus == SUCCESS || (checkInStatus == ALREADY_CHECK_IN && ticket.isCheckedIn())) {
// invoke the extension for customizing the URL, if any
// we call the extension from here because it will have a smaller impact on the throughput compared to
// calling it from the checkInManager
var customUrlOptional = extensionManager.handleOnlineCheckInLink(match.get(), ticket, event, data.getTicketAdditionalInfo());
return customUrlOptional.or(() -> match);
}
log.info("denying check-in for ticket {} because check-in status was {}", ticketUUID, checkInStatus);
return Optional.of("/event/"+event.getShortName()+"/ticket/"+ticketUUID+"/update");
log.info("denying check-in for ticket {} because check-in status was {}", publicUUID, checkInStatus);
return Optional.of("/event/"+event.getShortName()+"/ticket/"+ publicUUID +"/update");
}
log.info("validation was successful, but cannot find a valid link for {}", ticketUUID);
return Optional.of("/event/"+event.getShortName()+"/ticket/"+ticketUUID+"/check-in/"+ticketCodeHash+"/waiting-room");
log.info("validation was successful, but cannot find a valid link for {}", publicUUID);
return Optional.of("/event/"+event.getShortName()+"/ticket/"+ publicUUID +"/check-in/"+ticketCodeHash+"/waiting-room");
}
log.warn("code validation failed for ticket {}", ticketUUID);
log.warn("code validation failed for ticket {}", publicUUID);
return Optional.empty();
})
.map(link -> "redirect:"+link)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
import com.samskivert.mustache.MustacheException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
Expand All @@ -42,7 +42,6 @@
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;

import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -126,7 +125,7 @@ public void previewTemplate(@PathVariable TemplateResource name, @PathVariable S
Organization organization = organizationRepository.getById(organizationId);
Optional<TemplateResource.ImageData> image = TemplateProcessor.extractImageModel(purchaseContext, fileUploadManager);
Map<String, Object> model = name.prepareSampleModel(organization, purchaseContext, image);
String renderedTemplate = templateManager.renderString(purchaseContext, template.getFileAsString(), model, loc, name.getTemplateOutput());
String renderedTemplate = templateManager.renderString(purchaseContext, new String(name.replaceTokens(template.getFile())), model, loc, name.getTemplateOutput());
if(MediaType.TEXT_PLAIN_VALUE.equals(name.getRenderedContentType()) || TemplateResource.MULTIPART_ALTERNATIVE_MIMETYPE.equals(name.getRenderedContentType())) {
response.addHeader("Content-Disposition", "attachment; filename="+name.name()+".txt");
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@
import alfio.manager.PassKitManager;
import alfio.model.EventAndOrganizationId;
import alfio.model.Ticket;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

Expand Down Expand Up @@ -80,7 +79,7 @@ private void writePassResponse(HttpServletResponse response,
try (var os = response.getOutputStream()) {
response.setContentType("application/vnd.apple.pkpass");
if (addFilename) {
response.setHeader("Content-Disposition", "attachment; filename=Passbook-"+ticket.getUuid().substring(0, 8)+".pkpass");
response.setHeader("Content-Disposition", "attachment; filename=Passbook-"+ticket.getPublicUuid().toString().substring(0, 8)+".pkpass");
markAsNoIndex(response);
}
passKitManager.writePass(ticket, eventAndOrganizationId, os);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public BookingInfoTicket toBookingInfoTicket(Ticket t,
cancellationEnabled,
configuration.get(SEND_TICKETS_AUTOMATICALLY).getValueAsBooleanOrDefault(),
configuration.get(ALLOW_TICKET_DOWNLOAD).getValueAsBooleanOrDefault(),
additionalFieldsFilterer.getFieldsForTicket(t.getUuid(), contexts),
additionalFieldsFilterer.getFieldsForTicket(t.getPublicUuid(), contexts),
descriptionsByTicketFieldId,
valuesByTicketIds.getOrDefault(t.getId(), Collections.emptyList()),
formattedOnlineCheckInDate,
Expand Down Expand Up @@ -145,7 +145,7 @@ private static BookingInfoTicket toBookingInfoTicket(Ticket ticket,
.flatMap(tfc -> toAdditionalFieldsStream(descriptionsByTicketFieldId, tfc, valuesById))
.collect(Collectors.toList());

return new BookingInfoTicket(ticket.getUuid(),
return new BookingInfoTicket(ticket.getPublicUuid().toString(),
ticket.getFirstName(),
ticket.getLastName(),
ticket.getEmail(),
Expand Down
32 changes: 13 additions & 19 deletions src/main/java/alfio/controller/api/support/TicketHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@
import java.text.Collator;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -74,17 +72,13 @@ public BiFunction<Ticket, Event, List<FieldConfigurationDescriptionAndValue>> bu
return EventUtil.retrieveFieldValues(ticketRepository, purchaseContextFieldManager, additionalServiceItemRepository, formatValues);
}

public Function<String, Integer> getTicketUUIDToCategoryId() {
return ticketRepository::getTicketCategoryByUIID;
}

public Optional<Triple<ValidationResult, Event, Ticket>> assignTicket(String eventName,
String ticketIdentifier,
UpdateTicketOwnerForm updateTicketOwner,
Optional<BindingResult> bindingResult,
Locale fallbackLocale,
Optional<UserDetails> userDetails,
boolean addPrefix) {
UUID ticketIdentifier,
UpdateTicketOwnerForm updateTicketOwner,
Optional<BindingResult> bindingResult,
Locale fallbackLocale,
Optional<UserDetails> userDetails,
boolean addPrefix) {

return ticketReservationManager.fetchComplete(eventName, ticketIdentifier)
.map(result -> assignTicket(updateTicketOwner, bindingResult, fallbackLocale, userDetails, result, addPrefix ? "tickets["+ticketIdentifier+"]" : ""));
Expand Down Expand Up @@ -123,8 +117,8 @@ private Triple<ValidationResult, Event, Ticket> assignTicket(UpdateTicketOwnerFo

Validator.AdvancedValidationContext context = new Validator.AdvancedValidationContext(updateTicketOwner, fieldConf, t.getCategoryId(), t.getUuid(), formPrefix);

ValidationResult validationResult = Validator.validateTicketAssignment(updateTicketOwner, ticketFieldFilterer.getFieldsForTicket(t.getUuid(), EnumSet.of(ATTENDEE)), bindingResult, event, formPrefix, sameCountryValidator, extensionManager)
.or(validateAdditionalItemsFields(event, updateTicketOwner, t.getUuid(), ticketFieldFilterer.getFieldsForTicket(t.getUuid(), EnumSet.of(ADDITIONAL_SERVICE)), additionalServiceItems, bindingResult.orElse(null), sameCountryValidator))
ValidationResult validationResult = Validator.validateTicketAssignment(updateTicketOwner, ticketFieldFilterer.getFieldsForTicket(t.getPublicUuid(), EnumSet.of(ATTENDEE)), bindingResult, event, formPrefix, sameCountryValidator, extensionManager)
.or(validateAdditionalItemsFields(event, updateTicketOwner, t.getUuid(), ticketFieldFilterer.getFieldsForTicket(t.getPublicUuid(), EnumSet.of(ADDITIONAL_SERVICE)), additionalServiceItems, bindingResult.orElse(null), sameCountryValidator))
.or(Validator.performAdvancedValidation(advancedValidator, context, bindingResult.orElse(null)))
.ifSuccess(() -> updateTicketOwner(updateTicketOwner, fallbackLocale, t, event, ticketReservation, userDetails));
return Triple.of(validationResult, event, ticketsInReservation.stream().filter(t2 -> t2.getUuid().equals(t.getUuid())).findFirst().orElseThrow());
Expand Down Expand Up @@ -177,18 +171,18 @@ private ValidationResult validateAdditionalItemsFields(Event event,
*/
public Optional<Triple<ValidationResult, Event, Ticket>> preAssignTicket(String eventName,
String reservationId,
String ticketIdentifier,
UUID publicTicketUUID,
UpdateTicketOwnerForm updateTicketOwner,
Optional<BindingResult> bindingResult,
Locale fallbackLocale) {

return ticketReservationManager.from(eventName, reservationId, ticketIdentifier)
return ticketReservationManager.from(eventName, reservationId, publicTicketUUID)
.filter(temp -> PENDING_RESERVATION_STATUSES.contains(temp.getMiddle().getStatus()) && temp.getRight().getStatus() == Ticket.TicketStatus.PENDING)
.map(result -> assignTicket(updateTicketOwner, bindingResult, fallbackLocale,Optional.empty(), result,"tickets["+ticketIdentifier+"]"));
.map(result -> assignTicket(updateTicketOwner, bindingResult, fallbackLocale,Optional.empty(), result,"tickets["+publicTicketUUID.toString()+"]"));
}

public Optional<Triple<ValidationResult, Event, Ticket>> assignTicket(String eventName,
String ticketIdentifier,
UUID ticketIdentifier,
UpdateTicketOwnerForm updateTicketOwner,
Optional<BindingResult> bindingResult,
Locale locale) {
Expand All @@ -208,7 +202,7 @@ public Optional<Triple<ValidationResult, Event, Ticket>> directTicketAssignment(
if(tickets.size() > 1) {
return Optional.empty();
}
String ticketUuid = tickets.get(0).getUuid();
var ticketUuid = tickets.get(0).getPublicUuid();
UpdateTicketOwnerForm form = new UpdateTicketOwnerForm();
form.setAdditional(Collections.emptyMap());
form.setEmail(email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,14 +437,13 @@ public ResponseEntity<ValidatedResponse<Boolean>> validateToOverview(@PathVariab
);
}

if (purchaseContext.ofType(PurchaseContextType.event)) {
var event = (Event) purchaseContext;
if (purchaseContext instanceof Event event) {
if (event.supportsLinkedAdditionalServices() && contactAndTicketsForm.hasAdditionalServices()) {
var tickets = ticketReservationManager.findTicketsInReservation(reservationId);
additionalServiceManager.linkItemsToTickets(reservationId, contactAndTicketsForm.getAdditionalServices(), tickets);
additionalServiceManager.persistFieldsForAdditionalItems(event.getId(), event.getOrganizationId(), contactAndTicketsForm.getAdditionalServices(), tickets);
}
assignTickets(event.getShortName(), reservationId, contactAndTicketsForm, bindingResult, locale, true, true);
assignTickets(event, reservationId, contactAndTicketsForm, bindingResult, locale, true, true);
}

if(purchaseContext.ofType(PurchaseContextType.subscription)) {
Expand Down Expand Up @@ -518,14 +517,15 @@ private TicketReservationInvoicingAdditionalInfo.ItalianEInvoicing getItalianInv
contactAndTicketsForm.isItalyEInvoicingSplitPayment());
}

private void assignTickets(String eventName, String reservationId, ContactAndTicketsForm contactAndTicketsForm, BindingResult bindingResult, Locale locale, boolean preAssign, boolean skipValidation) {
private void assignTickets(Event event, String reservationId, ContactAndTicketsForm contactAndTicketsForm, BindingResult bindingResult, Locale locale, boolean preAssign, boolean skipValidation) {
if(!contactAndTicketsForm.isPostponeAssignment()) {
contactAndTicketsForm.getTickets().forEach((ticketId, owner) -> {
contactAndTicketsForm.getTickets().forEach((ticketPublicId, owner) -> {
var publicUUID = UUID.fromString(ticketPublicId);
if (preAssign) {
Optional<BindingResult> bindingResultOptional = skipValidation ? Optional.empty() : Optional.of(bindingResult);
ticketHelper.preAssignTicket(eventName, reservationId, ticketId, owner, bindingResultOptional, locale);
Validate.isTrue(ticketHelper.preAssignTicket(event.getShortName(), reservationId, publicUUID, owner, bindingResultOptional, locale).isPresent());
} else {
ticketHelper.assignTicket(eventName, ticketId, owner, Optional.of(bindingResult), locale, Optional.empty(), true);
Validate.isTrue(ticketHelper.assignTicket(event.getShortName(), publicUUID, owner, Optional.of(bindingResult), locale, Optional.empty(), true).isPresent());
}
});
}
Expand Down
Loading

0 comments on commit f2d89c9

Please sign in to comment.