/*
* Copyright 2014-2017 Groupon, Inc
* Copyright 2014-2017 The Billing Project, LLC
*
* The Billing Project 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.janitor;
import java.util.List;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.events.PaymentInternalEvent;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dao.PluginPropertySerializer;
import org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
import org.killbill.notificationq.api.NotificationQueue;
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;
/**
* Task to complete 'partially' incomplete payment attempts. Tis only matters for calls that went through PaymentControl apis.
* <p/>
* If the state of the transaction associated with the attempt completed, but the attempt state machine did not,
* we rerun the retry state machine to complete the call and transition the attempt into a terminal state.
*/
public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAttemptModelDao> {
private static final Logger log = LoggerFactory.getLogger(IncompletePaymentAttemptTask.class);
//
// Each paymentAttempt *should* transition to a new state, so fetching a limited size will still allow us to progress (as opposed to fetching the same entries over and over)
// We also don't expect to see too many entries in the INIT state.
//
private static final long MAX_ATTEMPTS_PER_ITERATIONS = 1000L;
private final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner;
@Inject
public IncompletePaymentAttemptTask(final InternalCallContextFactory internalCallContextFactory,
final PaymentConfig paymentConfig,
final PaymentDao paymentDao,
final Clock clock,
final PaymentStateMachineHelper paymentStateMachineHelper,
final PaymentControlStateMachineHelper retrySMHelper,
final AccountInternalApi accountInternalApi,
final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner,
final GlobalLocker locker) {
super(internalCallContextFactory, paymentConfig, paymentDao, clock, paymentStateMachineHelper, retrySMHelper, accountInternalApi, locker);
this.pluginControlledPaymentAutomatonRunner = pluginControlledPaymentAutomatonRunner;
}
@Override
public Iterable<PaymentAttemptModelDao> getItemsForIteration() {
final Pagination<PaymentAttemptModelDao> incompleteAttempts = paymentDao.getPaymentAttemptsByStateAcrossTenants(retrySMHelper.getInitialState().getName(), getCreatedDateBefore(), 0L, MAX_ATTEMPTS_PER_ITERATIONS);
if (incompleteAttempts.getTotalNbRecords() > 0) {
log.info("Janitor AttemptCompletionTask start run: found {} incomplete attempts", incompleteAttempts.getTotalNbRecords());
}
return incompleteAttempts;
}
@Override
public void doIteration(final PaymentAttemptModelDao attempt) {
// We don't grab account lock here as the lock will be taken when calling the completeRun API.
final InternalTenantContext tenantContext = internalCallContextFactory.createInternalTenantContext(attempt.getTenantRecordId(), attempt.getAccountRecordId());
final CallContext callContext = createCallContext("AttemptCompletionJanitorTask", tenantContext);
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(attempt.getAccountId(), callContext);
final List<PaymentTransactionModelDao> transactions = paymentDao.getPaymentTransactionsByExternalKey(attempt.getTransactionExternalKey(), tenantContext);
final List<PaymentTransactionModelDao> filteredTransactions = ImmutableList.copyOf(Iterables.filter(transactions, new Predicate<PaymentTransactionModelDao>() {
@Override
public boolean apply(final PaymentTransactionModelDao input) {
return input.getAttemptId().equals(attempt.getId());
}
}));
// We only expect at most one transaction for a given attempt, but as a precaution we check for more; if this is the case we log a warn and continue processing the first one.
if (filteredTransactions.size() > 1) {
log.warn("Found {} transactions for paymentAttempt {}", filteredTransactions.size(), attempt.getId());
}
final PaymentTransactionModelDao transaction = filteredTransactions.isEmpty() ? null : filteredTransactions.get(0);
if (transaction == null) {
log.info("Moving attemptId='{}' to ABORTED", attempt.getId());
paymentDao.updatePaymentAttempt(attempt.getId(), attempt.getTransactionId(), "ABORTED", internalCallContext);
return;
}
// UNKNOWN transaction are handled by the Janitor IncompletePaymentTransactionTask and should eventually transition to something else,
// at which point the attempt can also be transition to a different state.
if (transaction.getTransactionStatus() == TransactionStatus.UNKNOWN) {
return;
}
try {
log.info("Completing attemptId='{}'", attempt.getId());
final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
final boolean isApiPayment = true; // unclear
final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(),
isApiPayment,
null,
transaction.getPaymentId(),
attempt.getPaymentExternalKey(),
transaction.getId(),
transaction.getTransactionExternalKey(),
transaction.getTransactionType(),
account,
attempt.getPaymentMethodId(),
transaction.getAmount(),
transaction.getCurrency(),
PluginPropertySerializer.deserialize(attempt.getPluginProperties()),
internalCallContext,
callContext);
paymentStateContext.setAttemptId(attempt.getId()); // Normally set by leavingState Callback
paymentStateContext.setPaymentTransactionModelDao(transaction); // Normally set by raw state machine
//
// Will rerun the state machine with special callbacks to only make the executePluginOnSuccessCalls / executePluginOnFailureCalls calls
// to the PaymentControlPluginApi plugin and transition the state.
//
pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
} catch (final AccountApiException e) {
log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
} catch (final PluginPropertySerializerException e) {
log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
} catch (final PaymentApiException e) {
log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
}
}
@Override
public void processPaymentEvent(final PaymentInternalEvent event, final NotificationQueue janitorQueue) {
// Nothing
}
private DateTime getCreatedDateBefore() {
final long delayBeforeNowMs = paymentConfig.getIncompleteAttemptsTimeSpanDelay().getMillis();
return clock.getUTCNow().minusMillis((int) delayBeforeNowMs);
}
}