DefaultControlCompleted.java

88 lines | 4.866 kB Blame History Raw Download
/*
 * Copyright 2014-2015 Groupon, Inc
 * Copyright 2014-2015 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.sm.control;

import java.util.List;
import java.util.UUID;

import org.killbill.automaton.Operation.OperationCallback;
import org.killbill.automaton.OperationResult;
import org.killbill.automaton.State;
import org.killbill.automaton.State.EnteringStateCallback;
import org.killbill.automaton.State.LeavingStateCallback;
import org.killbill.billing.ObjectType;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.core.sm.PluginRoutingPaymentAutomatonRunner;
import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.retry.BaseRetryService.RetryServiceScheduler;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

public class DefaultControlCompleted implements EnteringStateCallback {

    private PluginRoutingPaymentAutomatonRunner retryablePaymentAutomatonRunner;
    private final PaymentStateControlContext paymentStateContext;
    private final RetryServiceScheduler retryServiceScheduler;
    private final State retriedState;

    public DefaultControlCompleted(final PluginRoutingPaymentAutomatonRunner retryablePaymentAutomatonRunner, final PaymentStateControlContext paymentStateContext,
                                   final State retriedState, final RetryServiceScheduler retryServiceScheduler) {
        this.retryablePaymentAutomatonRunner = retryablePaymentAutomatonRunner;
        this.paymentStateContext = paymentStateContext;
        this.retriedState = retriedState;
        this.retryServiceScheduler = retryServiceScheduler;
    }

    @Override
    public void enteringState(final State state, final OperationCallback operationCallback, final OperationResult operationResult, final LeavingStateCallback leavingStateCallback) {
        final PaymentAttemptModelDao attempt = retryablePaymentAutomatonRunner.getPaymentDao().getPaymentAttempt(paymentStateContext.getAttemptId(), paymentStateContext.getInternalCallContext());
        final UUID transactionId = paymentStateContext.getCurrentTransaction() != null ?
                                   paymentStateContext.getCurrentTransaction().getId() :
                                   null;
        retryablePaymentAutomatonRunner.getPaymentDao().updatePaymentAttempt(attempt.getId(), transactionId, state.getName(), paymentStateContext.getInternalCallContext());

        if (retriedState.getName().equals(state.getName()) && !isUnknownTransaction()) {
            retryServiceScheduler.scheduleRetry(ObjectType.PAYMENT_ATTEMPT, attempt.getId(), attempt.getId(), attempt.getTenantRecordId(),
                                                paymentStateContext.getPaymentControlPluginNames(), paymentStateContext.getRetryDate());
        }
    }

    //
    // If we see an UNKNOWN transaction we prevent it to be rescheduled as the Janitor will *try* to fix it, and that could lead to infinite retries from a badly behaved plugin
    // (In other words, plugin should ONLY retry 'known' transaction)
    //
    private boolean isUnknownTransaction() {
        if (paymentStateContext.getCurrentTransaction() != null) {
            return paymentStateContext.getCurrentTransaction().getTransactionStatus() == TransactionStatus.UNKNOWN;
        } else {
            final List<PaymentTransactionModelDao> transactions = retryablePaymentAutomatonRunner.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), paymentStateContext.getInternalCallContext());
            return Iterables.any(transactions, new Predicate<PaymentTransactionModelDao>() {
                @Override
                public boolean apply(final PaymentTransactionModelDao input) {
                    return input.getTransactionStatus() == TransactionStatus.UNKNOWN &&
                           // Not strictly required
                           // (Note, we don't match on AttemptId as it is risky, the row on disk would match the first attempt, not necessarily the current one)
                           input.getAccountRecordId().equals(paymentStateContext.getInternalCallContext().getAccountRecordId());
                }
            });
        }
    }
}