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

Refactor payment confirmation #1202

Merged
merged 13 commits into from
Mar 20, 2023
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