/*
* Copyright 2014 Groupon, Inc
* Copyright 2014 The Billing Project, LLC
*
* Groupon licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.killbill.billing.payment.core.sm.payments;
import java.math.BigDecimal;
import java.util.Iterator;
import org.killbill.automaton.Operation.OperationCallback;
import org.killbill.automaton.OperationException;
import org.killbill.automaton.OperationResult;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.PaymentTransactionInfoPluginConverter;
import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback;
import org.killbill.billing.payment.core.sm.OperationCallbackBase;
import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
import org.killbill.billing.payment.core.sm.PaymentStateContext;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dispatcher.PluginDispatcher;
import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.commons.locker.GlobalLocker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
// Encapsulates the payment specific logic
public abstract class PaymentOperation extends OperationCallbackBase<PaymentTransactionInfoPlugin, PaymentPluginApiException> implements OperationCallback {
private final Logger logger = LoggerFactory.getLogger(PaymentOperation.class);
protected final PaymentAutomatonDAOHelper daoHelper;
protected PaymentPluginApi paymentPluginApi;
protected PaymentOperation(final GlobalLocker locker,
final PaymentAutomatonDAOHelper daoHelper,
final PluginDispatcher<OperationResult> paymentPluginDispatcher,
final PaymentConfig paymentConfig,
final PaymentStateContext paymentStateContext) {
super(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext);
this.daoHelper = daoHelper;
}
@Override
public OperationResult doOperationCallback() throws OperationException {
try {
this.paymentPluginApi = daoHelper.getPaymentPluginApi();
} catch (final PaymentApiException e) {
throw convertToUnknownTransactionStatusAndErroredPaymentState(e);
}
if (paymentStateContext.shouldLockAccountAndDispatch()) {
// This will already call unwrapExceptionFromDispatchedTask
return doOperationCallbackWithDispatchAndAccountLock(daoHelper.getPluginName());
} else {
try {
return doSimpleOperationCallback();
} catch (final OperationException e) {
throw convertToUnknownTransactionStatusAndErroredPaymentState(e);
}
}
}
@Override
protected OperationException unwrapExceptionFromDispatchedTask(final PaymentApiException e) {
return convertToUnknownTransactionStatusAndErroredPaymentState(e);
}
//
// In case of exceptions, timeouts, we don't really know what happened:
// - Return an OperationResult.EXCEPTION to transition Payment State to Errored (see PaymentTransactionInfoPluginConverter#toOperationResult)
// - Construct a PaymentTransactionInfoPlugin whose PaymentPluginStatus = UNDEFINED to end up with a paymentTransactionStatus = UNKNOWN and have a chance to
// be fixed by Janitor.
//
private OperationException convertToUnknownTransactionStatusAndErroredPaymentState(final Exception e) {
final PaymentTransactionInfoPlugin paymentInfoPlugin = new DefaultNoOpPaymentInfoPlugin(paymentStateContext.getPaymentId(),
paymentStateContext.getTransactionId(),
paymentStateContext.getTransactionType(),
paymentStateContext.getAmount(),
paymentStateContext.getCurrency(),
paymentStateContext.getCallContext().getCreatedDate(),
paymentStateContext.getCallContext().getCreatedDate(),
PaymentPluginStatus.UNDEFINED,
null,
null);
paymentStateContext.setPaymentTransactionInfoPlugin(paymentInfoPlugin);
if (e.getCause() instanceof OperationException) {
return (OperationException) e.getCause();
}
if (e instanceof OperationException) {
return (OperationException) e;
}
return new OperationException(e, OperationResult.EXCEPTION);
}
@Override
protected abstract PaymentTransactionInfoPlugin doCallSpecificOperationCallback() throws PaymentPluginApiException;
protected Iterable<PaymentTransactionModelDao> getOnLeavingStateExistingTransactionsForType(final TransactionType transactionType) {
if (paymentStateContext.getOnLeavingStateExistingTransactions() == null || paymentStateContext.getOnLeavingStateExistingTransactions().isEmpty()) {
return ImmutableList.of();
}
return Iterables.filter(paymentStateContext.getOnLeavingStateExistingTransactions(), new Predicate<PaymentTransactionModelDao>() {
@Override
public boolean apply(final PaymentTransactionModelDao input) {
return input.getTransactionStatus() == TransactionStatus.SUCCESS && input.getTransactionType() == transactionType;
}
});
}
protected BigDecimal getSumAmount(final Iterable<PaymentTransactionModelDao> transactions) {
BigDecimal result = BigDecimal.ZERO;
final Iterator<PaymentTransactionModelDao> iterator = transactions.iterator();
while (iterator.hasNext()) {
result = result.add(iterator.next().getAmount());
}
return result;
}
private OperationResult doOperationCallbackWithDispatchAndAccountLock(String pluginName) throws OperationException {
return dispatchWithAccountLockAndTimeout(pluginName, new DispatcherCallback<PluginDispatcherReturnType<OperationResult>, OperationException>() {
@Override
public PluginDispatcherReturnType<OperationResult> doOperation() throws OperationException {
final OperationResult result = doSimpleOperationCallback();
return PluginDispatcher.createPluginDispatcherReturnType(result);
}
});
}
private OperationResult doSimpleOperationCallback() throws OperationException {
try {
return doOperation();
} catch (final PaymentApiException e) {
throw new OperationException(e, OperationResult.EXCEPTION);
} catch (final RuntimeException e) {
throw new OperationException(e, OperationResult.EXCEPTION);
}
}
private OperationResult doOperation() throws PaymentApiException {
try {
//
// If the OperationResult was specified in the plugin, it means we want to bypass the plugin and just care
// about running through the state machine to bring the transaction/payment into a new state.
//
if (paymentStateContext.getOverridePluginOperationResult() == null) {
final PaymentTransactionInfoPlugin paymentInfoPlugin = doCallSpecificOperationCallback();
//
// We catch null paymentInfoPlugin and throw a RuntimeException to end up in an UNKNOWN transactionStatus
// That way we can use the null paymentInfoPlugin when a PaymentPluginApiException is thrown and correctly
// make the transition to PLUGIN_FAILURE
//
if (paymentInfoPlugin == null) {
throw new IllegalStateException("Payment plugin returned a null result");
}
paymentStateContext.setPaymentTransactionInfoPlugin(paymentInfoPlugin);
return PaymentTransactionInfoPluginConverter.toOperationResult(paymentStateContext.getPaymentTransactionInfoPlugin());
} else {
final PaymentTransactionInfoPlugin paymentInfoPlugin = new DefaultNoOpPaymentInfoPlugin(paymentStateContext.getPaymentId(),
paymentStateContext.getTransactionId(),
paymentStateContext.getTransactionType(),
paymentStateContext.getPaymentTransactionModelDao().getProcessedAmount(),
paymentStateContext.getPaymentTransactionModelDao().getProcessedCurrency(),
paymentStateContext.getPaymentTransactionModelDao().getEffectiveDate(),
paymentStateContext.getPaymentTransactionModelDao().getCreatedDate(),
buildPaymentPluginStatusFromOperationResult(paymentStateContext.getOverridePluginOperationResult()),
null,
null);
paymentStateContext.setPaymentTransactionInfoPlugin(paymentInfoPlugin);
return paymentStateContext.getOverridePluginOperationResult();
}
} catch (final PaymentPluginApiException e) {
throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_EXCEPTION, e.getErrorMessage());
}
}
private PaymentPluginStatus buildPaymentPluginStatusFromOperationResult(final OperationResult operationResult) {
switch (operationResult) {
case PENDING:
return PaymentPluginStatus.PENDING;
case SUCCESS:
return PaymentPluginStatus.PROCESSED;
case FAILURE:
return PaymentPluginStatus.ERROR;
case EXCEPTION:
default:
return PaymentPluginStatus.UNDEFINED;
}
}
}