Skip to content

Commit

Permalink
#343: save platform fees and gateway fees
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Oct 17, 2017
1 parent 435acc7 commit 545aa3d
Show file tree
Hide file tree
Showing 21 changed files with 490 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
import alfio.model.result.ValidationResult;
import alfio.model.user.Organization;
import alfio.model.user.Role;
import alfio.repository.*;
import alfio.repository.DynamicFieldTemplateRepository;
import alfio.repository.SponsorScanRepository;
import alfio.repository.TicketCategoryDescriptionRepository;
import alfio.repository.TicketFieldRepository;
import alfio.util.Json;
import alfio.util.MonetaryUtil;
import alfio.util.TemplateManager;
Expand Down Expand Up @@ -433,7 +436,7 @@ public Integer getPendingPaymentsCount(@PathVariable("eventName") String eventNa
@RequestMapping(value = "/events/{eventName}/pending-payments/{reservationId}/confirm", method = POST)
public String confirmPayment(@PathVariable("eventName") String eventName, @PathVariable("reservationId") String reservationId, Principal principal,
Model model, HttpServletRequest request) {
ticketReservationManager.confirmOfflinePayment(loadEvent(eventName, principal), reservationId);
ticketReservationManager.confirmOfflinePayment(loadEvent(eventName, principal), reservationId, principal.getName());
ticketReservationManager.findById(reservationId)
.filter(TicketReservation::isDirectAssignmentRequested)
.ifPresent(reservation -> ticketHelper.directTicketAssignment(eventName, reservationId, reservation.getEmail(), reservation.getFullName(), reservation.getFirstName(), reservation.getLastName(), reservation.getUserLanguage(), Optional.empty(), request, model));
Expand All @@ -459,7 +462,7 @@ public List<Triple<Boolean, String, String>> bulkConfirmation(@PathVariable("eve
try {
Validate.isTrue(line.length >= 2);
reservationID = line[0];
ticketReservationManager.validateAndConfirmOfflinePayment(reservationID, event, new BigDecimal(line[1]));
ticketReservationManager.validateAndConfirmOfflinePayment(reservationID, event, new BigDecimal(line[1]), principal.getName());
return Triple.of(Boolean.TRUE, reservationID, "");
} catch (Exception e) {
return Triple.of(Boolean.FALSE, Optional.ofNullable(reservationID).orElse(""), e.getMessage());
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/alfio/manager/CheckInManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public class CheckInManager {
private final ConfigurationManager configurationManager;
private final OrganizationRepository organizationRepository;
private final UserRepository userRepository;
private final TicketReservationManager ticketReservationManager;


private void checkIn(String uuid) {
Expand All @@ -87,8 +88,10 @@ private void checkIn(String uuid) {
}

private void acquire(String uuid) {
Validate.isTrue(ticketRepository.findByUUID(uuid).getStatus() == TicketStatus.TO_BE_PAID);
Ticket ticket = ticketRepository.findByUUID(uuid);
Validate.isTrue(ticket.getStatus() == TicketStatus.TO_BE_PAID);
ticketRepository.updateTicketStatusWithUUID(uuid, TicketStatus.ACQUIRED.toString());
ticketReservationManager.registerAlfioTransaction(eventRepository.findById(ticket.getEventId()), ticket.getTicketsReservationId(), PaymentProxy.ON_SITE);
}

public TicketAndCheckInResult confirmOnSitePayment(String eventName, String ticketIdentifier, Optional<String> ticketCode, String user) {
Expand Down
61 changes: 49 additions & 12 deletions src/main/java/alfio/manager/PaymentManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package alfio.manager;

import alfio.manager.support.FeeCalculator;
import alfio.manager.support.PaymentResult;
import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
Expand All @@ -24,12 +25,14 @@
import alfio.model.transaction.PaymentProxy;
import alfio.model.transaction.Transaction;
import alfio.repository.AuditingRepository;
import alfio.repository.TicketRepository;
import alfio.repository.TransactionRepository;
import alfio.repository.user.UserRepository;
import alfio.util.ErrorsCode;
import com.paypal.base.rest.PayPalRESTException;
import com.stripe.exception.StripeException;
import com.stripe.model.Charge;
import com.stripe.model.Fee;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.log4j.Log4j2;
Expand All @@ -39,6 +42,7 @@

import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Component
Expand All @@ -53,6 +57,7 @@ public class PaymentManager {
private final ConfigurationManager configurationManager;
private final AuditingRepository auditingRepository;
private final UserRepository userRepository;
private final TicketRepository ticketRepository;

/**
* This method processes the pending payment using the configured payment gateway (at the time of writing, only STRIPE)
Expand Down Expand Up @@ -80,8 +85,15 @@ PaymentResult processStripePayment(String reservationId,
final Charge charge = stripeManager.chargeCreditCard(gatewayToken, price,
event, reservationId, email, customerName.getFullName(), billingAddress);
log.info("transaction {} paid: {}", reservationId, charge.getPaid());
Pair<Long, Long> fees = Optional.ofNullable(charge.getBalanceTransactionObject()).map(bt -> {
List<Fee> feeDetails = bt.getFeeDetails();
return Pair.of(Optional.ofNullable(StripeManager.getFeeAmount(feeDetails, "application_fee")).map(Long::parseLong).orElse(0L),
Optional.ofNullable(StripeManager.getFeeAmount(feeDetails, "stripe_fee")).map(Long::parseLong).orElse(0L));
}).orElse(null);

transactionRepository.insert(charge.getId(), null, reservationId,
ZonedDateTime.now(), price, event.getCurrency(), charge.getDescription(), PaymentProxy.STRIPE.name());
ZonedDateTime.now(), price, event.getCurrency(), charge.getDescription(), PaymentProxy.STRIPE.name(),
fees != null ? fees.getLeft() : 0L, fees != null ? fees.getRight() : 0L);
return PaymentResult.successful(charge.getId());
} catch (Exception e) {
if(e instanceof StripeException) {
Expand All @@ -101,8 +113,18 @@ PaymentResult processPayPalPayment(String reservationId,
Pair<String, String> captureAndPaymentId = paypalManager.commitPayment(reservationId, token, payerId, event);
String captureId = captureAndPaymentId.getLeft();
String paymentId = captureAndPaymentId.getRight();
Supplier<String> feeSupplier = () -> FeeCalculator.getCalculator(event, configurationManager)
.apply(ticketRepository.countTicketsInReservation(reservationId), (long) price)
.map(String::valueOf)
.orElse("0");
Pair<Long, Long> fees = paypalManager.getInfo(paymentId, captureId, event, feeSupplier).map(i -> {
Long platformFee = Optional.ofNullable(i.getPlatformFee()).map(Long::parseLong).orElse(0L);
Long gatewayFee = Optional.ofNullable(i.getFee()).map(Long::parseLong).orElse(0L);
return Pair.of(platformFee, gatewayFee);
}).orElseGet(() -> Pair.of(0L, 0L));
transactionRepository.insert(captureId, paymentId, reservationId,
ZonedDateTime.now(), price, event.getCurrency(), "Paypal confirmation", PaymentProxy.PAYPAL.name());
ZonedDateTime.now(), price, event.getCurrency(), "Paypal confirmation", PaymentProxy.PAYPAL.name(),
fees.getLeft(), fees.getRight());
return PaymentResult.successful(captureId);
} catch (Exception e) {
log.warn("errow while processing paypal payment: " + e.getMessage(), e);
Expand Down Expand Up @@ -164,19 +186,34 @@ Audit.EventType.REFUND, new Date(), Audit.EntityType.RESERVATION, reservation.ge
}

TransactionAndPaymentInfo getInfo(TicketReservation reservation, Event event) {
Optional<Transaction> maybeTransaction = transactionRepository.loadOptionalByReservationId(reservation.getId());
return maybeTransaction.map(transaction -> {
switch(reservation.getPaymentMethod()) {
case PAYPAL:
return new TransactionAndPaymentInfo(reservation.getPaymentMethod(), transaction, paypalManager.getInfo(transaction, event).orElse(null));
case STRIPE:
return new TransactionAndPaymentInfo(reservation.getPaymentMethod(), transaction, stripeManager.getInfo(transaction, event).orElse(null));
default:
return new TransactionAndPaymentInfo(reservation.getPaymentMethod(), transaction, new PaymentInformation(reservation.getPaidAmount(), null, null, null));
Optional<TransactionAndPaymentInfo> maybeTransaction = transactionRepository.loadOptionalByReservationId(reservation.getId())
.map(transaction -> {
switch(reservation.getPaymentMethod()) {
case PAYPAL:
return new TransactionAndPaymentInfo(reservation.getPaymentMethod(), transaction, paypalManager.getInfo(transaction, event).orElse(null));
case STRIPE:
return new TransactionAndPaymentInfo(reservation.getPaymentMethod(), transaction, stripeManager.getInfo(transaction, event).orElse(null));
default:
return new TransactionAndPaymentInfo(reservation.getPaymentMethod(), transaction, new PaymentInformation(reservation.getPaidAmount(), null, String.valueOf(transaction.getGatewayFee()), String.valueOf(transaction.getPlatformFee())));
}
});
maybeTransaction.ifPresent(info -> {
try {
Transaction transaction = info.getTransaction();
String transactionId = transaction.getTransactionId();
PaymentInformation paymentInformation = info.getPaymentInformation();
if(paymentInformation != null) {
transactionRepository.updateFees(transactionId, reservation.getId(), Long.parseLong(paymentInformation.getPlatformFee()), Long.parseLong(paymentInformation.getFee()));
}
} catch (Exception e) {
log.warn("cannot update fees", e);
}
}).orElseGet(() -> new TransactionAndPaymentInfo(reservation.getPaymentMethod(),null, new PaymentInformation(reservation.getPaidAmount(), null, null, null)));
});
return maybeTransaction.orElseGet(() -> new TransactionAndPaymentInfo(reservation.getPaymentMethod(),null, new PaymentInformation(reservation.getPaidAmount(), null, null, null)));
}



@Data
public static final class PaymentMethod {

Expand Down
34 changes: 28 additions & 6 deletions src/main/java/alfio/manager/PaypalManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
*/
package alfio.manager;

import alfio.manager.support.FeeCalculator;
import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
import alfio.model.Event;
import alfio.model.system.Configuration;
import alfio.model.system.ConfigurationKeys;
import alfio.repository.TicketRepository;
import alfio.repository.TicketReservationRepository;
import alfio.util.MonetaryUtil;
import com.paypal.api.payments.*;
import com.paypal.api.payments.Currency;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.PayPalRESTException;
import lombok.AllArgsConstructor;
Expand All @@ -41,6 +44,7 @@
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import static alfio.util.MonetaryUtil.formatCents;

Expand All @@ -53,6 +57,7 @@ public class PaypalManager {
private final MessageSource messageSource;
private final ConcurrentHashMap<String, String> cachedWebProfiles = new ConcurrentHashMap<>();
private final TicketReservationRepository ticketReservationRepository;
private final TicketRepository ticketRepository;

private APIContext getApiContext(Event event) {
int orgId = event.getOrganizationId();
Expand Down Expand Up @@ -235,14 +240,14 @@ public Pair<String, String> commitPayment(String reservationId, String token, St
return Pair.of(captureId, payment.getId());
}

public Optional<PaymentInformation> getInfo(alfio.model.transaction.Transaction transaction, Event event) {
Optional<PaymentInformation> getInfo(String paymentId, String transactionId, Event event, Supplier<String> platformFeeSupplier) {
try {
String refund = null;

//check for backward compatibility reason...
if(transaction.getPaymentId() != null) {
if(paymentId != null) {
//navigate in all refund objects and sum their amount
refund = Payment.get(getApiContext(event), transaction.getPaymentId()).getTransactions().stream()
refund = Payment.get(getApiContext(event), paymentId).getTransactions().stream()
.map(Transaction::getRelatedResources)
.flatMap(List::stream)
.filter(f -> f.getRefund() != null)
Expand All @@ -253,13 +258,30 @@ public Optional<PaymentInformation> getInfo(alfio.model.transaction.Transaction
.reduce(BigDecimal.ZERO, BigDecimal::add).toPlainString();
//
}
Capture c = Capture.get(getApiContext(event), transaction.getTransactionId());
return Optional.of(new PaymentInformation(c.getAmount().getTotal(), refund, c.getTransactionFee().getValue(), null));
Capture c = Capture.get(getApiContext(event), transactionId);
String gatewayFee = Optional.ofNullable(c.getTransactionFee())
.map(Currency::getValue)
.map(BigDecimal::new)
.map(MonetaryUtil::unitToCents)
.map(String::valueOf)
.orElse(null);
return Optional.of(new PaymentInformation(c.getAmount().getTotal(), refund, gatewayFee, platformFeeSupplier.get()));
} catch (PayPalRESTException ex) {
log.warn("Paypal: error while fetching information for payment id " + transaction.getTransactionId(), ex);
log.warn("Paypal: error while fetching information for payment id " + transactionId, ex);
return Optional.empty();
}
}
Optional<PaymentInformation> getInfo(alfio.model.transaction.Transaction transaction, Event event) {
return getInfo(transaction.getPaymentId(), transaction.getTransactionId(), event, () -> {
if(transaction.getPlatformFee() > 0) {
return String.valueOf(transaction.getPlatformFee());
}
return FeeCalculator.getCalculator(event, configurationManager)
.apply(ticketRepository.countTicketsInReservation(transaction.getReservationId()), (long) transaction.getPriceInCents())
.map(String::valueOf)
.orElse("0");
});
}

public boolean refund(alfio.model.transaction.Transaction transaction, Event event, Optional<Integer> amount) {
String captureId = transaction.getTransactionId();
Expand Down
49 changes: 16 additions & 33 deletions src/main/java/alfio/manager/StripeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package alfio.manager;


import alfio.manager.support.FeeCalculator;
import alfio.manager.system.ConfigurationManager;
import alfio.manager.user.UserManager;
import alfio.model.Event;
Expand Down Expand Up @@ -46,12 +47,10 @@
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;

import static alfio.model.system.ConfigurationKeys.*;
import static org.apache.commons.lang3.StringUtils.*;

@Component
@Log4j2
Expand Down Expand Up @@ -175,7 +174,7 @@ Charge chargeCreditCard(String stripeToken, long amountInCent, Event event,
int tickets = ticketRepository.countTicketsInReservation(reservationId);
Map<String, Object> chargeParams = new HashMap<>();
chargeParams.put("amount", amountInCent);
Optional.ofNullable(calculateFee(event, tickets, amountInCent)).ifPresent(fee -> chargeParams.put("application_fee", fee));
FeeCalculator.getCalculator(event, configurationManager).apply(tickets, amountInCent).ifPresent(fee -> chargeParams.put("application_fee", fee));
chargeParams.put("currency", event.getCurrency());
chargeParams.put("card", stripeToken);

Expand All @@ -189,16 +188,20 @@ Charge chargeCreditCard(String stripeToken, long amountInCent, Event event,
initialMetadata.put("billingAddress", billingAddress);
}
chargeParams.put("metadata", initialMetadata);
return Charge.create(chargeParams, options(event));
RequestOptions options = options(event);
Charge charge = Charge.create(chargeParams, options);
if(charge.getBalanceTransactionObject() == null) {
try {
charge.setBalanceTransactionObject(retrieveBalanceTransaction(charge.getBalanceTransaction(), options));
} catch(Exception e) {
log.warn("can't retrieve balance transaction", e);
}
}
return charge;
}

private Long calculateFee(Event event, int numTickets, long amountInCent) {
if(isConnectEnabled(event)) {
String feeAsString = configurationManager.getStringConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), PLATFORM_FEE), "0");
String minimumFee = configurationManager.getStringConfigValue(Configuration.from(event.getOrganizationId(), event.getId(), PLATFORM_MINIMUM_FEE), "0");
return new FeeCalculator(feeAsString, minimumFee, numTickets).calculate(amountInCent);
}
return null;
private BalanceTransaction retrieveBalanceTransaction(String balanceTransaction, RequestOptions options) throws AuthenticationException, InvalidRequestException, APIConnectionException, CardException, APIException {
return BalanceTransaction.retrieve(balanceTransaction, options);
}


Expand All @@ -218,14 +221,14 @@ Optional<PaymentInformation> getInfo(Transaction transaction, Event event) {
Charge charge = Charge.retrieve(transaction.getTransactionId(), options);
String paidAmount = MonetaryUtil.formatCents(charge.getAmount());
String refundedAmount = MonetaryUtil.formatCents(charge.getAmountRefunded());
List<Fee> fees = BalanceTransaction.retrieve(charge.getBalanceTransaction(), options).getFeeDetails();
List<Fee> fees = retrieveBalanceTransaction(charge.getBalanceTransaction(), options).getFeeDetails();
return Optional.of(new PaymentInformation(paidAmount, refundedAmount, getFeeAmount(fees, "stripe_fee"), getFeeAmount(fees, "application_fee")));
} catch (StripeException e) {
return Optional.empty();
}
}

private String getFeeAmount(List<Fee> fees, String feeType) {
static String getFeeAmount(List<Fee> fees, String feeType) {
return fees.stream()
.filter(f -> f.getType().equals(feeType))
.findFirst()
Expand Down Expand Up @@ -358,24 +361,4 @@ public static class ConnectURL {
private final String code;
}

private static class FeeCalculator {
private final BigDecimal fee;
private final BigDecimal minimumFee;
private final boolean percentage;
private final int numTickets;

private FeeCalculator(String feeAsString, String minimumFeeAsString, int numTickets) {
this.percentage = feeAsString.endsWith("%");
this.fee = new BigDecimal(defaultIfEmpty(substringBefore(feeAsString, "%"), "0"));
this.minimumFee = new BigDecimal(defaultIfEmpty(trimToNull(minimumFeeAsString), "0"));
this.numTickets = numTickets;
}

private long calculate(long price) {
long result = percentage ? MonetaryUtil.calcPercentage(price, fee, BigDecimal::longValueExact) : MonetaryUtil.unitToCents(fee);
long minFee = MonetaryUtil.unitToCents(minimumFee, BigDecimal::longValueExact) * numTickets;
return Math.max(result, minFee);
}
}

}
Loading

0 comments on commit 545aa3d

Please sign in to comment.