Skip to content

Commit

Permalink
introduce cache for PayPalHttpClient; handle corner cases
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Jan 27, 2020
1 parent e83b3fb commit 4623256
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 20 deletions.
14 changes: 7 additions & 7 deletions src/main/java/alfio/manager/PaymentManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,13 @@ public Optional<PaymentToken> getPaymentToken(String reservationId) {
return transactionRepository.loadOptionalByReservationId(reservationId)
.filter(t->t.getStatus() == Transaction.Status.PENDING)
.flatMap(t -> {
if(t.getMetadata().containsKey(PAYMENT_TOKEN)) {
return lookupByTransactionAndCapabilities(t, List.of(ExtractPaymentTokenFromTransaction.class))
.map(ExtractPaymentTokenFromTransaction.class::cast)
.flatMap(paymentProvider -> paymentProvider.extractToken(t));
}
return Optional.empty();
});
if(t.getMetadata().containsKey(PAYMENT_TOKEN)) {
return lookupByTransactionAndCapabilities(t, List.of(ExtractPaymentTokenFromTransaction.class))
.map(ExtractPaymentTokenFromTransaction.class::cast)
.flatMap(paymentProvider -> paymentProvider.extractToken(t));
}
return Optional.empty();
});
}

public boolean removePaymentTokenReservation(String reservationId) {
Expand Down
60 changes: 47 additions & 13 deletions src/main/java/alfio/manager/payment/PayPalManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import alfio.util.ErrorsCode;
import alfio.util.Json;
import alfio.util.MonetaryUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
import com.paypal.http.HttpResponse;
Expand All @@ -44,6 +46,7 @@
import com.paypal.payments.CapturesRefundRequest;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -56,6 +59,7 @@
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
Expand All @@ -74,6 +78,9 @@
@AllArgsConstructor
public class PayPalManager implements PaymentProvider, RefundRequest, PaymentInfo, ExtractPaymentTokenFromTransaction {

private final Cache<String, PayPalHttpClient> cachedClients = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofHours(1L))
.build();
private final ConfigurationManager configurationManager;
private final MessageSourceManager messageSourceManager;
private final TicketReservationRepository ticketReservationRepository;
Expand All @@ -82,7 +89,12 @@ public class PayPalManager implements PaymentProvider, RefundRequest, PaymentInf
private final Json json;

private PayPalHttpClient getClient(EventAndOrganizationId event) {
return new PayPalHttpClient(getApiContext(event));
PayPalEnvironment apiContext = getApiContext(event);
return cachedClients.get(generateKey(apiContext), key -> new PayPalHttpClient(apiContext));
}

private String generateKey(PayPalEnvironment environment) {
return DigestUtils.sha256Hex(environment.baseUrl() + "::" + environment.clientId() + "::" + environment.clientSecret());
}

private PayPalEnvironment getApiContext(EventAndOrganizationId event) {
Expand All @@ -104,8 +116,9 @@ private String createCheckoutRequest(PaymentSpecification spec) throws Exception
String baseUrl = StringUtils.removeEnd(configurationManager.getFor(ConfigurationKeys.BASE_URL, ConfigurationLevel.event(spec.getEvent())).getRequiredValue(), "/");
String bookUrl = baseUrl+"/event/" + eventName + "/reservation/" + spec.getReservationId() + "/payment/paypal/" + URL_PLACEHOLDER;

String hmac = computeHMAC(spec.getCustomerName(), spec.getEmail(), spec.getBillingAddress(), spec.getEvent());
UriComponentsBuilder bookUrlBuilder = UriComponentsBuilder.fromUriString(bookUrl)
.queryParam("hmac", computeHMAC(spec.getCustomerName(), spec.getEmail(), spec.getBillingAddress(), spec.getEvent()));
.queryParam("hmac", hmac);
String finalUrl = bookUrlBuilder.toUriString();

ApplicationContext applicationContext = new ApplicationContext()
Expand All @@ -120,19 +133,24 @@ private String createCheckoutRequest(PaymentSpecification spec) throws Exception
.checkoutPaymentIntent("CAPTURE")
.purchaseUnits(List.of(new PurchaseUnitRequest().amountWithBreakdown(new AmountWithBreakdown().currencyCode(spec.getCurrencyCode()).value(spec.getOrderSummary().getTotalPrice()))));
OrdersCreateRequest request = new OrdersCreateRequest().requestBody(orderRequest);
request.header("prefer","return=representation");
request.header("PayPal-Request-Id", reservation.getId());
HttpResponse<Order> response = getClient(spec.getEvent()).execute(request);
if(requireNonNull(HttpStatus.resolve(response.statusCode())).is2xxSuccessful()) {
Order order = response.result();
var status = order.status();

if(!"CREATED".equals(order.status())) {
throw new Exception();
if("APPROVED".equals(status) || "COMPLETED".equals(status)) {
if("APPROVED".equals(status)) {
saveToken(reservation.getId(), spec.getEvent(), new PayPalToken(order.payer().payerId(), order.id(), hmac));
}
throw new AlreadyApprovedException();
} else if("CREATED".equals(status)) {
//add 15 minutes of validity in case the paypal flow is slow
ticketReservationRepository.updateValidity(spec.getReservationId(), DateUtils.addMinutes(reservation.getValidity(), 15));
return order.links().stream().filter(l -> l.rel().equals("approve")).map(LinkDescription::href).findFirst().orElseThrow();
}

//add 15 minutes of validity in case the paypal flow is slow
ticketReservationRepository.updateValidity(spec.getReservationId(), DateUtils.addMinutes(reservation.getValidity(), 15));

return order.links().stream().filter(l -> l.rel().equals("approve")).map(LinkDescription::href).findFirst().orElseThrow();
}
throw new IllegalStateException();
}
Expand Down Expand Up @@ -183,11 +201,21 @@ private PayPalChargeDetails commitPayment(String reservationId, PayPalToken payP
throw new IllegalStateException();
}

var capture = result.purchaseUnits().get(0).payments().captures().get(0);
var captureId = capture.id();
var payPalFee = capture.sellerReceivableBreakdown().paypalFee();

return new PayPalChargeDetails(captureId, result.id(), MonetaryUtil.unitToCents(new BigDecimal(payPalFee.value()), payPalFee.currencyCode()));
var captureOptional = result.purchaseUnits().stream()
.map(PurchaseUnit::payments)
.flatMap(pc -> pc.captures().stream())
.findFirst();
var captureId = captureOptional.map(Capture::id).orElseGet(() -> {
log.warn("PayPal: null Capture returned for OrderId {}", result.id());
return result.id();
});

var payPalFee = captureOptional.map(Capture::sellerReceivableBreakdown)
.map(MerchantReceivableBreakdown::paypalFee)
.map(money -> MonetaryUtil.unitToCents(new BigDecimal(money.value()), money.currencyCode()))
.orElse(0);

return new PayPalChargeDetails(captureId, result.id(), payPalFee);
}
} catch (HttpException e) {
mappedException(e).ifPresent(message -> {
Expand Down Expand Up @@ -301,6 +329,8 @@ public PaymentResult getToken(PaymentSpecification spec) {
return PaymentResult.initialized(gatewayToken.getToken());
}
return PaymentResult.redirect(createCheckoutRequest(spec));
} catch(AlreadyApprovedException ex) {
return PaymentResult.redirect("/event/"+spec.getEvent().getShortName()+"/reservation/"+spec.getReservationId());
} catch (Exception e) {
log.error(e);
return PaymentResult.failed( ErrorsCode.STEP_2_PAYMENT_REQUEST_CREATION );
Expand Down Expand Up @@ -373,6 +403,10 @@ private static class PayPalChargeDetails {
private final long payPalFee;
}

private static class AlreadyApprovedException extends RuntimeException {

}

private static boolean statusCodeIsSuccessful(HttpResponse<?> response) {
return requireNonNull(HttpStatus.resolve(response.statusCode())).is2xxSuccessful();
}
Expand Down

0 comments on commit 4623256

Please sign in to comment.