Skip to content

Commit

Permalink
Offline external payment (#854)
Browse files Browse the repository at this point in the history
* initial implementation of DeferredBankTransferManager

* update public frontend and admin

* update italian translation
  • Loading branch information
cbellone committed Jan 8, 2020
1 parent c9f0258 commit b6565b9
Show file tree
Hide file tree
Showing 20 changed files with 333 additions and 59 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ junitVersion=5.1.0
systemProp.jdk.tls.client.protocols="TLSv1,TLSv1.1,TLSv1.2"

# https://jitpack.io/#alfio-event/alf.io-public-frontend -> go to commit tab, set the version
alfioPublicFrontendVersion=78a49b22b3
alfioPublicFrontendVersion=9cfee80051
9 changes: 4 additions & 5 deletions src/main/java/alfio/controller/IndexController.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public ResponseEntity<String> replyToK8s() {
"/event/{eventShortName}/reservation/{reservationId}/overview",
"/event/{eventShortName}/reservation/{reservationId}/waitingPayment",
"/event/{eventShortName}/reservation/{reservationId}/waiting-payment",
"/event/{eventShortName}/reservation/{reservationId}/deferred-payment",
"/event/{eventShortName}/reservation/{reservationId}/processing-payment",
"/event/{eventShortName}/reservation/{reservationId}/success",
"/event/{eventShortName}/reservation/{reservationId}/not-found",
Expand Down Expand Up @@ -171,24 +172,22 @@ public void replyToIndex(@PathVariable(value = "eventShortName", required = fals
public String redirectToReservation(@PathVariable(value = "eventShortName") String eventShortName, @PathVariable(value = "reservationId") String reservationId) {
if (eventRepository.existsByShortName(eventShortName)) {
var reservationStatusUrlSegment = ticketReservationRepository.findOptionalStatusAndValidationById(reservationId)
.map(status -> reservationStatusToUrlMapping(status)).orElse("not-found");
.map(IndexController::reservationStatusToUrlMapping).orElse("not-found");

var redirectUrl = "redirect:" + UriComponentsBuilder.fromPath("/event/{eventShortName}/reservation/{reservationId}/{status}")
return "redirect:" + UriComponentsBuilder.fromPath("/event/{eventShortName}/reservation/{reservationId}/{status}")
.buildAndExpand(Map.of("eventShortName", eventShortName, "reservationId", reservationId, "status",reservationStatusUrlSegment))
.toUriString();

return redirectUrl;
} else {
return "redirect:/";
}
}

private static String reservationStatusToUrlMapping(TicketReservationStatusAndValidation status) {
// PENDING, IN_PAYMENT, EXTERNAL_PROCESSING_PAYMENT, WAITING_EXTERNAL_CONFIRMATION, OFFLINE_PAYMENT, COMPLETE, STUCK, CANCELLED, CREDIT_NOTE_ISSUED
switch (status.getStatus()) {
case PENDING: return Boolean.TRUE.equals(status.getValidated()) ? "overview" : "book";
case COMPLETE: return "success";
case OFFLINE_PAYMENT: return "waiting-payment";
case DEFERRED_OFFLINE_PAYMENT: return "deferred-payment";
case EXTERNAL_PROCESSING_PAYMENT:
case WAITING_EXTERNAL_CONFIRMATION: return "processing-payment";
case IN_PAYMENT:
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/alfio/manager/TicketReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ public void confirmOfflinePayment(Event event, String reservationId, String user
TicketReservation ticketReservation = findById(reservationId).orElseThrow(IllegalArgumentException::new);
ticketReservationRepository.lockReservationForUpdate(reservationId);
Validate.isTrue(ticketReservation.getPaymentMethod() == PaymentProxy.OFFLINE, "invalid payment method");
Validate.isTrue(ticketReservation.getStatus() == TicketReservationStatus.OFFLINE_PAYMENT, "invalid status");
Validate.isTrue(ticketReservation.isPendingOfflinePayment(), "invalid status");


ticketReservationRepository.confirmOfflinePayment(reservationId, TicketReservationStatus.COMPLETE.name(), ZonedDateTime.now(event.getZoneId()));
Expand All @@ -675,7 +675,7 @@ public void confirmOfflinePayment(Event event, String reservationId, String user
void registerAlfioTransaction(Event event, String reservationId, PaymentProxy paymentProxy) {
var totalPrice = totalReservationCostWithVAT(reservationId);
int priceWithVAT = totalPrice.getPriceWithVAT();
Long platformFee = FeeCalculator.getCalculator(event, configurationManager, Objects.requireNonNullElse(totalPrice.getCurrencyCode(), event.getCurrency()))
long platformFee = FeeCalculator.getCalculator(event, configurationManager, Objects.requireNonNullElse(totalPrice.getCurrencyCode(), event.getCurrency()))
.apply(ticketRepository.countTicketsInReservation(reservationId), (long) priceWithVAT)
.orElse(0L);

Expand Down Expand Up @@ -1231,6 +1231,7 @@ public OrderSummary orderSummaryForReservation(TicketReservation reservation, Ev
formatCents(reservationCost.getPriceWithVAT(), currencyCode),
formatCents(reservationCost.getVAT(), currencyCode),
reservation.getStatus() == TicketReservationStatus.OFFLINE_PAYMENT,
reservation.getStatus() == DEFERRED_OFFLINE_PAYMENT,
reservation.getPaymentMethod() == PaymentProxy.ON_SITE,
Optional.ofNullable(event.getVat()).map(p -> MonetaryUtil.formatCents(MonetaryUtil.unitToCents(p, currencyCode), currencyCode)).orElse(null),
reservation.getVatStatus(),
Expand Down
24 changes: 21 additions & 3 deletions src/main/java/alfio/manager/payment/BankTransferManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import alfio.manager.system.ConfigurationLevel;
import alfio.manager.system.ConfigurationManager;
import alfio.model.Event;
import alfio.model.TicketReservation;
import alfio.model.system.ConfigurationKeys;
import alfio.model.transaction.*;
import alfio.repository.TicketReservationRepository;
Expand All @@ -35,14 +36,19 @@
import java.util.*;

import static alfio.manager.TicketReservationManager.NOT_YET_PAID_TRANSACTION_ID;
import static alfio.model.TicketReservation.TicketReservationStatus.OFFLINE_PAYMENT;
import static alfio.model.system.ConfigurationKeys.*;
import static alfio.model.system.ConfigurationKeys.BANK_TRANSFER_ENABLED;
import static java.time.ZoneOffset.UTC;

@Component
@Log4j2
@AllArgsConstructor
public class BankTransferManager implements PaymentProvider {

private static final EnumSet<ConfigurationKeys> OPTIONS_TO_LOAD = EnumSet.of(BANK_TRANSFER_ENABLED,
DEFERRED_BANK_TRANSFER_ENABLED, OFFLINE_PAYMENT_DAYS, REVOLUT_ENABLED, REVOLUT_API_KEY,
REVOLUT_LIVE_MODE, REVOLUT_MANUAL_REVIEW);
private final ConfigurationManager configurationManager;
private final TicketReservationRepository ticketReservationRepository;
private final TransactionRepository transactionRepository;
Expand All @@ -63,17 +69,25 @@ boolean bankTransferEnabled(PaymentMethod paymentMethod, PaymentContext paymentC
}

Map<ConfigurationKeys, ConfigurationManager.MaybeConfiguration> options(PaymentContext paymentContext) {
return configurationManager.getFor(EnumSet.of(BANK_TRANSFER_ENABLED, OFFLINE_PAYMENT_DAYS, REVOLUT_ENABLED, REVOLUT_API_KEY, REVOLUT_LIVE_MODE, REVOLUT_MANUAL_REVIEW), paymentContext.getConfigurationLevel());
return configurationManager.getFor(OPTIONS_TO_LOAD, paymentContext.getConfigurationLevel());
}

boolean isPaymentDeferredEnabled(Map<ConfigurationKeys, ConfigurationManager.MaybeConfiguration> options) {
return options.get(DEFERRED_BANK_TRANSFER_ENABLED).getValueAsBooleanOrDefault(false);
}

@Override
public PaymentResult doPayment(PaymentSpecification spec) {
transitionToOfflinePayment(spec);
overrideExistingTransactions(spec);
return PaymentResult.successful(NOT_YET_PAID_TRANSACTION_ID);
}

void overrideExistingTransactions(PaymentSpecification spec) {
PaymentManagerUtils.invalidateExistingTransactions(spec.getReservationId(), transactionRepository);
transactionRepository.insert(UUID.randomUUID().toString(), null,
spec.getReservationId(), ZonedDateTime.now(UTC), spec.getPriceWithVAT(), spec.getCurrencyCode(),
"", PaymentProxy.OFFLINE.name(), 0L, 0L, Transaction.Status.PENDING, Map.of());
return PaymentResult.successful(NOT_YET_PAID_TRANSACTION_ID);
}

@Override
Expand All @@ -95,7 +109,11 @@ public PaymentResult doPayment(PaymentSpecification spec) {

private void transitionToOfflinePayment(PaymentSpecification spec) {
ZonedDateTime deadline = getOfflinePaymentDeadline(spec.getPaymentContext(), configurationManager);
int updatedReservation = ticketReservationRepository.postponePayment(spec.getReservationId(), Date.from(deadline.toInstant()), spec.getEmail(),
postponePayment(spec, OFFLINE_PAYMENT, deadline);
}

void postponePayment(PaymentSpecification spec, TicketReservation.TicketReservationStatus status, ZonedDateTime deadline) {
int updatedReservation = ticketReservationRepository.postponePayment(spec.getReservationId(), status, Date.from(deadline.toInstant()), spec.getEmail(),
spec.getCustomerName().getFullName(), spec.getCustomerName().getFirstName(), spec.getCustomerName().getLastName(), spec.getBillingAddress(), spec.getCustomerReference());
Validate.isTrue(updatedReservation == 1, "expected exactly one updated reservation, got " + updatedReservation);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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.payment;

import alfio.manager.support.PaymentResult;
import alfio.manager.system.ConfigurationManager;
import alfio.model.TicketReservation;
import alfio.model.transaction.*;
import alfio.repository.TicketReservationRepository;
import alfio.repository.TransactionRepository;
import lombok.extern.log4j.Log4j2;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;
import java.util.*;

import static alfio.manager.TicketReservationManager.NOT_YET_PAID_TRANSACTION_ID;
import static alfio.model.TicketReservation.TicketReservationStatus.DEFERRED_OFFLINE_PAYMENT;
import static java.time.ZoneOffset.UTC;

@Component
@Order(0)
@Log4j2
public class DeferredBankTransferManager implements PaymentProvider {

private final TicketReservationRepository ticketReservationRepository;
private final BankTransferManager bankTransferManager;

public DeferredBankTransferManager(TicketReservationRepository ticketReservationRepository,
BankTransferManager bankTransferManager) {
this.ticketReservationRepository = ticketReservationRepository;
this.bankTransferManager = bankTransferManager;
}

@Override
public boolean accept(PaymentMethod paymentMethod, PaymentContext paymentContext) {
var options = bankTransferManager.options(paymentContext);
return paymentContext.getEvent() != null
&& bankTransferManager.bankTransferEnabled(paymentMethod, paymentContext, options)
&& bankTransferManager.isPaymentDeferredEnabled(options);
}

@Override
public PaymentResult doPayment(PaymentSpecification spec) {
bankTransferManager.postponePayment(spec, DEFERRED_OFFLINE_PAYMENT, Objects.requireNonNull(spec.getEvent()).getBegin());
bankTransferManager.overrideExistingTransactions(spec);
return PaymentResult.successful(NOT_YET_PAID_TRANSACTION_ID);
}

@Override
public Map<String, ?> getModelOptions(PaymentContext context) {
return Map.of("deferred", true);
}

@Override
public boolean accept(Transaction transaction) {
return PaymentProxy.OFFLINE == transaction.getPaymentProxy() && isReservationStatusCompatible(transaction);
}

private Boolean isReservationStatusCompatible(Transaction transaction) {
return ticketReservationRepository.findOptionalStatusAndValidationById(transaction.getReservationId())
.map(sv -> sv.getStatus() == DEFERRED_OFFLINE_PAYMENT)
.orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -59,6 +60,7 @@
import static alfio.util.EventUtil.JSON_DATETIME_FORMATTER;

@Component
@Order(1)
@Transactional
@Log4j2
@AllArgsConstructor
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/alfio/model/OrderSummary.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class OrderSummary {
private final String totalPrice;
private final String totalVAT;
private final boolean waitingForPayment;
private final boolean deferredPayment;
private final boolean cashPayment;
private final String vatPercentage;
private final PriceContainer.VatStatus vatStatus;
Expand All @@ -49,7 +50,7 @@ public boolean getCashPayment() {
}

public boolean getNotYetPaid() {
return waitingForPayment || cashPayment;
return waitingForPayment || cashPayment || deferredPayment;
}

public int getTicketAmount() {
Expand Down
16 changes: 15 additions & 1 deletion src/main/java/alfio/model/TicketReservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,16 @@
public class TicketReservation implements PriceContainer {

public enum TicketReservationStatus {
PENDING, IN_PAYMENT, EXTERNAL_PROCESSING_PAYMENT, WAITING_EXTERNAL_CONFIRMATION, OFFLINE_PAYMENT, COMPLETE, STUCK, CANCELLED, CREDIT_NOTE_ISSUED
PENDING,
IN_PAYMENT,
EXTERNAL_PROCESSING_PAYMENT,
WAITING_EXTERNAL_CONFIRMATION,
OFFLINE_PAYMENT,
DEFERRED_OFFLINE_PAYMENT,
COMPLETE,
STUCK,
CANCELLED,
CREDIT_NOTE_ISSUED
}

private final String id;
Expand Down Expand Up @@ -207,4 +216,9 @@ public String getPaidAmount() {
public Optional<BigDecimal> getOptionalVatPercentage() {
return Optional.ofNullable(usedVatPercent);
}

public boolean isPendingOfflinePayment() {
return status == TicketReservationStatus.OFFLINE_PAYMENT
|| status == TicketReservationStatus.DEFERRED_OFFLINE_PAYMENT;
}
}
1 change: 1 addition & 0 deletions src/main/java/alfio/model/system/ConfigurationKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public enum ConfigurationKeys {
SMTP_PROPERTIES("SMTP Properties", false, SettingCategory.MAIL, ComponentType.TEXTAREA, false, EnumSet.of(SYSTEM)),

BANK_TRANSFER_ENABLED("Bank transfer enabled", false, SettingCategory.PAYMENT_OFFLINE, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM, ORGANIZATION)),
DEFERRED_BANK_TRANSFER_ENABLED("Handle Bank Transfer payment externally (default false)", false, SettingCategory.PAYMENT_OFFLINE, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT)),
OFFLINE_PAYMENT_DAYS("Maximum number of days allowed to pay an offline ticket", false, SettingCategory.PAYMENT_OFFLINE, ComponentType.TEXT, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT)),
OFFLINE_REMINDER_HOURS("How many hours before expiration should be sent a reminder e-mail for offline payments?", false, SettingCategory.PAYMENT_OFFLINE, ComponentType.TEXT, false, EnumSet.of(SYSTEM, ORGANIZATION, EVENT)),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ int updateTicketReservation(@Bind("reservationId") String reservationId,
@Bind("paymentMethod") String paymentMethod,
@Bind("customerReference") String customerReference);

@Query("update tickets_reservation set validity = :validity, status = 'OFFLINE_PAYMENT', payment_method = 'OFFLINE', full_name = :fullName, first_name = :firstName," +
@Query("update tickets_reservation set validity = :validity, status = :status, payment_method = 'OFFLINE', full_name = :fullName, first_name = :firstName," +
" last_name = :lastName, email_address = :email, billing_address = :billingAddress, customer_reference = :customerReference where id = :reservationId")
int postponePayment(@Bind("reservationId") String reservationId, @Bind("validity") Date validity, @Bind("email") String email,
int postponePayment(@Bind("reservationId") String reservationId, @Bind("status") TicketReservation.TicketReservationStatus status, @Bind("validity") Date validity, @Bind("email") String email,
@Bind("fullName") String fullName, @Bind("firstName") String firstName, @Bind("lastName") String lastName,
@Bind("billingAddress") String billingAddress,
@Bind("customerReference") String customerReference);
Expand All @@ -69,7 +69,7 @@ int postponePayment(@Bind("reservationId") String reservationId, @Bind("validity
@Query("update tickets_reservation set full_name = :fullName where id = :reservationId")
int updateAssignee(@Bind("reservationId") String reservationId, @Bind("fullName") String fullName);

@Query("select count(id) from tickets_reservation where status = 'OFFLINE_PAYMENT' and event_id_fk = :eventId")
@Query("select count(id) from tickets_reservation where status in('OFFLINE_PAYMENT', 'DEFERRED_OFFLINE_PAYMENT') and event_id_fk = :eventId")
Integer findAllReservationsWaitingForPaymentCountInEventId(@Bind("eventId") int eventId);

@Query("select * from tickets_reservation where status = 'OFFLINE_PAYMENT' and date_trunc('day', validity) <= :expiration and offline_payment_reminder_sent = false for update skip locked")
Expand Down Expand Up @@ -159,7 +159,7 @@ int updateBillingData(@Bind("vatStatus") PriceContainer.VatStatus vatStatus,
@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', 'COMPLETE', 'STUCK') 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")
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/alfio/repository/TicketSearchRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ List<TicketReservation> findReservationsForEvent(@Bind("eventId") int eventId,
@Query("select distinct on(tr_id) "+RESERVATION_SEARCH_FIELD+", "+TRANSACTION_FIELDS+"," +PROMO_CODE_FIELDS+" from reservation_and_ticket_and_tx where tr_id in (:reservationIds) and tr_status = 'OFFLINE_PAYMENT' and bt_reservation_id is not null")
List<TicketReservationWithTransaction> findOfflineReservationsWithTransaction(@Bind("reservationIds") List<String> reservationIds);

@Query("select distinct on(tr_id) "+RESERVATION_SEARCH_FIELD+", "+TRANSACTION_FIELDS+"," +PROMO_CODE_FIELDS+" from reservation_and_ticket_and_tx where tr_event_id = :eventId and tr_id is not null and tr_status = 'OFFLINE_PAYMENT'")
@Query("select distinct on(tr_id) "+RESERVATION_SEARCH_FIELD+", "+TRANSACTION_FIELDS+"," +PROMO_CODE_FIELDS+" from reservation_and_ticket_and_tx where tr_event_id = :eventId and tr_id is not null and tr_status in('OFFLINE_PAYMENT', 'DEFERRED_OFFLINE_PAYMENT')")
List<TicketReservationWithTransaction> findOfflineReservationsWithOptionalTransaction(@Bind("eventId") int eventId);

@Query("select count(distinct tr_id) from (" + FIND_ALL_TICKETS_INCLUDING_NEW +" ) as d_tbl")
Expand Down
Loading

0 comments on commit b6565b9

Please sign in to comment.