Skip to content

Commit

Permalink
Fix latest issues (security) 28/12 - 16/1 (#5)
Browse files Browse the repository at this point in the history
* fix subscription type detection

* alfio-event#576 generate invoice number only when the payment gateway replies with a successful status

* alfio-event#571 rename method

* use text-indent instead of padding for better cross-browser support

* alfio-event#576 - use 10-digit scale for decimals while calculating VAT

* [Gradle Release Plugin] - pre tag commit:  '1.16.2'.

* [Gradle Release Plugin] - new version commit:  '1.17-SNAPSHOT'.

* update ticket uuid when transitioning to RELEASED

* [Gradle Release Plugin] - pre tag commit:  '1.16.3'.

* [Gradle Release Plugin] - new version commit:  '1.17-SNAPSHOT'.
  • Loading branch information
Nacho Cougil authored Jan 17, 2019
1 parent dcd0328 commit 0f024ad
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 70 deletions.
73 changes: 44 additions & 29 deletions src/main/java/alfio/manager/TicketReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -379,28 +379,13 @@ public PaymentResult confirm(String gatewayToken, String payerId, Event event, S
try {
PaymentResult paymentResult;
ticketReservationRepository.lockReservationForUpdate(reservationId);
//save billing data in case we have to go back to PENDING
ticketReservationRepository.updateBillingData(vatStatus, vatNr, vatCountryCode, invoiceRequested, reservationId);
if(isDiscountCodeUsageExceeded(reservationId)) {
return PaymentResult.unsuccessful(ErrorsCode.STEP_2_DISCOUNT_CODE_USAGE_EXCEEDED);
}
if(reservationCost.getPriceWithVAT() > 0) {
if(invoiceRequested && configurationManager.hasAllConfigurationsForInvoice(event)) {
int invoiceSequence = invoiceSequencesRepository.lockReservationForUpdate(event.getOrganizationId());
invoiceSequencesRepository.incrementSequenceFor(event.getOrganizationId());
String pattern = configurationManager.getStringConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.INVOICE_NUMBER_PATTERN), "%d");
ticketReservationRepository.setInvoiceNumber(reservationId, String.format(pattern, invoiceSequence));
}
ticketReservationRepository.updateBillingData(vatStatus, vatNr, vatCountryCode, invoiceRequested, reservationId);

//
extensionManager.handleInvoiceGeneration(event, reservationId,
email, customerName, userLanguage, billingAddress, customerReference,
reservationCost, invoiceRequested, vatCountryCode, vatNr, vatStatus).ifPresent(invoiceGeneration -> {
if (invoiceGeneration.getInvoiceNumber() != null) {
ticketReservationRepository.setInvoiceNumber(reservationId, invoiceGeneration.getInvoiceNumber());
}
});

//
boolean toBePaid = reservationCost.getPriceWithVAT() > 0;
if(toBePaid) {
switch(paymentProxy) {
case STRIPE:
paymentResult = paymentManager.processStripePayment(reservationId, gatewayToken, reservationCost.getPriceWithVAT(), event, email, customerName, billingAddress);
Expand Down Expand Up @@ -429,6 +414,12 @@ public PaymentResult confirm(String gatewayToken, String payerId, Event event, S
} else {
paymentResult = PaymentResult.successful(NOT_YET_PAID_TRANSACTION_ID);
}
if(!paymentResult.isSuccessful()) {
return paymentResult;
}
if(toBePaid) {
generateInvoiceNumber(event, reservationId, email, customerName, userLanguage, billingAddress, customerReference, reservationCost, invoiceRequested, vatCountryCode, vatNr, vatStatus);
}
completeReservation(event, reservationId, email, customerName, userLanguage, billingAddress, specialPriceSessionId, paymentProxy, customerReference, tcAccepted, privacyPolicyAccepted);
return paymentResult;
} catch(Exception ex) {
Expand All @@ -440,6 +431,24 @@ public PaymentResult confirm(String gatewayToken, String payerId, Event event, S

}

private void generateInvoiceNumber(Event event, String reservationId, String email, CustomerName customerName, Locale userLanguage, String billingAddress, String customerReference, TotalPrice reservationCost, boolean invoiceRequested, String vatCountryCode, String vatNr, PriceContainer.VatStatus vatStatus) {

if(!invoiceRequested || !configurationManager.hasAllConfigurationsForInvoice(event)) {
return;
}

String invoiceNumber = extensionManager.handleInvoiceGeneration(event, reservationId, email, customerName, userLanguage, billingAddress, customerReference, reservationCost, true, vatCountryCode, vatNr, vatStatus)
.flatMap(invoiceGeneration -> Optional.ofNullable(StringUtils.trimToNull(invoiceGeneration.getInvoiceNumber())))
.orElseGet(() -> {
int invoiceSequence = invoiceSequencesRepository.lockReservationForUpdate(event.getOrganizationId());
invoiceSequencesRepository.incrementSequenceFor(event.getOrganizationId());
String pattern = configurationManager.getStringConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.INVOICE_NUMBER_PATTERN), "%d");
return String.format(pattern, invoiceSequence);
});

ticketReservationRepository.setInvoiceNumber(reservationId, invoiceNumber);
}

private boolean isDiscountCodeUsageExceeded(String reservationId) {
TicketReservation reservation = ticketReservationRepository.findReservationById(reservationId);
if(reservation.getPromoCodeDiscountId() != null) {
Expand Down Expand Up @@ -872,22 +881,26 @@ private static TotalPrice totalReservationCostWithVAT(PromoCodeDiscount promoCod
Stream<Pair<AdditionalService, List<AdditionalServiceItem>>> additionalServiceItems) {

List<TicketPriceContainer> ticketPrices = tickets.stream().map(t -> TicketPriceContainer.from(t, reservationVatStatus, event, promoCodeDiscount)).collect(toList());
BigDecimal totalVAT = ticketPrices.stream().map(TicketPriceContainer::getVAT).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalDiscount = ticketPrices.stream().map(TicketPriceContainer::getAppliedDiscount).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalNET = ticketPrices.stream().map(TicketPriceContainer::getFinalPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalVAT = calcTotal(ticketPrices, PriceContainer::getRawVAT);
BigDecimal totalDiscount = calcTotal(ticketPrices, PriceContainer::getAppliedDiscount);
BigDecimal totalNET = calcTotal(ticketPrices, PriceContainer::getFinalPrice);
int discountedTickets = (int) ticketPrices.stream().filter(t -> t.getAppliedDiscount().compareTo(BigDecimal.ZERO) > 0).count();
int discountAppliedCount = discountedTickets <= 1 || promoCodeDiscount.getDiscountType() == DiscountType.FIXED_AMOUNT ? discountedTickets : 1;

List<AdditionalServiceItemPriceContainer> asPrices = additionalServiceItems
.flatMap(generateASIPriceContainers(event, null))
.collect(toList());

BigDecimal asTotalVAT = asPrices.stream().map(AdditionalServiceItemPriceContainer::getVAT).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal asTotalVAT = calcTotal(asPrices, PriceContainer::getRawVAT);
//FIXME discount is not applied to donations, as it wouldn't make sense. Must be implemented for #111
BigDecimal asTotalNET = asPrices.stream().map(AdditionalServiceItemPriceContainer::getFinalPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal asTotalNET = calcTotal(asPrices, PriceContainer::getFinalPrice);
return new TotalPrice(unitToCents(totalNET.add(asTotalNET)), unitToCents(totalVAT.add(asTotalVAT)), -(MonetaryUtil.unitToCents(totalDiscount)), discountAppliedCount);
}

private static BigDecimal calcTotal(List<? extends PriceContainer> elements, Function<? super PriceContainer, BigDecimal> operator) {
return elements.stream().map(operator).reduce(BigDecimal.ZERO, BigDecimal::add);
}

private static Function<Pair<AdditionalService, List<AdditionalServiceItem>>, Stream<? extends AdditionalServiceItemPriceContainer>> generateASIPriceContainers(Event event, PromoCodeDiscount discount) {
return p -> p.getValue().stream().map(asi -> AdditionalServiceItemPriceContainer.from(asi, p.getKey(), event, discount));
}
Expand Down Expand Up @@ -959,10 +972,10 @@ List<SummaryRow> extractSummary(String reservationId, PriceContainer.VatStatus r
.collect(Collectors.groupingBy(TicketPriceContainer::getCategoryId))
.forEach((categoryId, ticketsByCategory) -> {
final int subTotal = ticketsByCategory.stream().mapToInt(TicketPriceContainer::getSummarySrcPriceCts).sum();
final int subTotalBeforeVat = ticketsByCategory.stream().mapToInt(TicketPriceContainer::getSummaryPriceBeforeVatCts).sum();
final int subTotalBeforeVat = SummaryPriceContainer.getSummaryPriceBeforeVatCts(ticketsByCategory);
TicketPriceContainer firstTicket = ticketsByCategory.get(0);
final int ticketPriceCts = firstTicket.getSummarySrcPriceCts();
final int priceBeforeVat = firstTicket.getSummaryPriceBeforeVatCts();
final int priceBeforeVat = SummaryPriceContainer.getSummaryPriceBeforeVatCts(singletonList(firstTicket));
String categoryName = ticketCategoryRepository.getByIdAndActive(categoryId, event.getId()).getName();
summary.add(new SummaryRow(categoryName, formatCents(ticketPriceCts), formatCents(priceBeforeVat), ticketsByCategory.size(), formatCents(subTotal), formatCents(subTotalBeforeVat), subTotal, SummaryRow.SummaryType.TICKET));
});
Expand All @@ -977,8 +990,8 @@ List<SummaryRow> extractSummary(String reservationId, PriceContainer.VatStatus r
List<AdditionalServiceItemPriceContainer> prices = generateASIPriceContainers(event, null).apply(entry).collect(toList());
AdditionalServiceItemPriceContainer first = prices.get(0);
final int subtotal = prices.stream().mapToInt(AdditionalServiceItemPriceContainer::getSrcPriceCts).sum();
final int subtotalBeforeVat = prices.stream().mapToInt(AdditionalServiceItemPriceContainer::getSummaryPriceBeforeVatCts).sum();
return new SummaryRow(title.getValue(), formatCents(first.getSrcPriceCts()), formatCents(first.getSummaryPriceBeforeVatCts()), prices.size(), formatCents(subtotal), formatCents(subtotalBeforeVat), subtotal, SummaryRow.SummaryType.ADDITIONAL_SERVICE);
final int subtotalBeforeVat = SummaryPriceContainer.getSummaryPriceBeforeVatCts(prices);
return new SummaryRow(title.getValue(), formatCents(first.getSrcPriceCts()), formatCents(SummaryPriceContainer.getSummaryPriceBeforeVatCts(singletonList(first))), prices.size(), formatCents(subtotal), formatCents(subtotalBeforeVat), subtotal, SummaryRow.SummaryType.ADDITIONAL_SERVICE);
}).collect(Collectors.toList()));

Optional.ofNullable(promoCodeDiscount).ifPresent(promo -> {
Expand Down Expand Up @@ -1053,7 +1066,9 @@ private void cancelReservation(String reservationId, boolean expired) {
ticketFieldRepository.deleteAllValuesForReservations(reservationIdsToRemove);
Event event = eventRepository.findByReservationId(reservationId);
int updatedAS = additionalServiceItemRepository.updateItemsStatusWithReservationUUID(reservationId, expired ? AdditionalServiceItemStatus.EXPIRED : AdditionalServiceItemStatus.CANCELLED);
int updatedTickets = ticketRepository.findTicketsInReservation(reservationId).stream().mapToInt(t -> ticketRepository.releaseExpiredTicket(reservationId, event.getId(), t.getId())).sum();
int updatedTickets = ticketRepository.findTicketIdsInReservation(reservationId).stream().mapToInt(
tickedId -> ticketRepository.releaseExpiredTicket(reservationId, event.getId(), tickedId, UUID.randomUUID().toString())
).sum();
Validate.isTrue(updatedTickets + updatedAS > 0, "no items have been updated");
waitingQueueManager.fireReservationExpired(reservationId);
groupManager.deleteWhitelistedTicketsForReservation(reservationId);
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/alfio/manager/WaitingQueueManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ private void notifySubscription(Event event, CustomerName name, String email, Lo
private WaitingQueueSubscription.Type getSubscriptionType(Event event) {
ZonedDateTime now = ZonedDateTime.now(event.getZoneId());
return ticketCategoryRepository.findByEventId(event.getId()).stream()
.filter(tc -> now.isAfter(tc.getInception(event.getZoneId())))
.findFirst()
.filter(tc -> now.isBefore(tc.getInception(event.getZoneId())))
.map(tc -> WaitingQueueSubscription.Type.PRE_SALES)
.orElse(WaitingQueueSubscription.Type.SOLD_OUT);
.map(tc -> WaitingQueueSubscription.Type.SOLD_OUT)
.orElse(WaitingQueueSubscription.Type.PRE_SALES);
}

private void validateSubscriptionType(Event event, WaitingQueueSubscription.Type type) {
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/alfio/model/PriceContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@

public interface PriceContainer {

BiFunction<BigDecimal, BigDecimal, BigDecimal> includedVatExtractor = (price, vatPercentage) -> MonetaryUtil.extractVAT(price, vatPercentage).setScale(2, RoundingMode.HALF_UP);
BiFunction<BigDecimal, BigDecimal, BigDecimal> notIncludedVatCalculator = (price, vatPercentage) -> MonetaryUtil.calcVat(price, vatPercentage).setScale(2, RoundingMode.HALF_UP);
BiFunction<BigDecimal, BigDecimal, BigDecimal> includedVatExtractor = MonetaryUtil::extractVAT;
BiFunction<BigDecimal, BigDecimal, BigDecimal> notIncludedVatCalculator = MonetaryUtil::calcVat;

enum VatStatus {
NONE((price, vatPercentage) -> BigDecimal.ZERO, UnaryOperator.identity()),
Expand All @@ -51,6 +51,10 @@ enum VatStatus {
}

public BigDecimal extractVat(BigDecimal price, BigDecimal vatPercentage) {
return this.extractRawVAT(price, vatPercentage).setScale(2, RoundingMode.HALF_UP);
}

public BigDecimal extractRawVAT(BigDecimal price, BigDecimal vatPercentage) {
return this.extractor.andThen(transformer).apply(price, vatPercentage);
}

Expand Down Expand Up @@ -123,6 +127,18 @@ default BigDecimal getVAT() {
return getVAT(price.subtract(getAppliedDiscount()), getVatStatus(), getVatPercentageOrZero());
}


/**
* Returns the VAT, with a reasonable, less error-prone, rounding
* @return vat
* @see MonetaryUtil#ROUNDING_SCALE
*/
default BigDecimal getRawVAT() {
final BigDecimal price = MonetaryUtil.centsToUnit(getSrcPriceCts());
return getVatStatus().extractRawVAT(price.subtract(getAppliedDiscount()), getVatPercentageOrZero());
}


/**
* @return the discount applied, if any
*/
Expand Down
26 changes: 16 additions & 10 deletions src/main/java/alfio/model/SummaryPriceContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@
*/
package alfio.model;

import alfio.util.MonetaryUtil;

import java.math.BigDecimal;
import java.util.List;

import static alfio.util.MonetaryUtil.centsToUnit;
import static alfio.util.MonetaryUtil.unitToCents;

public interface SummaryPriceContainer extends PriceContainer {
Integer getVatCts();

Integer getFinalPriceCts();

default int getSummaryPriceBeforeVatCts() {
PriceContainer.VatStatus vatStatus = getVatStatus();
if(vatStatus == PriceContainer.VatStatus.NOT_INCLUDED_EXEMPT) {
return getSrcPriceCts();
} else if(vatStatus == PriceContainer.VatStatus.INCLUDED_EXEMPT) {
return getSrcPriceCts() + unitToCents(vatStatus.extractVat(centsToUnit(getSrcPriceCts()), getVatPercentageOrZero()));
}
return getFinalPriceCts() - getVatCts();
static int getSummaryPriceBeforeVatCts(List<? extends SummaryPriceContainer> elements) {
return elements.stream().map(item -> {
PriceContainer.VatStatus vatStatus = item.getVatStatus();
if(vatStatus == PriceContainer.VatStatus.NOT_INCLUDED_EXEMPT) {
return MonetaryUtil.centsToUnit(item.getSrcPriceCts());
} else if(vatStatus == PriceContainer.VatStatus.INCLUDED_EXEMPT) {
return MonetaryUtil.centsToUnit(item.getSrcPriceCts()).add(vatStatus.extractRawVAT(centsToUnit(item.getSrcPriceCts()), item.getVatPercentageOrZero()));
}
return MonetaryUtil.centsToUnit(item.getFinalPriceCts()).subtract(item.getRawVAT());
}).reduce(BigDecimal::add).map(MonetaryUtil::unitToCents).orElse(0);
}
}
5 changes: 0 additions & 5 deletions src/main/java/alfio/model/decorator/TicketPriceContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ public static TicketPriceContainer from(Ticket t, VatStatus reservationVatStatus
return new TicketPriceContainer(t, discount, e.getCurrency(), e.getVat(), vatStatus);
}

@Override
public Integer getVatCts() {
return ticket.getVatCts();
}

@Override
public Integer getFinalPriceCts() {
return ticket.getFinalPriceCts();
Expand Down
22 changes: 14 additions & 8 deletions src/main/java/alfio/repository/TicketRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@
import alfio.model.TicketInfo;
import alfio.model.TicketWithReservationAndTransaction;
import ch.digitalfondue.npjt.*;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import java.time.ZonedDateTime;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;

@QueryRepository
public interface TicketRepository {
Expand Down Expand Up @@ -124,6 +123,9 @@ public interface TicketRepository {
@Query("select * from ticket where tickets_reservation_id = :reservationId order by category_id asc, uuid asc")
List<Ticket> findTicketsInReservation(@Bind("reservationId") String reservationId);

@Query("select id from ticket where tickets_reservation_id = :reservationId order by category_id asc, uuid asc")
List<Integer> findTicketIdsInReservation(@Bind("reservationId") String reservationId);

@Query("select * from ticket where tickets_reservation_id = :reservationId order by category_id asc, uuid asc LIMIT 1 OFFSET 0")
Optional<Ticket> findFirstTicketInReservation(@Bind("reservationId") String reservationId);

Expand Down Expand Up @@ -238,11 +240,15 @@ public interface TicketRepository {
@Query(value = RELEASE_TICKET_QUERY, type = QueryType.TEMPLATE)
String batchReleaseTickets();

@Query("update ticket set status = 'RELEASED', " + RESET_TICKET + " where id = :ticketId and status = 'PENDING' and tickets_reservation_id = :reservationId and event_id = :eventId")
int releaseExpiredTicket(@Bind("reservationId") String reservationId, @Bind("eventId") int eventId, @Bind("ticketId") int ticketId);
@Query("update ticket set status = 'RELEASED', uuid = :newUuid, " + RESET_TICKET + " where id = :ticketId and status = 'PENDING' and tickets_reservation_id = :reservationId and event_id = :eventId")
int releaseExpiredTicket(@Bind("reservationId") String reservationId, @Bind("eventId") int eventId, @Bind("ticketId") int ticketId, @Bind("newUuid") String newUuid);

NamedParameterJdbcTemplate getNamedParameterJdbcTemplate();

@Query("update ticket set status = 'RELEASED', " + RESET_TICKET + " where id in (:ticketIds)")
int resetTickets(@Bind("ticketIds") List<Integer> ticketIds);
default void resetTickets(List<Integer> ticketIds) {
MapSqlParameterSource[] params = ticketIds.stream().map(ticketId -> new MapSqlParameterSource("ticketId", ticketId).addValue("newUuid", UUID.randomUUID().toString())).toArray(MapSqlParameterSource[]::new);
getNamedParameterJdbcTemplate().batchUpdate("update ticket set status = 'RELEASED', uuid = :newUuid, " + RESET_TICKET + " where id = :ticketId", params);
}

@Query("select count(*) from ticket where status = 'RELEASED' and event_id = :eventId")
Integer countWaiting(@Bind("eventId") int eventId);
Expand Down
Loading

0 comments on commit 0f024ad

Please sign in to comment.