Skip to content

Commit

Permalink
retry failed reservation
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Mar 17, 2023
1 parent 1092c24 commit a4a2591
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 55 deletions.
15 changes: 9 additions & 6 deletions src/main/java/alfio/config/DataSourceConfiguration.java
Expand Up @@ -22,10 +22,7 @@
import alfio.config.support.PlatformProvider;
import alfio.extension.ExtensionService;
import alfio.job.Jobs;
import alfio.job.executor.AssignTicketToSubscriberJobExecutor;
import alfio.job.executor.BillingDocumentJobExecutor;
import alfio.job.executor.ReservationJobExecutor;
import alfio.job.executor.RetryFailedExtensionJobExecutor;
import alfio.job.executor.*;
import alfio.manager.*;
import alfio.manager.i18n.MessageSourceManager;
import alfio.manager.system.AdminJobManager;
Expand Down Expand Up @@ -252,9 +249,10 @@ AdminJobManager adminJobManager(AdminJobQueueRepository adminJobQueueRepository,
ReservationJobExecutor reservationJobExecutor,
BillingDocumentJobExecutor billingDocumentJobExecutor,
AssignTicketToSubscriberJobExecutor assignTicketToSubscriberJobExecutor,
RetryFailedExtensionJobExecutor retryFailedExtensionJobExecutor) {
RetryFailedExtensionJobExecutor retryFailedExtensionJobExecutor,
RetryFailedReservationConfirmationExecutor retryFailedReservationConfirmationExecutor) {
return new AdminJobManager(
List.of(reservationJobExecutor, billingDocumentJobExecutor, assignTicketToSubscriberJobExecutor, retryFailedExtensionJobExecutor),
List.of(reservationJobExecutor, billingDocumentJobExecutor, assignTicketToSubscriberJobExecutor, retryFailedExtensionJobExecutor, retryFailedReservationConfirmationExecutor),
adminJobQueueRepository,
transactionManager,
clockProvider);
Expand Down Expand Up @@ -294,6 +292,11 @@ RetryFailedExtensionJobExecutor retryFailedExtensionJobExecutor(ExtensionService
return new RetryFailedExtensionJobExecutor(extensionService);
}

@Bean
RetryFailedReservationConfirmationExecutor retryFailedReservationConfirmationExecutor(ReservationFinalizer reservationFinalizer, Json json) {
return new RetryFailedReservationConfirmationExecutor(reservationFinalizer, json);
}

@Bean
@Profile(Initializer.PROFILE_DEMO)
DemoModeDataManager demoModeDataManager(UserRepository userRepository,
Expand Down
@@ -0,0 +1,51 @@
/**
* 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.job.executor;

import alfio.manager.ReservationFinalizer;
import alfio.manager.support.RetryFinalizeReservation;
import alfio.manager.system.AdminJobExecutor;
import alfio.model.system.AdminJobSchedule;
import alfio.util.Json;

import java.util.EnumSet;
import java.util.Set;

public class RetryFailedReservationConfirmationExecutor implements AdminJobExecutor {

private final ReservationFinalizer reservationFinalizer;
private final Json json;

public RetryFailedReservationConfirmationExecutor(ReservationFinalizer reservationFinalizer,
Json json) {
this.reservationFinalizer = reservationFinalizer;
this.json = json;
}

@Override
public Set<JobName> getJobNames() {
return EnumSet.of(JobName.RETRY_RESERVATION_CONFIRMATION);
}

@Override
public String process(AdminJobSchedule schedule) {
var metadata = schedule.getMetadata();
var retryFinalizeReservation = (String) metadata.get("payload");
reservationFinalizer.retryFinalizeReservation(json.fromJsonString(retryFinalizeReservation, RetryFinalizeReservation.class));
return null;
}
}
40 changes: 32 additions & 8 deletions src/main/java/alfio/manager/ReservationFinalizer.java
Expand Up @@ -19,6 +19,7 @@
import alfio.manager.payment.PaymentSpecification;
import alfio.manager.support.FeeCalculator;
import alfio.manager.support.IncompatibleStateException;
import alfio.manager.support.RetryFinalizeReservation;
import alfio.manager.support.reservation.OrderSummaryGenerator;
import alfio.manager.support.reservation.ReservationAuditingHelper;
import alfio.manager.support.reservation.ReservationCostCalculator;
Expand All @@ -38,6 +39,7 @@
import alfio.repository.system.AdminJobQueueRepository;
import alfio.repository.user.UserRepository;
import alfio.util.ClockProvider;
import alfio.util.Json;
import alfio.util.LocaleUtil;
import alfio.util.ReservationUtil;
import org.apache.commons.collections4.CollectionUtils;
Expand Down Expand Up @@ -93,6 +95,8 @@ public class ReservationFinalizer {
private final ReservationEmailContentHelper reservationHelper;
private final TransactionRepository transactionRepository;
private final AdminJobQueueRepository adminJobQueueRepository;
private final PurchaseContextManager purchaseContextManager;
private final Json json;


public ReservationFinalizer(PlatformTransactionManager transactionManager,
Expand All @@ -113,7 +117,9 @@ public ReservationFinalizer(PlatformTransactionManager transactionManager,
AdditionalServiceItemRepository additionalServiceItemRepository,
OrderSummaryGenerator orderSummaryGenerator,
TransactionRepository transactionRepository,
AdminJobQueueRepository adminJobQueueRepository) {
AdminJobQueueRepository adminJobQueueRepository,
PurchaseContextManager purchaseContextManager,
Json json) {
this.ticketReservationRepository = ticketReservationRepository;
this.userRepository = userRepository;
this.extensionManager = extensionManager;
Expand All @@ -136,14 +142,32 @@ public ReservationFinalizer(PlatformTransactionManager transactionManager,
this.reservationHelper = reservationEmailContentHelper;
this.auditingHelper = new ReservationAuditingHelper(auditingRepository);
this.adminJobQueueRepository = adminJobQueueRepository;
this.purchaseContextManager = purchaseContextManager;
this.json = json;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void finalizeCommandReceived(FinalizeReservation finalizeReservation) {
transactionTemplate.executeWithoutResult(ctx -> processFinalizeReservation(finalizeReservation, ctx));
transactionTemplate.executeWithoutResult(ctx -> processFinalizeReservation(finalizeReservation, ctx, true));
}

private void processFinalizeReservation(FinalizeReservation finalizeReservation, TransactionStatus ctx) {
public void retryFinalizeReservation(RetryFinalizeReservation retryFinalizeReservation) {
var purchaseContextAndReservation = purchaseContextManager.getReservationWithPurchaseContext(retryFinalizeReservation.getReservationId()).orElseThrow();
var reservation = purchaseContextAndReservation.getRight();
var purchaseContext = purchaseContextAndReservation.getLeft();
var costResult = reservationCostCalculator.totalReservationCostWithVAT(reservation);
var totalPrice = costResult.getLeft();
var orderSummary = orderSummaryGenerator.orderSummaryForReservation(reservation, purchaseContext);

/*String paymentToken = null;
if (totalPrice.requiresPayment()) {
paymentToken = transactionRepository.loadByReservationId(retryFinalizeReservation.getReservationId()).getTransactionId();
}*/
var paymentSpecification = new PaymentSpecification(reservation, totalPrice, purchaseContext, null, orderSummary, retryFinalizeReservation.isTcAccepted(), retryFinalizeReservation.isPrivacyPolicyAccepted());
transactionTemplate.executeWithoutResult(ctx -> processFinalizeReservation(new FinalizeReservation(paymentSpecification, retryFinalizeReservation.getPaymentProxy(), retryFinalizeReservation.isSendReservationConfirmationEmail(), retryFinalizeReservation.isSendTickets(), retryFinalizeReservation.getUsername()), ctx, false));
}

private void processFinalizeReservation(FinalizeReservation finalizeReservation, TransactionStatus ctx, boolean scheduleRetryOnError) {
Object savepoint = ctx.createSavepoint();
var spec = finalizeReservation.getPaymentSpecification();
try {
Expand All @@ -168,12 +192,12 @@ private void processFinalizeReservation(FinalizeReservation finalizeReservation,
completeReservation(spec, finalizeReservation.getPaymentProxy(), finalizeReservation.isSendReservationConfirmationEmail(), finalizeReservation.isSendTickets(), finalizeReservation.getUsername());
} catch(Exception e) {
ctx.rollbackToSavepoint(savepoint);
if (!scheduleRetryOnError) {
throw e;
}
boolean scheduled = AdminJobManager.executionScheduler(
RETRY_RESERVATION_CONFIRMATION,
Map.ofEntries(
Map.entry("reservationId", finalizeReservation.getPaymentSpecification().getReservationId()),
Map.entry("username", StringUtils.trimToEmpty(finalizeReservation.getUsername()))
),
Map.of("payload", json.asJsonString(RetryFinalizeReservation.fromFinalizeReservation(finalizeReservation))),
ZonedDateTime.now(clockProvider.getClock()).plusSeconds(2L)
).apply(adminJobQueueRepository);
if(!scheduled) {
Expand Down Expand Up @@ -395,6 +419,7 @@ public void confirmOfflinePayment(Event event, String reservationId, String user

auditingRepository.insert(reservationId, userRepository.findIdByUserName(username).orElse(null), event.getId(), Audit.EventType.RESERVATION_OFFLINE_PAYMENT_CONFIRMED, new Date(), Audit.EntityType.RESERVATION, ticketReservation.getId());

ticketReservationRepository.setMetadata(reservationId, metadata.withFinalized(true));
CustomerName customerName = new CustomerName(ticketReservation.getFullName(), ticketReservation.getFirstName(), ticketReservation.getLastName(), event.mustUseFirstAndLastName());
acquireItems(PaymentProxy.OFFLINE, reservationId, ticketReservation.getEmail(), customerName,
ticketReservation.getUserLanguage(), ticketReservation.getBillingAddress(),
Expand All @@ -403,7 +428,6 @@ public void confirmOfflinePayment(Event event, String reservationId, String user
Locale language = ReservationUtil.getReservationLocale(ticketReservation);
final TicketReservation finalReservation = ticketReservationRepository.findReservationById(reservationId);
billingDocumentManager.createBillingDocument(event, finalReservation, username, orderSummaryGenerator.orderSummaryForReservation(finalReservation, event));
ticketReservationRepository.setMetadata(reservationId, metadata.withFinalized(true));
var configuration = configurationManager.getFor(EnumSet.of(DEFERRED_BANK_TRANSFER_ENABLED, DEFERRED_BANK_TRANSFER_SEND_CONFIRMATION_EMAIL), ConfigurationLevel.event(event));
if(!configuration.get(DEFERRED_BANK_TRANSFER_ENABLED).getValueAsBooleanOrDefault() || configuration.get(DEFERRED_BANK_TRANSFER_SEND_CONFIRMATION_EMAIL).getValueAsBooleanOrDefault()) {
reservationHelper.sendConfirmationEmail(event, findById(reservationId).orElseThrow(IllegalArgumentException::new), language, username);
Expand Down
89 changes: 89 additions & 0 deletions src/main/java/alfio/manager/support/RetryFinalizeReservation.java
@@ -0,0 +1,89 @@
/**
* 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.support;

import alfio.model.system.command.FinalizeReservation;
import alfio.model.transaction.PaymentProxy;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class RetryFinalizeReservation {

private final String reservationId;
private final PaymentProxy paymentProxy;
private final boolean sendReservationConfirmationEmail;
private final boolean sendTickets;
private final String username;
private final boolean tcAccepted;
private final boolean privacyPolicyAccepted;

@JsonCreator
public RetryFinalizeReservation(@JsonProperty("reservationId") String reservationId,
@JsonProperty("paymentProxy") PaymentProxy paymentProxy,
@JsonProperty("sendReservationConfirmationEmail") boolean sendReservationConfirmationEmail,
@JsonProperty("sendTickets") boolean sendTickets,
@JsonProperty("username") String username,
@JsonProperty("tcAccepted") boolean tcAccepted,
@JsonProperty("privacyPolicyAccepted") boolean privacyPolicyAccepted) {
this.reservationId = reservationId;
this.paymentProxy = paymentProxy;
this.sendReservationConfirmationEmail = sendReservationConfirmationEmail;
this.sendTickets = sendTickets;
this.username = username;
this.tcAccepted = tcAccepted;
this.privacyPolicyAccepted = privacyPolicyAccepted;
}

public String getReservationId() {
return reservationId;
}

public PaymentProxy getPaymentProxy() {
return paymentProxy;
}

public boolean isSendReservationConfirmationEmail() {
return sendReservationConfirmationEmail;
}

public boolean isSendTickets() {
return sendTickets;
}

public String getUsername() {
return username;
}

public boolean isTcAccepted() {
return tcAccepted;
}

public boolean isPrivacyPolicyAccepted() {
return privacyPolicyAccepted;
}

public static RetryFinalizeReservation fromFinalizeReservation(FinalizeReservation finalizeReservation) {
var paymentSpecification = finalizeReservation.getPaymentSpecification();
return new RetryFinalizeReservation(paymentSpecification.getReservationId(),
finalizeReservation.getPaymentProxy(),
finalizeReservation.isSendReservationConfirmationEmail(),
finalizeReservation.isSendTickets(),
finalizeReservation.getUsername(),
paymentSpecification.isTcAccepted(),
paymentSpecification.isPrivacyAccepted());
}
}
17 changes: 0 additions & 17 deletions src/main/java/alfio/manager/system/AdminJobManager.java
Expand Up @@ -25,7 +25,6 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -75,20 +74,6 @@ public AdminJobManager(List<AdminJobExecutor> jobExecutors,
this.clockProvider = clockProvider;
}

@Scheduled(fixedDelay = 1000L)
void processPendingExtensionRetry() {
log.trace("Processing pending extensions retry");
processPendingExtensionRetry(ZonedDateTime.now(clockProvider.getClock()));
log.trace("done processing pending extensions retry");
}

@Scheduled(fixedDelay = 1000L)
void processPendingReservationsRetry() {
log.trace("Processing pending reservations retry");
processPendingReservationsRetry(ZonedDateTime.now(clockProvider.getClock()));
log.trace("done processing pending reservations retry");
}

// internal method invoked by tests
void processPendingExtensionRetry(ZonedDateTime timestamp) {
internalProcessPendingSchedules(adminJobQueueRepository.loadPendingSchedules(EXTENSIONS_JOB, timestamp));
Expand All @@ -98,7 +83,6 @@ void processPendingReservationsRetry(ZonedDateTime timestamp) {
internalProcessPendingSchedules(adminJobQueueRepository.loadPendingSchedules(RESERVATIONS_JOB, timestamp));
}

@Scheduled(fixedDelay = 60 * 1000)
void processPendingRequests() {
log.trace("Processing pending requests");
internalProcessPendingSchedules(adminJobQueueRepository.loadPendingSchedules(ADMIN_JOBS, ZonedDateTime.now(clockProvider.getClock())));
Expand Down Expand Up @@ -137,7 +121,6 @@ static ZonedDateTime getNextExecution(int currentAttempt) {
.plusSeconds((long) Math.pow(2, currentAttempt + 1D));
}

@Scheduled(cron = "#{environment.acceptsProfiles('dev') ? '0 * * * * *' : '0 0 0 * * *'}")
void cleanupExpiredRequests() {
log.trace("Cleanup expired requests");
ZonedDateTime now = ZonedDateTime.now(clockProvider.getClock());
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/alfio/manager/system/AdminJobManagerScheduler.java
@@ -0,0 +1,68 @@
/**
* 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.system;

import alfio.config.Initializer;
import alfio.util.ClockProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;

@Component
@Profile("!" + Initializer.PROFILE_DISABLE_JOBS)
public class AdminJobManagerScheduler {

private static final Logger log = LoggerFactory.getLogger(AdminJobManagerScheduler.class);
private final AdminJobManager adminJobManager;
private final ClockProvider clockProvider;

public AdminJobManagerScheduler(AdminJobManager adminJobManager,
ClockProvider clockProvider) {
this.adminJobManager = adminJobManager;
this.clockProvider = clockProvider;
}

@Scheduled(fixedDelay = 1000L)
void processPendingExtensionRetry() {
log.trace("Processing pending extensions retry");
adminJobManager.processPendingExtensionRetry(ZonedDateTime.now(clockProvider.getClock()));
log.trace("done processing pending extensions retry");
}

@Scheduled(fixedDelay = 1000L)
void processPendingReservationsRetry() {
log.trace("Processing pending reservations retry");
adminJobManager.processPendingReservationsRetry(ZonedDateTime.now(clockProvider.getClock()));
log.trace("done processing pending reservations retry");
}

@Scheduled(fixedDelay = 60 * 1000)
void processPendingRequests() {
log.trace("Processing pending requests");
adminJobManager.processPendingRequests();
log.trace("done processing pending requests");
}

@Scheduled(cron = "#{environment.acceptsProfiles('dev') ? '0 * * * * *' : '0 0 0 * * *'}")
void cleanupExpiredRequests() {
adminJobManager.cleanupExpiredRequests();
}
}

0 comments on commit a4a2591

Please sign in to comment.