Skip to content

Commit

Permalink
#874 - add missing data points, show weeks instead of days in case th…
Browse files Browse the repository at this point in the history
…e graphs shows more than 3 months

(cherry picked from commit e16b8d3)
  • Loading branch information
cbellone committed Mar 24, 2020
1 parent 3bcc4ec commit 4e42708
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 44 deletions.
49 changes: 35 additions & 14 deletions src/main/java/alfio/controller/api/admin/EventApiController.java
Expand Up @@ -45,7 +45,6 @@
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.dao.DataAccessException;
Expand All @@ -62,14 +61,16 @@
import java.io.OutputStream;
import java.math.BigDecimal;
import java.security.Principal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
Expand Down Expand Up @@ -636,16 +637,35 @@ public PageAndContent<List<TicketWithStatistic>> getTicketsInCategory(@PathVaria
}

@GetMapping("/events/{eventName}/ticket-sold-statistics")
public TicketsStatistics getTicketsStatistics(@PathVariable("eventName") String eventName, @RequestParam(value = "from", required = false) String f, @RequestParam(value = "to", required = false) String t, Principal principal) throws ParseException {
EventAndOrganizationId event = eventManager.getEventAndOrganizationId(eventName, principal.getName());
DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
//TODO: cleanup
Date from = DateUtils.truncate(f == null ? new Date(0) : format.parse(f), Calendar.HOUR);
Date to = DateUtils.addMilliseconds(DateUtils.ceiling(t == null ? new Date() : format.parse(t), Calendar.DATE), -1);
//

int eventId = event.getId();
return new TicketsStatistics(eventStatisticsManager.getTicketSoldStatistics(eventId, from, to), eventStatisticsManager.getTicketReservedStatistics(eventId, from, to));
public ResponseEntity<TicketsStatistics> getTicketsStatistics(@PathVariable("eventName") String eventName,
@RequestParam(value = "from", required = false) String f,
@RequestParam(value = "to", required = false) String t,
Principal principal) {

return ResponseEntity.of(eventManager.getOptionalByName(eventName, principal.getName()).map(event -> {
var eventId = event.getId();
var zoneId = event.getZoneId();
var from = parseDate(f, zoneId, () -> eventStatisticsManager.getFirstReservationConfirmedTimestamp(event.getId()), () -> ZonedDateTime.now(zoneId).minusDays(1));
var reservedFrom = parseDate(f, zoneId, () -> eventStatisticsManager.getFirstReservationCreatedTimestamp(event.getId()), () -> ZonedDateTime.now(zoneId).minusDays(1));
var to = parseDate(t, zoneId, Optional::empty, () -> ZonedDateTime.now(zoneId)).plusDays(1L);

boolean weeksGranularity = ChronoUnit.MONTHS.between(from, to) > 3;
var ticketSoldStatistics = eventStatisticsManager.getTicketSoldStatistics(eventId, from, to, weeksGranularity);
var ticketReservedStatistics = eventStatisticsManager.getTicketReservedStatistics(eventId, reservedFrom, to, weeksGranularity);
return new TicketsStatistics(weeksGranularity ? "week" : "day", ticketSoldStatistics, ticketReservedStatistics);
}));
}

private ZonedDateTime parseDate(String dateToParse,
ZoneId zoneId,
Supplier<Optional<ZonedDateTime>> dateLoader,
Supplier<ZonedDateTime> orElseGet) {
var dateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");
return Optional.ofNullable(dateToParse).map(p -> LocalDate.parse(p, dateFormatter).atTime(23, 59, 59).atZone(zoneId))
.or(dateLoader)
.map(z -> z.withZoneSameInstant(zoneId))
.orElseGet(orElseGet)
.truncatedTo(ChronoUnit.DAYS);
}

@DeleteMapping("/events/{eventName}/reservation/{reservationId}/transaction/{transactionId}/discard")
Expand All @@ -668,6 +688,7 @@ private Event loadEvent(String eventName, Principal principal) {

@Data
static class TicketsStatistics {
private final String granularity;
private final List<TicketsByDateStatistic> sold;
private final List<TicketsByDateStatistic> reserved;
}
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/alfio/manager/EventStatisticsManager.java
Expand Up @@ -31,6 +31,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
Expand Down Expand Up @@ -151,12 +152,20 @@ public Predicate<EventAndOrganizationId> noSeatsAvailable() {
};
}

public List<TicketsByDateStatistic> getTicketSoldStatistics(int eventId, Date from, Date to) {
return ticketReservationRepository.getSoldStatistic(eventId, from, to);
public Optional<ZonedDateTime> getFirstReservationConfirmedTimestamp(int eventId) {
return ticketReservationRepository.getFirstConfirmationTimestampForEvent(eventId);
}

public List<TicketsByDateStatistic> getTicketReservedStatistics(int eventId, Date from, Date to) {
return ticketReservationRepository.getReservedStatistic(eventId, from, to);
public Optional<ZonedDateTime> getFirstReservationCreatedTimestamp(int eventId) {
return ticketReservationRepository.getFirstReservationCreatedTimestampForEvent(eventId);
}

public List<TicketsByDateStatistic> getTicketSoldStatistics(int eventId, ZonedDateTime from, ZonedDateTime to, boolean weeksGranularity) {
return ticketReservationRepository.getSoldStatistic(eventId, from, to, weeksGranularity ? "week" : "day");
}

public List<TicketsByDateStatistic> getTicketReservedStatistics(int eventId, ZonedDateTime from, ZonedDateTime to, boolean weeksGranularity) {
return ticketReservationRepository.getReservedStatistic(eventId, from, to, weeksGranularity ? "week" : "day");
}


Expand Down
35 changes: 19 additions & 16 deletions src/main/java/alfio/repository/TicketReservationRepository.java
Expand Up @@ -151,22 +151,25 @@ int updateBillingData(@Bind("vatStatus") PriceContainer.VatStatus vatStatus,
@Bind("reservationId") String reservationId);


@Query("select count(ticket.id) ticket_count, to_char(date_trunc('day', confirmation_ts), 'YYYY-MM-DD') as day from ticket " +
"inner join tickets_reservation on tickets_reservation_id = tickets_reservation.id where " +
"ticket.event_id = :eventId and " +
"confirmation_ts is not null and " +
"confirmation_ts >= :from and " +
"confirmation_ts <= :to group by day order by day asc")
List<TicketsByDateStatistic> getSoldStatistic(@Bind("eventId") int eventId, @Bind("from") Date from, @Bind("to") Date to);

@Query("select count(ticket.id) ticket_count, to_char(date_trunc('day', tickets_reservation.creation_ts), 'YYYY-MM-DD') as day from ticket " +
"inner join tickets_reservation on tickets_reservation_id = tickets_reservation.id where " +
"ticket.event_id = :eventId and " +
"tickets_reservation.status in ('IN_PAYMENT', 'EXTERNAL_PROCESSING_PAYMENT', 'OFFLINE_PAYMENT', 'DEFERRED_OFFLINE_PAYMENT', 'COMPLETE', 'STUCK') and " +
"tickets_reservation.creation_ts >= :from and " +
"tickets_reservation.creation_ts <= :to " +
"group by day order by day asc")
List<TicketsByDateStatistic> getReservedStatistic(@Bind("eventId") int eventId, @Bind("from") Date from, @Bind("to") Date to);
@Query("select min(confirmation_ts) from tickets_reservation where event_id_fk = :eventId and confirmation_ts is not null")
Optional<ZonedDateTime> getFirstConfirmationTimestampForEvent(@Bind("eventId") int eventId);

@Query("select min(creation_ts) from tickets_reservation where event_id_fk = :eventId")
Optional<ZonedDateTime> getFirstReservationCreatedTimestampForEvent(@Bind("eventId") int eventId);

@Query("select to_char(date_trunc(:granularity, d.day), 'YYYY-MM-DD') as day, count(ticket.id) ticket_count " +
" from (select generate_series(lower(r), upper(r), '1 day')::date as day, generate_series(lower(r), upper(r), '1 day')::timestamp as ts from tsrange(:fromDate::timestamp, :toDate::timestamp) r) as d " +
" left join (select id, confirmation_ts from tickets_reservation where confirmation_ts is not null and event_id_fk = :eventId) res on date_trunc('day', res.confirmation_ts) = d.day" +
" left join ticket on event_id = :eventId and tickets_reservation_id = res.id"+
" group by 1 order by 1")
List<TicketsByDateStatistic> getSoldStatistic(@Bind("eventId") int eventId, @Bind("fromDate") ZonedDateTime from, @Bind("toDate") ZonedDateTime to, @Bind("granularity") String granularity);

@Query("select to_char(date_trunc(:granularity, d.day), 'YYYY-MM-DD') as day, count(ticket.id) ticket_count " +
" from (select generate_series(lower(r), upper(r), '1 day')::date as day, generate_series(lower(r), upper(r), '1 day')::timestamp as ts from tsrange(:fromDate::timestamp, :toDate::timestamp) r) as d " +
" left join (select id, creation_ts from tickets_reservation where event_id_fk = :eventId and status in ('IN_PAYMENT', 'EXTERNAL_PROCESSING_PAYMENT', 'OFFLINE_PAYMENT', 'DEFERRED_OFFLINE_PAYMENT', 'COMPLETE', 'STUCK')) res on date_trunc('day', res.creation_ts) = d.day " +
" left join ticket on event_id = :eventId and tickets_reservation_id = res.id"+
" group by 1 order by 1")
List<TicketsByDateStatistic> getReservedStatistic(@Bind("eventId") int eventId, @Bind("fromDate") ZonedDateTime from, @Bind("toDate") ZonedDateTime to, @Bind("granularity") String granularity);



Expand Down
Expand Up @@ -141,6 +141,7 @@
labels: !mobileView ? _.map(labels, function(l) {return moment(l).format('MMM D')}) : [],
series: [reservedSeries, soldSeries]
}, {
seriesBarDistance: 10,
ignoreEmptyValues:true,
showLabels: false,
fullWidth: true,
Expand Down Expand Up @@ -197,9 +198,7 @@
showLabels: false,
fullWidth: true,
showArea: false,
lineSmooth: Chartist.Interpolation.cardinal({
fillHoles: true
}),
lineSmooth: false,
chartPadding: {
right: 10
},
Expand Down Expand Up @@ -254,13 +253,27 @@
split: function(stats) {
var soldByDay = stats.sold;
var reservedByDay = stats.reserved;
var weeks = stats.granularity === 'week';
var labels = _.chain(soldByDay)
.concat(reservedByDay)
.map('date')
.sortBy()
.uniq(true)
.value();

function labelText(date) {
if(weeks) {
// if granularity is "week", date will be the first day of the week.
var end = moment.min(moment(date).add(6, 'days'), moment());
var differentMonth = end.month() !== date.month();
var differentYear = end.year() !== date.year();
var startFormat = 'MMM Do' + (differentYear ? ' YYYY': '');
var endFormat = (differentMonth ? 'MMM ': '') + 'Do YYYY';
return date.format(startFormat) + ' - ' + end.format(endFormat);
}
return date.format('MMM Do YYYY');
}

function generateSeries(byDay, labels) {
return _.map(labels, function(label) {
var stat = _.find(byDay, function(d) { return d.date === label; });
Expand All @@ -271,8 +284,8 @@
var mDate = moment.utc(label);
return {
value: count,
name: mDate.format('MMM Do YYYY'),
meta: mDate.format('MMM Do YYYY')
name: labelText(mDate),
meta: labelText(mDate)
};
});
}
Expand Down
Expand Up @@ -193,14 +193,17 @@ public void testTicketSelection() {

assertEquals(1, ticketReservationManager.getPendingPayments(event.getShortName()).size());

Date now = new Date();
Date from = DateUtils.addDays(now, -1);
Date to = DateUtils.addDays(now, 1);
var from = ZonedDateTime.now(event.getZoneId()).minusDays(1);
var to = ZonedDateTime.now(event.getZoneId()).plusDays(1);

assertTrue(ticketReservationRepository.getSoldStatistic(event.getId(), from, to).isEmpty()); // -> no reservations
assertTrue(ticketReservationRepository.getSoldStatistic(event.getId(), from, to, "day").stream().allMatch(tds -> tds.getCount() == 0L)); // -> no reservations
ticketReservationManager.validateAndConfirmOfflinePayment(reservationId, event, new BigDecimal("190.00"), eventAndUsername.getValue());

assertEquals(19, ticketReservationRepository.getSoldStatistic(event.getId(), from, to).get(0).getCount()); // -> 19 tickets reserved
var soldStatisticsList = ticketReservationRepository.getSoldStatistic(event.getId(), from, to, "day");
assertEquals(3, soldStatisticsList.size());
assertEquals(LocalDate.now().toString(), soldStatisticsList.get(1).getDate());
assertEquals(19L, soldStatisticsList.get(1).getCount()); // -> 19 tickets reserved
assertEquals(19L, soldStatisticsList.stream().mapToLong(TicketsByDateStatistic::getCount).sum());

assertEquals(10, eventStatisticsManager.loadModifiedTickets(event.getId(), bounded.getId(), 0, null).size());
assertEquals(Integer.valueOf(10), eventStatisticsManager.countModifiedTicket(event.getId(), bounded.getId(), null));
Expand Down

0 comments on commit 4e42708

Please sign in to comment.