/**
* 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;
import alfio.manager.system.ConfigurationManager;
import alfio.model.Event;
import alfio.model.PaymentInformations;
import alfio.model.system.Configuration;
import alfio.model.transaction.Transaction;
import alfio.repository.TicketRepository;
import alfio.util.MonetaryUtil;
import com.stripe.exception.*;
import com.stripe.model.Charge;
import com.stripe.model.Refund;
import com.stripe.net.RequestOptions;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static alfio.model.system.ConfigurationKeys.STRIPE_PUBLIC_KEY;
import static alfio.model.system.ConfigurationKeys.STRIPE_SECRET_KEY;
@Component
@Log4j2
public class StripeManager {
public static final String STRIPE_UNEXPECTED = "error.STEP2_STRIPE_unexpected";
private final Map<Class<? extends StripeException>, StripeExceptionHandler> handlers;
private final ConfigurationManager configurationManager;
private final TicketRepository ticketRepository;
@Autowired
public StripeManager(ConfigurationManager configurationManager,
TicketRepository ticketRepository) {
this.configurationManager = configurationManager;
this.ticketRepository = ticketRepository;
handlers = new HashMap<>();
handlers.put(CardException.class, this::handleCardException);
handlers.put(InvalidRequestException.class, this::handleInvalidRequestException);
handlers.put(AuthenticationException.class, this::handleAuthenticationException);
handlers.put(APIConnectionException.class, this::handleApiConnectionException);
handlers.put(StripeException.class, this::handleGenericException);
}
private String getSecretKey(Event event) {
return configurationManager.getRequiredValue(Configuration.from(event.getOrganizationId(), event.getId(), STRIPE_SECRET_KEY));
}
public String getPublicKey(Event event) {
return configurationManager.getRequiredValue(Configuration.from(event.getOrganizationId(), event.getId(), STRIPE_PUBLIC_KEY));
}
/**
* After client side integration with stripe widget, our server receives the stripeToken
* StripeToken is a single-use token that allows our server to effectively charge the credit card and
* get money on our account.
* <p>
* as documented in https://stripe.com/docs/tutorials/charges
*
* @return
* @throws StripeException
*/
public Charge chargeCreditCard(String stripeToken, long amountInCent, Event event,
String reservationId, String email, String fullName, String billingAddress) throws StripeException {
Map<String, Object> chargeParams = new HashMap<>();
chargeParams.put("amount", amountInCent);
chargeParams.put("currency", event.getCurrency());
chargeParams.put("card", stripeToken);
int tickets = ticketRepository.countTicketsInReservation(reservationId);
chargeParams.put("description", String.format("%d ticket(s) for event %s", tickets, event.getDisplayName()));
Map<String, String> initialMetadata = new HashMap<>();
initialMetadata.put("reservationId", reservationId);
initialMetadata.put("email", email);
initialMetadata.put("fullName", fullName);
if (StringUtils.isNotBlank(billingAddress)) {
initialMetadata.put("billingAddress", billingAddress);
}
chargeParams.put("metadata", initialMetadata);
return Charge.create(chargeParams, options(event));
}
private RequestOptions options(Event event) {
return RequestOptions.builder().setApiKey(getSecretKey(event)).build();
}
public Optional<PaymentInformations> getInfo(Transaction transaction, Event event) {
try {
Charge charge = Charge.retrieve(transaction.getTransactionId(), options(event));
PaymentInformations pi = new PaymentInformations(MonetaryUtil.formatCents(charge.getAmount()), MonetaryUtil.formatCents(charge.getAmountRefunded()));
return Optional.ofNullable(pi);
} catch (StripeException e) {
return Optional.empty();
}
}
// https://stripe.com/docs/api#create_refund
public boolean refund(Transaction transaction, Event event, Optional<Integer> amount) {
String chargeId = transaction.getTransactionId();
try {
String amountOrFull = amount.map(MonetaryUtil::formatCents).orElse("full");
log.info("Stripe: trying to do a refund for payment {} with amount: {}", chargeId, amountOrFull);
Map<String, Object> params = new HashMap<>();
params.put("charge", chargeId);
amount.ifPresent(a -> params.put("amount", a));
Refund r = Refund.create(params, options(event));
if("succeeded".equals(r.getStatus())) {
log.info("Stripe: refund for payment {} executed with success for amount: {}", chargeId, amountOrFull);
return true;
} else {
log.warn("Stripe: was not able to refund payment with id {}, returned status is not 'succeded' but {}", chargeId, r.getStatus());
return false;
}
} catch (StripeException e) {
log.warn("Stripe: was not able to refund payment with id " + chargeId, e);
return false;
}
}
public String handleException(StripeException exc) {
return findExceptionHandler(exc).handle(exc);
}
private StripeExceptionHandler findExceptionHandler(StripeException exc) {
final Optional<StripeExceptionHandler> eh = Optional.ofNullable(handlers.get(exc.getClass()));
if(!eh.isPresent()) {
log.warn("cannot find an ExceptionHandler for {}. Falling back to the default one.", exc.getClass());
}
return eh.orElseGet(() -> handlers.get(StripeException.class));
}
/* exception handlers... */
/**
* This handler simply returns the message code from stripe.
* There is no need in writing something in the log.
* @param e the exception
* @return the code
*/
private String handleCardException(StripeException e) {
CardException ce = (CardException)e;
return "error.STEP2_STRIPE_" + ce.getCode();
}
/**
* handles invalid request exception using the error.STEP2_STRIPE_invalid_ prefix for the message.
* @param e the exception
* @return message code
*/
private String handleInvalidRequestException(StripeException e) {
InvalidRequestException ire = (InvalidRequestException)e;
return "error.STEP2_STRIPE_invalid_" + ire.getParam();
}
/**
* Logs the failure and report the failure to the admin (to be done)
* @param e the exception
* @return error.STEP2_STRIPE_abort
*/
private String handleAuthenticationException(StripeException e) {
log.error("an AuthenticationException has occurred. Please fix configuration!!", e);
return "error.STEP2_STRIPE_abort";
}
/**
* Logs the failure and report the failure to the admin (to be done)
* @param e
* @return
*/
private String handleApiConnectionException(StripeException e) {
log.error("unable to connect to the Stripe API", e);
return "error.STEP2_STRIPE_abort";
}
/**
* Logs the failure and report the failure to the admin (to be done)
* @param e
* @return
*/
private String handleGenericException(StripeException e) {
log.error("unexpected error during transaction", e);
return STRIPE_UNEXPECTED;
}
@FunctionalInterface
private interface StripeExceptionHandler {
String handle(StripeException exc);
}
}