Skip to content

Commit

Permalink
Refactor payment confirmation (#1202)
Browse files Browse the repository at this point in the history
* simplify RefreshableDataSource by extending DelegatingDataSource
* make sure to acquire all special price tokens before running detached confirmation
* call INVOICE_GENERATION extension after processing payment
* retry failed reservation
* introduce FINALIZING state for reservation
* update frontend version
  • Loading branch information
cbellone committed Mar 20, 2023
1 parent bbe8108 commit fb568c3
Show file tree
Hide file tree
Showing 93 changed files with 3,274 additions and 1,251 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ targetCompatibility=11
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=f895ce2a0e
alfioPublicFrontendVersion=5149b98fb9
15 changes: 9 additions & 6 deletions src/main/java/alfio/config/DataSourceConfiguration.java
Original file line number Diff line number Diff line change
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
10 changes: 8 additions & 2 deletions src/main/java/alfio/controller/IndexController.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
import alfio.manager.system.ConfigurationLevel;
import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
import alfio.model.TicketReservation.TicketReservationStatus;
import alfio.model.system.ConfigurationKeys;
import alfio.model.transaction.PaymentProxy;
import alfio.model.user.Role;
import alfio.repository.*;
import alfio.repository.user.OrganizationRepository;
Expand Down Expand Up @@ -290,8 +292,12 @@ private static Element buildScripTag(String content, String type, String id, Str
private static String reservationStatusToUrlMapping(TicketReservationStatusAndValidation status) {
switch (status.getStatus()) {
case PENDING: return Boolean.TRUE.equals(status.getValidated()) ? "overview" : "book";
case COMPLETE: return "success";
case OFFLINE_PAYMENT: return "waiting-payment";
case COMPLETE:
case FINALIZING:
return "success";
case OFFLINE_PAYMENT:
case OFFLINE_FINALIZING:
return "waiting-payment";
case DEFERRED_OFFLINE_PAYMENT: return "deferred-payment";
case EXTERNAL_PROCESSING_PAYMENT:
case WAITING_EXTERNAL_CONFIRMATION: return "processing-payment";
Expand Down
7 changes: 2 additions & 5 deletions src/main/java/alfio/controller/api/support/TicketHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
import alfio.model.user.Organization;
import alfio.repository.*;
import alfio.repository.user.OrganizationRepository;
import alfio.util.EventUtil;
import alfio.util.LocaleUtil;
import alfio.util.TemplateManager;
import alfio.util.Validator;
import alfio.util.*;
import alfio.util.Validator.AdvancedTicketAssignmentValidator;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -217,7 +214,7 @@ private void updateTicketOwner(UpdateTicketOwnerForm updateTicketOwner, Locale f

private PartialTicketTextGenerator getOwnerChangeTextBuilder(Locale ticketLanguage, Ticket t, Event event) {
Organization organization = organizationRepository.getById(event.getOrganizationId());
String ticketUrl = ticketReservationManager.ticketUpdateUrl(event, t.getUuid());
String ticketUrl = ReservationUtil.ticketUpdateUrl(event, t, configurationManager);
return TemplateProcessor.buildEmailForOwnerChange(event, t, organization, ticketUrl, templateManager, ticketLanguage);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private CreationResponse postCreate(ReservationAPICreationRequest creationReques
ticketReservationManager.setReservationOwner(id, user.getUsername(), user.getEmail(), user.getFirstName(), user.getLastName(), locale.getLanguage());
}
if(creationRequest.getReservationConfiguration() != null) {
ticketReservationManager.setReservationMetadata(id, new ReservationMetadata(creationRequest.getReservationConfiguration().isHideContactData()));
ticketReservationManager.setReservationMetadata(id, new ReservationMetadata(creationRequest.getReservationConfiguration().isHideContactData(), false, false));
}
var subscriptionId = creationRequest instanceof TicketReservationCreationRequest ? ((TicketReservationCreationRequest) creationRequest).getSubscriptionId() : null;
return CreationResponse.success(id, ticketReservationManager.reservationUrlForExternalClients(id, purchaseContext, locale.getLanguage(), user != null, subscriptionId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public ResponseEntity<ReservationInfo> getReservationInfo(@PathVariable("reserva

var additionalInfo = ticketReservationRepository.getAdditionalInfo(reservationId);

var shortReservationId = ticketReservationManager.getShortReservationID(purchaseContext, reservation);
var shortReservationId = configurationManager.getShortReservationID(purchaseContext, reservation);
//


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import alfio.manager.*;
import alfio.manager.i18n.MessageSourceManager;
import alfio.manager.support.response.ValidatedResponse;
import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
import alfio.model.transaction.PaymentProxy;
import alfio.model.user.Organization;
Expand Down Expand Up @@ -77,6 +78,7 @@ public class TicketApiV2Controller {
private final BookingInfoTicketLoader bookingInfoTicketLoader;
private final TicketRepository ticketRepository;
private final SubscriptionManager subscriptionManager;
private final ConfigurationManager configurationManager;


@GetMapping(value = {
Expand Down Expand Up @@ -119,7 +121,7 @@ public void generateTicketPdf(@PathVariable("eventName") String eventName,
try (OutputStream os = response.getOutputStream()) {
TicketCategory ticketCategory = ticketCategoryRepository.getByIdAndActive(ticket.getCategoryId(), event.getId());
Organization organization = organizationRepository.getById(event.getOrganizationId());
String reservationID = ticketReservationManager.getShortReservationID(event, ticketReservation);
String reservationID = configurationManager.getShortReservationID(event, ticketReservation);
var ticketWithMetadata = TicketWithMetadataAttributes.build(ticket, ticketRepository.getTicketMetadata(ticket.getId()));
var locale = LocaleUtil.getTicketLanguage(ticket, LocaleUtil.forLanguageTag(ticketReservation.getUserLanguage(), event));
TemplateProcessor.renderPDFTicket(
Expand Down Expand Up @@ -232,7 +234,7 @@ public ResponseEntity<TicketInfo> getTicketInfo(@PathVariable("eventName") Strin
ticket.getUuid(),
ticketCategory.getName(),
ticketReservation.getFullName(),
ticketReservationManager.getShortReservationID(event, ticketReservation),
configurationManager.getShortReservationID(event, ticketReservation),
deskPaymentRequired,
event.getTimeZone(),
DatesWithTimeZoneOffset.fromEvent(event),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public Object get(String name, Scriptable start) {
throw new OutOfBoundariesException("Out of boundaries class use.");
}

if (map.get(name) == null) {
// prevent NPE on Rhino when map has an explicit null value for a given key
return null;
}

return super.get(name, start);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
30 changes: 17 additions & 13 deletions src/main/java/alfio/manager/AdminReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
import alfio.controller.support.TemplateProcessor;
import alfio.manager.i18n.MessageSourceManager;
import alfio.manager.payment.PaymentSpecification;
import alfio.manager.support.IncompatibleStateException;
import alfio.manager.support.DuplicateReferenceException;
import alfio.manager.support.IncompatibleStateException;
import alfio.manager.support.reservation.ReservationEmailContentHelper;
import alfio.manager.system.ReservationPriceCalculator;
import alfio.model.*;
import alfio.model.PurchaseContext.PurchaseContextType;
Expand Down Expand Up @@ -81,6 +82,7 @@
import static alfio.util.Wrappers.optionally;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;
import static java.util.stream.Collectors.*;
import static org.apache.commons.lang3.StringUtils.firstNonBlank;
Expand Down Expand Up @@ -119,6 +121,7 @@ public class AdminReservationManager {
private final BillingDocumentManager billingDocumentManager;
private final ClockProvider clockProvider;
private final SubscriptionRepository subscriptionRepository;
private final ReservationEmailContentHelper reservationEmailContentHelper;

//the following methods have an explicit transaction handling, therefore the @Transactional annotation is not helpful here
Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> confirmReservation(PurchaseContextType purchaseContextType,
Expand All @@ -129,25 +132,26 @@ Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> confirmReservat
UUID subscriptionId) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionTemplate template = new TransactionTemplate(transactionManager, definition);
return template.execute(status -> {
Result<String> result = template.execute(status -> {
try {
Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> result = purchaseContextManager.findBy(purchaseContextType, eventName)
Result<String> confirmationResult = purchaseContextManager.findBy(purchaseContextType, eventName)
.map(purchaseContext -> ticketReservationRepository.findOptionalReservationById(reservationId)
.filter(r -> r.getStatus() == TicketReservationStatus.PENDING || r.getStatus() == TicketReservationStatus.STUCK)
.map(r -> performConfirmation(reservationId, purchaseContext, r, notification, username, subscriptionId))
.orElseGet(() -> Result.error(ErrorCode.ReservationError.UPDATE_FAILED))
).orElseGet(() -> Result.error(ErrorCode.ReservationError.NOT_FOUND));
if(!result.isSuccess()) {
if(!confirmationResult.isSuccess()) {
log.debug("Reservation confirmation failed for eventName: {} reservationId: {}, username: {}", eventName, reservationId, username);
status.setRollbackOnly();
}
return result;
return confirmationResult;
} catch (Exception e) {
log.error("Error during confirmation of reservation eventName: {} reservationId: {}, username: {}", eventName, reservationId, username);
status.setRollbackOnly();
return Result.error(singletonList(ErrorCode.custom("", e.getMessage())));
}
});
return requireNonNull(result).flatMap(this::loadReservation);
}
public Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> confirmReservation(PurchaseContextType purchaseContextType,
String eventName,
Expand Down Expand Up @@ -247,7 +251,7 @@ private void sendTicketToAttendees(Event event, TicketReservation reservation, P
.forEach(t -> {
Locale locale = LocaleUtil.forLanguageTag(t.getUserLanguage());
var additionalInfo = ticketReservationManager.retrieveAttendeeAdditionalInfoForTicket(t);
ticketReservationManager.sendTicketByEmail(t, locale, event, ticketReservationManager.getTicketEmailGenerator(event, reservation, locale, additionalInfo));
reservationEmailContentHelper.sendTicketByEmail(t, locale, event, ticketReservationManager.getTicketEmailGenerator(event, reservation, locale, additionalInfo));
});
}

Expand Down Expand Up @@ -357,12 +361,12 @@ private Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> loadRes
.orElseGet(() -> Result.error(ErrorCode.ReservationError.NOT_FOUND));
}

private Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> performConfirmation(String reservationId,
PurchaseContext purchaseContext,
TicketReservation original,
Notification notification,
String username,
UUID subscriptionId) {
private Result<String> performConfirmation(String reservationId,
PurchaseContext purchaseContext,
TicketReservation original,
Notification notification,
String username,
UUID subscriptionId) {
try {

var reservation = original;
Expand Down Expand Up @@ -417,7 +421,7 @@ private Result<Triple<TicketReservation, List<Ticket>, PurchaseContext>> perform
notification.isCustomer(),
notification.isAttendees(),
username);
return loadReservation(reservationId);
return Result.success(reservationId);
} catch(Exception e) {
return Result.error(ErrorCode.ReservationError.UPDATE_FAILED);
}
Expand Down

0 comments on commit fb568c3

Please sign in to comment.