Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline external payment #854

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,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 @@ -111,6 +111,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