Skip to content

Commit

Permalink
Porting #744 to master (#755)
Browse files Browse the repository at this point in the history
* #742 - model and backend code for supporting multiple check-ins

* #742 - cast ZonedDateTime to timestamp in order to truncate it
fix logged event (use BADGE_SCAN instead of CHECK_IN)

* #742 - add check-in strategy to category_with_currency

* #742 - add integration test

* #742 - fix test

* #742 add ticket validity check for badge scan

* #742 include check-in strategy in the encrypted payload for Alf.io-PI
include badge scanning auditing in the check-in log

* #742 include ticket validity start/end (if present) in the encrypted payload for Alf.io-PI

* fix test

* #744 - M2 backoffice
  • Loading branch information
cbellone authored and syjer committed Sep 14, 2019
1 parent 1dcfac3 commit de8d941
Show file tree
Hide file tree
Showing 33 changed files with 338 additions and 153 deletions.
5 changes: 3 additions & 2 deletions src/main/java/alfio/manager/AdminReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ private Result<TicketCategory> createCategory(TicketsInfo ti, Event event, Admin
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, 0);
true, null, null, null, null, null, 0, null);
int notAllocated = getNotAllocatedTickets(event);
int missingTickets = Math.max(tickets - notAllocated, 0);
Event modified = increaseSeatsIfNeeded(ti, event, missingTickets, event);
Expand Down Expand Up @@ -550,7 +550,8 @@ private Result<TicketCategory> checkExistingCategory(TicketsInfo ti, Event event
fromZonedDateTime(existing.getValidCheckInFrom(modified.getZoneId())),
fromZonedDateTime(existing.getValidCheckInTo(modified.getZoneId())),
fromZonedDateTime(existing.getTicketValidityStart(modified.getZoneId())),
fromZonedDateTime(existing.getTicketValidityEnd(modified.getZoneId())), 0);
fromZonedDateTime(existing.getTicketValidityEnd(modified.getZoneId())), 0,
existing.getTicketCheckInStrategy());
return eventManager.updateCategory(existingCategoryId, modified, tcm, username, true);
}
return Result.success(existing);
Expand Down
57 changes: 48 additions & 9 deletions src/main/java/alfio/manager/CheckInManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
Expand All @@ -59,6 +60,7 @@
import java.util.stream.Collectors;

import static alfio.manager.support.CheckInStatus.*;
import static alfio.model.Audit.EventType.*;
import static alfio.model.system.ConfigurationKeys.*;
import static java.util.stream.Collectors.toMap;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
Expand Down Expand Up @@ -136,11 +138,17 @@ public TicketAndCheckInResult checkIn(String shortName, String ticketIdentifier,

public TicketAndCheckInResult checkIn(int eventId, String ticketIdentifier, Optional<String> ticketCode, String user) {
TicketAndCheckInResult descriptor = extractStatus(eventId, ticketRepository.findByUUIDForUpdate(ticketIdentifier), ticketIdentifier, ticketCode);
if(descriptor.getResult().getStatus() == OK_READY_TO_BE_CHECKED_IN) {
var checkInStatus = descriptor.getResult().getStatus();
if(checkInStatus == OK_READY_TO_BE_CHECKED_IN) {
checkIn(ticketIdentifier);
scanAuditRepository.insert(ticketIdentifier, eventId, ZonedDateTime.now(), user, SUCCESS, ScanAudit.Operation.SCAN);
auditingRepository.insert(descriptor.getTicket().getTicketsReservationId(), userRepository.findIdByUserName(user).orElse(null), eventId, Audit.EventType.CHECK_IN, new Date(), Audit.EntityType.TICKET, Integer.toString(descriptor.getTicket().getId()));
auditingRepository.insert(descriptor.getTicket().getTicketsReservationId(), userRepository.findIdByUserName(user).orElse(null), eventId, CHECK_IN, new Date(), Audit.EntityType.TICKET, Integer.toString(descriptor.getTicket().getId()));
return new TicketAndCheckInResult(descriptor.getTicket(), new DefaultCheckInResult(SUCCESS, "success"));
} else if(checkInStatus == BADGE_SCAN_ALREADY_DONE || checkInStatus == OK_READY_FOR_BADGE_SCAN) {
var auditingStatus = checkInStatus == OK_READY_FOR_BADGE_SCAN ? BADGE_SCAN_SUCCESS : checkInStatus;
scanAuditRepository.insert(ticketIdentifier, eventId, ZonedDateTime.now(), user, auditingStatus, ScanAudit.Operation.SCAN);
auditingRepository.insert(descriptor.getTicket().getTicketsReservationId(), userRepository.findIdByUserName(user).orElse(null), eventId, BADGE_SCAN, new Date(), Audit.EntityType.TICKET, Integer.toString(descriptor.getTicket().getId()));
return new TicketAndCheckInResult(null, new DefaultCheckInResult(auditingStatus, checkInStatus == OK_READY_FOR_BADGE_SCAN ? "scan successful" : "already scanned"));
}
return descriptor;
}
Expand Down Expand Up @@ -201,20 +209,32 @@ private TicketAndCheckInResult extractStatus(Optional<Event> maybeEvent, Optiona
return new TicketAndCheckInResult(null, new DefaultCheckInResult(TICKET_NOT_FOUND, "Ticket with uuid " + ticketIdentifier + " not found"));
}

if(ticketCode.filter(StringUtils::isNotEmpty).isEmpty()) {
return new TicketAndCheckInResult(null, new DefaultCheckInResult(EMPTY_TICKET_CODE, "Missing ticket code"));
}

Ticket ticket = maybeTicket.get();
Event event = maybeEvent.get();
String code = ticketCode.get();

if(ticket.getCategoryId() == null) {
return new TicketAndCheckInResult(new TicketWithCategory(ticket, null), new DefaultCheckInResult(INVALID_TICKET_STATE, "Invalid ticket state"));
}

TicketCategory tc = ticketCategoryRepository.getById(ticket.getCategoryId());

Event event = maybeEvent.get();
if(ticketCode.filter(StringUtils::isNotBlank).isEmpty()) {
if(ticket.isCheckedIn() && tc.getTicketCheckInStrategy() == TicketCategory.TicketCheckInStrategy.ONCE_PER_DAY) {
if(!isBadgeValidNow(tc, event)) {
// if the badge is not currently valid, we give an error
return new TicketAndCheckInResult(new TicketWithCategory(ticket, null), new DefaultCheckInResult(INVALID_TICKET_CATEGORY_CHECK_IN_DATE, "Not allowed to check in at this time."));
}
var ticketsReservationId = ticket.getTicketsReservationId();
int previousScan = auditingRepository.countAuditsOfTypesInTheSameDay(ticketsReservationId, Set.of(CHECK_IN.name(), MANUAL_CHECK_IN.name(), BADGE_SCAN.name()), ZonedDateTime.now(event.getZoneId()));
if(previousScan > 0) {
return new TicketAndCheckInResult(new TicketWithCategory(ticket, null), new DefaultCheckInResult(BADGE_SCAN_ALREADY_DONE, "Badge scan already done"));
}
return new TicketAndCheckInResult(new TicketWithCategory(ticket, null), new DefaultCheckInResult(OK_READY_FOR_BADGE_SCAN, "Badge scan already done"));
}
return new TicketAndCheckInResult(null, new DefaultCheckInResult(EMPTY_TICKET_CODE, "Missing ticket code"));
}

String code = ticketCode.get();

ZonedDateTime now = ZonedDateTime.now(event.getZoneId());
if(!tc.hasValidCheckIn(now, event.getZoneId())) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy - hh:mm");
Expand Down Expand Up @@ -247,6 +267,18 @@ private TicketAndCheckInResult extractStatus(Optional<Event> maybeEvent, Optiona
return new TicketAndCheckInResult(new TicketWithCategory(ticket, tc), new DefaultCheckInResult(OK_READY_TO_BE_CHECKED_IN, "Ready to be checked in"));
}

private static boolean isBadgeValidNow(TicketCategory tc, Event event) {
var zoneId = event.getZoneId();
var now = ZonedDateTime.now(zoneId);
return now.isAfter(toZoneIdIfNotNull(tc.getValidCheckInFrom(), zoneId).orElse(event.getBegin()))
&& now.isAfter(toZoneIdIfNotNull(tc.getTicketValidityStart(), zoneId).orElse(event.getBegin()))
&& now.isBefore(toZoneIdIfNotNull(tc.getTicketValidityEnd(), zoneId).orElse(event.getEnd()));
}

private static Optional<ZonedDateTime> toZoneIdIfNotNull(ZonedDateTime in, ZoneId zoneId) {
return Optional.ofNullable(in).map(d -> d.withZoneSameInstant(zoneId));
}

private static Pair<Cipher, SecretKeySpec> getCypher(String key) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
Expand Down Expand Up @@ -370,6 +402,13 @@ public Map<String,String> getEncryptedAttendeesInformation(Event ev, Set<String>
if (tc.getValidCheckInTo() != null) {
info.put("validCheckInTo", Long.toString(tc.getValidCheckInTo(event.getZoneId()).toEpochSecond()));
}
if (tc.getTicketValidityStart() != null) {
info.put("ticketValidityStart", Long.toString(tc.getTicketValidityStart(event.getZoneId()).toEpochSecond()));
}
if (tc.getTicketValidityEnd() != null) {
info.put("ticketValidityEnd", Long.toString(tc.getTicketValidityEnd(event.getZoneId()).toEpochSecond()));
}
info.put("categoryCheckInStrategy", tc.getTicketCheckInStrategy().name());
//

List<BookedAdditionalService> additionalServices = additionalServiceItemRepository.getAdditionalServicesBookedForReservation(ticket.getTicketsReservationId(), ticket.getUserLanguage(), ticket.getEventId());
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/alfio/manager/EventManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static alfio.model.TicketCategory.TicketCheckInStrategy.ONCE_PER_EVENT;
import static alfio.model.modification.DateTimeModification.toZonedDateTime;
import static alfio.util.EventUtil.*;
import static alfio.util.Wrappers.optionally;
Expand Down Expand Up @@ -546,7 +547,7 @@ private void createCategoriesForEvent(EventModification em, Event event) {
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), tc.getOrdinal());
toZonedDateTime(tc.getTicketValidityStart(), zoneId), toZonedDateTime(tc.getTicketValidityEnd(), zoneId), tc.getOrdinal(), Optional.ofNullable(tc.getTicketCheckInStrategy()).orElse(ONCE_PER_EVENT));

insertOrUpdateTicketCategoryDescription(category.getKey(), tc, event);

Expand All @@ -566,7 +567,8 @@ private Integer insertCategory(TicketCategoryModification tc, Event event) {
toZonedDateTime(tc.getValidCheckInFrom(), zoneId),
toZonedDateTime(tc.getValidCheckInTo(), zoneId),
toZonedDateTime(tc.getTicketValidityStart(), zoneId),
toZonedDateTime(tc.getTicketValidityEnd(), zoneId), tc.getOrdinal());
toZonedDateTime(tc.getTicketValidityEnd(), zoneId), tc.getOrdinal(),
Optional.ofNullable(tc.getTicketCheckInStrategy()).orElse(ONCE_PER_EVENT));
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()));
Expand Down Expand Up @@ -617,7 +619,8 @@ private void updateCategory(TicketCategoryModification tc,
toZonedDateTime(tc.getValidCheckInFrom(), zoneId),
toZonedDateTime(tc.getValidCheckInTo(), (zoneId)),
toZonedDateTime(tc.getTicketValidityStart(), zoneId),
toZonedDateTime(tc.getTicketValidityEnd(), zoneId));
toZonedDateTime(tc.getTicketValidityEnd(), zoneId),
Optional.ofNullable(tc.getTicketCheckInStrategy()).orElse(ONCE_PER_EVENT));
TicketCategory updated = ticketCategoryRepository.getByIdAndActive(tc.getId(), eventId);
int addedTickets = 0;
if(original.isBounded() ^ tc.isBounded()) {
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/alfio/manager/support/CheckInStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,17 @@
package alfio.manager.support;

public enum CheckInStatus {
EVENT_NOT_FOUND, TICKET_NOT_FOUND, EMPTY_TICKET_CODE, INVALID_TICKET_CODE, INVALID_TICKET_STATE, ALREADY_CHECK_IN, MUST_PAY, OK_READY_TO_BE_CHECKED_IN, SUCCESS, INVALID_TICKET_CATEGORY_CHECK_IN_DATE
EVENT_NOT_FOUND,
TICKET_NOT_FOUND,
EMPTY_TICKET_CODE,
INVALID_TICKET_CODE,
INVALID_TICKET_STATE,
ALREADY_CHECK_IN,
MUST_PAY,
OK_READY_TO_BE_CHECKED_IN,
SUCCESS,
INVALID_TICKET_CATEGORY_CHECK_IN_DATE,
BADGE_SCAN_ALREADY_DONE,
OK_READY_FOR_BADGE_SCAN,
BADGE_SCAN_SUCCESS
}
1 change: 1 addition & 0 deletions src/main/java/alfio/model/Audit.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum EventType {
CHECK_IN,
MANUAL_CHECK_IN,
REVERT_CHECK_IN,
BADGE_SCAN,
UPDATE_TICKET,
UPDATE_TICKET_CATEGORY,
UPDATE_INVOICE,
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/alfio/model/FullTicketInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ public FullTicketInfo(@Column("t_id") int id,
@Column("tc_valid_checkin_to") ZonedDateTime validCheckInTo,
@Column("tc_ticket_validity_start") ZonedDateTime ticketValidityStart,
@Column("tc_ticket_validity_end") ZonedDateTime ticketValidityEnd,
@Column("tc_ordinal") int ordinal
@Column("tc_ordinal") int ordinal,
@Column("tc_ticket_checkin_strategy") TicketCategory.TicketCheckInStrategy ticketCheckInStrategy
) {

this.ticket = new Ticket(id, uuid, creation, categoryId, status, eventId, ticketsReservationId, fullName, firstName, lastName, email,
Expand All @@ -126,7 +127,7 @@ public FullTicketInfo(@Column("t_id") int id,
reservationRegistrationTimestamp, reservationSrcPriceCts, reservationFinalPriceCts, reservationVatCts, reservationDiscountCts, reservationCurrencyCode);
this.ticketCategory = new TicketCategory(tcId, tcUtcInception, tcUtcExpiration, tcMaxTickets, tcName,
tcAccessRestricted, tcStatus, tcEventId, bounded, tcSrcPriceCts, code, validCheckInFrom, validCheckInTo,
ticketValidityStart, ticketValidityEnd, currencyCode, ordinal);
ticketValidityStart, ticketValidityEnd, currencyCode, ordinal, ticketCheckInStrategy);

this.billingDetails = new BillingDetails(billingAddressCompany, billingAddressLine1, billingAddressLine2, billingAddressZip, billingAddressCity, vatCountry, vatNr, invoicingAdditionalInfo);

Expand Down
23 changes: 22 additions & 1 deletion src/main/java/alfio/model/TicketCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ public enum Status {
ACTIVE, NOT_ACTIVE
}

/**
* Defines the check-in strategy that must be adopted for this TicketCategory
*/
public enum TicketCheckInStrategy {
/**
* Default, retro-compatible. Attendees can check-in only once, using their ticket
*/
ONCE_PER_EVENT,
/**
* Extends ONCE_PER_EVENT by adding the possibility to re-enter the event by using the printed badge.
* If an attendee tries to enter more than once in a single day, the operator will receive a warning.
* ** IMPORTANT **:
* additional check-ins can be done only using the printed badge.
* This ensures that a badge is printed only once
*/
ONCE_PER_DAY
}

private final int id;
private final ZonedDateTime utcInception;
private final ZonedDateTime utcExpiration;
Expand All @@ -53,6 +71,7 @@ public enum Status {
private final ZonedDateTime ticketValidityEnd;
private final String currencyCode;
private final int ordinal;
private final TicketCheckInStrategy ticketCheckInStrategy;


public TicketCategory(@JsonProperty("id") @Column("id") int id,
Expand All @@ -71,7 +90,8 @@ public TicketCategory(@JsonProperty("id") @Column("id") int id,
@JsonProperty("ticketValidityStart") @Column("ticket_validity_start") ZonedDateTime ticketValidityStart,
@JsonProperty("ticketValidityEnd") @Column("ticket_validity_end") ZonedDateTime ticketValidityEnd,
@JsonProperty("currencyCode") @Column("currency_code") String currencyCode,
@JsonProperty("ordinal") @Column("ordinal") Integer ordinal) {
@JsonProperty("ordinal") @Column("ordinal") Integer ordinal,
@JsonProperty("ticketCheckInStrategy") @Column("ticket_checkin_strategy") TicketCheckInStrategy ticketCheckInStrategy) {
this.id = id;
this.utcInception = utcInception;
this.utcExpiration = utcExpiration;
Expand All @@ -89,6 +109,7 @@ public TicketCategory(@JsonProperty("id") @Column("id") int id,
this.ticketValidityEnd = ticketValidityEnd;
this.currencyCode = currencyCode;
this.ordinal = ordinal != null ? ordinal : 0;
this.ticketCheckInStrategy = ticketCheckInStrategy;
}

public BigDecimal getPrice() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ TicketCategoryModification toTicketCategoryModification(Integer categoryId) {
customValidityOpt.flatMap(x -> Optional.ofNullable(x.checkInTo)).map(x -> new DateTimeModification(x.toLocalDate(),x.toLocalTime())).orElse(null),
customValidityOpt.flatMap(x -> Optional.ofNullable(x.validityStart)).map(x -> new DateTimeModification(x.toLocalDate(),x.toLocalTime())).orElse(null),
customValidityOpt.flatMap(x -> Optional.ofNullable(x.validityEnd)).map(x -> new DateTimeModification(x.toLocalDate(),x.toLocalTime())).orElse(null),
0
0,
null
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
*/
package alfio.model.modification;

import alfio.model.TicketCategory;
import alfio.model.TicketCategory.TicketCheckInStrategy;
import alfio.util.MonetaryUtil;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
Expand Down Expand Up @@ -44,6 +47,7 @@ public class TicketCategoryModification {
private final DateTimeModification ticketValidityStart;
private final DateTimeModification ticketValidityEnd;
private final int ordinal;
private final TicketCheckInStrategy ticketCheckInStrategy;

@JsonCreator
public TicketCategoryModification(@JsonProperty("id") Integer id,
Expand All @@ -61,7 +65,8 @@ public TicketCategoryModification(@JsonProperty("id") Integer id,
@JsonProperty("validCheckInTo") DateTimeModification validCheckInTo,
@JsonProperty("ticketValidityStart") DateTimeModification ticketValidityStart,
@JsonProperty("ticketValidityEnd") DateTimeModification ticketValidityEnd,
@JsonProperty("ordinal") Integer ordinal) {
@JsonProperty("ordinal") Integer ordinal,
@JsonProperty("ticketCheckInStrategy") TicketCheckInStrategy ticketCheckInStrategy) {
this.id = id;
this.name = name;
this.maxTickets = maxTickets;
Expand All @@ -78,6 +83,7 @@ public TicketCategoryModification(@JsonProperty("id") Integer id,
this.ticketValidityStart = ticketValidityStart;
this.ticketValidityEnd = ticketValidityEnd;
this.ordinal = ordinal != null ? ordinal : 0;
this.ticketCheckInStrategy = ticketCheckInStrategy;
}

}
Loading

0 comments on commit de8d941

Please sign in to comment.