Search in sources :

Example 21 with PaymentApiException

use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.

the class PluginControlPaymentProcessor method retryPaymentTransaction.

public void retryPaymentTransaction(final UUID attemptId, final List<String> paymentControlPluginNames, final InternalCallContext internalCallContext) {
    final PaymentAttemptModelDao attempt = paymentDao.getPaymentAttempt(attemptId, internalCallContext);
    log.info("Retrying attemptId='{}', paymentExternalKey='{}', transactionExternalKey='{}'. paymentControlPluginNames='{}'", attemptId, attempt.getPaymentExternalKey(), attempt.getTransactionExternalKey(), paymentControlPluginNames);
    final PaymentModelDao paymentModelDao = paymentDao.getPaymentByExternalKey(attempt.getPaymentExternalKey(), internalCallContext);
    final UUID paymentId = paymentModelDao != null ? paymentModelDao.getId() : null;
    final CallContext callContext = buildCallContext(internalCallContext);
    final String transactionType = TransactionType.PURCHASE.name();
    Account account = null;
    Payment payment = null;
    PaymentTransaction paymentTransaction = null;
    try {
        account = accountInternalApi.getAccountById(attempt.getAccountId(), internalCallContext);
        final State state = paymentControlStateMachineHelper.getState(attempt.getStateName());
        final Iterable<PluginProperty> pluginProperties = PluginPropertySerializer.deserialize(attempt.getPluginProperties());
        logEnterAPICall(log, transactionType, account, attempt.getPaymentMethodId(), paymentId, null, attempt.getAmount(), attempt.getCurrency(), attempt.getPaymentExternalKey(), attempt.getTransactionExternalKey(), null, paymentControlPluginNames);
        payment = pluginControlledPaymentAutomatonRunner.run(state, false, attempt.getTransactionType(), ControlOperation.valueOf(attempt.getTransactionType().toString()), account, attempt.getPaymentMethodId(), paymentId, attempt.getPaymentExternalKey(), attempt.getTransactionExternalKey(), // The amount on the payment attempt drives the amount of the new payment transaction
        attempt.getAmount(), attempt.getCurrency(), null, pluginProperties, paymentControlPluginNames, callContext, internalCallContext);
        log.debug("retryPaymentTransaction result: payment='{}'", payment);
        paymentTransaction = Iterables.<PaymentTransaction>find(Lists.<PaymentTransaction>reverse(payment.getTransactions()), new Predicate<PaymentTransaction>() {

            @Override
            public boolean apply(final PaymentTransaction input) {
                return attempt.getTransactionExternalKey().equals(input.getExternalKey());
            }
        });
    } catch (final AccountApiException e) {
        log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
    } catch (final PaymentApiException e) {
        // Log exception unless nothing left to be paid
        if (e.getCode() == ErrorCode.PAYMENT_PLUGIN_API_ABORTED.getCode() && paymentControlPluginNames != null && paymentControlPluginNames.size() == 1 && InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentControlPluginNames.get(0))) {
            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames));
        } else {
            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
        }
    } catch (final PluginPropertySerializerException e) {
        log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
    } catch (final MissingEntryException e) {
        log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
    } finally {
        logExitAPICall(log, transactionType, account, payment != null ? payment.getPaymentMethodId() : null, payment != null ? payment.getId() : null, paymentTransaction != null ? paymentTransaction.getId() : null, paymentTransaction != null ? paymentTransaction.getProcessedAmount() : null, paymentTransaction != null ? paymentTransaction.getProcessedCurrency() : null, payment != null ? payment.getExternalKey() : null, paymentTransaction != null ? paymentTransaction.getExternalKey() : null, paymentTransaction != null ? paymentTransaction.getTransactionStatus() : null, paymentControlPluginNames, null);
    }
}
Also used : PaymentAttemptModelDao(org.killbill.billing.payment.dao.PaymentAttemptModelDao) Account(org.killbill.billing.account.api.Account) PaymentModelDao(org.killbill.billing.payment.dao.PaymentModelDao) PluginPropertySerializerException(org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) InternalCallContext(org.killbill.billing.callcontext.InternalCallContext) CallContext(org.killbill.billing.util.callcontext.CallContext) Predicate(com.google.common.base.Predicate) PaymentTransaction(org.killbill.billing.payment.api.PaymentTransaction) PluginProperty(org.killbill.billing.payment.api.PluginProperty) Payment(org.killbill.billing.payment.api.Payment) State(org.killbill.automaton.State) AccountApiException(org.killbill.billing.account.api.AccountApiException) MissingEntryException(org.killbill.automaton.MissingEntryException) UUID(java.util.UUID)

Example 22 with PaymentApiException

use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.

the class IncompletePaymentAttemptTask method doIteration.

// Since the code is a bit tedious to follow, I'm adding some notes here on where isApiPayment is used (valid as of 09/19/2019 - might become stale!):
// * Passed through to plugins as PaymentControlContext#isApiPayment (e.g. InvoicePaymentControlPluginApi)
// * Used in PaymentEnteringStateCallback to decide whether to send a PaymentErrorInternalEvent
// To ensure that payments are retried, isApiPayment must be true (see https://github.com/killbill/killbill/issues/880).
@VisibleForTesting
public boolean doIteration(final PaymentAttemptModelDao attempt, final boolean isApiPayment) {
    // 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 InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(tenantContext.getTenantRecordId(), tenantContext.getAccountRecordId(), "AttemptCompletionJanitorTask", CallOrigin.INTERNAL, UserType.SYSTEM, UUIDs.randomUUID());
    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.updatePaymentAttemptWithProperties(attempt.getId(), attempt.getPaymentMethodId(), attempt.getTransactionId(), "ABORTED", // Keep the initial amount, as this will drive the retries
        attempt.getAmount(), attempt.getCurrency(), attempt.getPluginProperties(), internalCallContext);
        return true;
    }
    // at which point the attempt can also be transition to a different state.
    if (transaction.getTransactionStatus() == TransactionStatus.UNKNOWN) {
        return false;
    }
    try {
        log.info("Completing attemptId='{}', stateName='{}'", attempt.getId(), attempt.getStateName());
        final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
        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(), null, PluginPropertySerializer.deserialize(attempt.getPluginProperties()), internalCallContext, internalCallContextFactory.createCallContext(internalCallContext));
        // Normally set by leavingState Callback
        paymentStateContext.setAttemptId(attempt.getId());
        // Normally set by raw state machine
        paymentStateContext.setPaymentTransactionModelDao(transaction);
        // 
        // 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);
        return true;
    } 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);
    }
    return false;
}
Also used : Account(org.killbill.billing.account.api.Account) PaymentTransactionModelDao(org.killbill.billing.payment.dao.PaymentTransactionModelDao) PluginPropertySerializerException(org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException) InternalTenantContext(org.killbill.billing.callcontext.InternalTenantContext) AccountApiException(org.killbill.billing.account.api.AccountApiException) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) InternalCallContext(org.killbill.billing.callcontext.InternalCallContext) Predicate(com.google.common.base.Predicate) PaymentStateControlContext(org.killbill.billing.payment.core.sm.control.PaymentStateControlContext) VisibleForTesting(com.google.common.annotations.VisibleForTesting)

Example 23 with PaymentApiException

use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.

the class PaymentMethodProcessor method refreshPaymentMethods.

/**
 * This refreshed the payment methods from the plugin for cases when adding payment method does not flow through KB because of PCI compliance
 * issues. The logic below is not optimal because there is no atomicity in the step but the good news is that this is idempotent so can always be
 * replayed if necessary-- partial failure scenario.
 *
 * @param pluginName
 * @param account
 * @param context
 * @return the list of payment methods -- should be identical between KB, the plugin view-- if it keeps a state-- and the gateway.
 * @throws PaymentApiException
 */
public List<PaymentMethod> refreshPaymentMethods(final String pluginName, final Account account, final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context) throws PaymentApiException {
    // Don't hold the account lock while fetching the payment methods from the gateway as those could change anyway
    final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
    final List<PaymentMethodInfoPlugin> pluginPms;
    try {
        pluginPms = pluginApi.getPaymentMethods(account.getId(), true, properties, callContext);
        // The method should never return null by convention, but let's not trust the plugin...
        if (pluginPms == null) {
            log.debug("No payment methods defined on the account {} for plugin {}", account.getId(), pluginName);
            return ImmutableList.<PaymentMethod>of();
        }
    } catch (final PaymentPluginApiException e) {
        throw new PaymentApiException(e, ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
    }
    try {
        final PluginDispatcherReturnType<List<PaymentMethod>> result = new WithAccountLock<List<PaymentMethod>, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<List<PaymentMethod>>, PaymentApiException>() {

            @Override
            public PluginDispatcherReturnType<List<PaymentMethod>> doOperation() throws PaymentApiException {
                UUID defaultPaymentMethodId = null;
                final List<PaymentMethodInfoPlugin> pluginPmsWithId = new ArrayList<PaymentMethodInfoPlugin>();
                final List<PaymentMethodModelDao> finalPaymentMethods = new ArrayList<PaymentMethodModelDao>();
                for (final PaymentMethodInfoPlugin cur : pluginPms) {
                    // If the kbPaymentId is NULL, the plugin does not know about it, so we create a new UUID
                    final UUID paymentMethodId = cur.getPaymentMethodId() != null ? cur.getPaymentMethodId() : UUIDs.randomUUID();
                    final String externalKey = cur.getExternalPaymentMethodId() != null ? cur.getExternalPaymentMethodId() : paymentMethodId.toString();
                    final PaymentMethod input = new DefaultPaymentMethod(paymentMethodId, externalKey, account.getId(), pluginName);
                    final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(input.getId(), input.getExternalKey(), input.getCreatedDate(), input.getUpdatedDate(), input.getAccountId(), input.getPluginName(), input.isActive());
                    finalPaymentMethods.add(pmModel);
                    pluginPmsWithId.add(new DefaultPaymentMethodInfoPlugin(cur, paymentMethodId));
                    // will always return false - it's Kill Bill in that case which is responsible to manage default payment methods
                    if (cur.isDefault()) {
                        defaultPaymentMethodId = paymentMethodId;
                    }
                }
                final List<PaymentMethodModelDao> refreshedPaymentMethods = paymentDao.refreshPaymentMethods(pluginName, finalPaymentMethods, context);
                try {
                    pluginApi.resetPaymentMethods(account.getId(), pluginPmsWithId, properties, callContext);
                } catch (final PaymentPluginApiException e) {
                    throw new PaymentApiException(e, ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
                }
                try {
                    updateDefaultPaymentMethodIfNeeded(pluginName, account, defaultPaymentMethodId, context);
                } catch (final AccountApiException e) {
                    throw new PaymentApiException(e);
                }
                final List<PaymentMethod> result = ImmutableList.<PaymentMethod>copyOf(Collections2.transform(refreshedPaymentMethods, new Function<PaymentMethodModelDao, PaymentMethod>() {

                    @Override
                    public PaymentMethod apply(final PaymentMethodModelDao input) {
                        return new DefaultPaymentMethod(input, null);
                    }
                }));
                return PluginDispatcher.createPluginDispatcherReturnType(result);
            }
        });
        return result.getReturnType();
    } catch (final Exception e) {
        throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
    }
}
Also used : PluginDispatcherReturnType(org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType) DefaultPaymentMethodInfoPlugin(org.killbill.billing.payment.provider.DefaultPaymentMethodInfoPlugin) PaymentPluginApiException(org.killbill.billing.payment.plugin.api.PaymentPluginApiException) PaymentMethodModelDao(org.killbill.billing.payment.dao.PaymentMethodModelDao) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) DefaultPaymentMethod(org.killbill.billing.payment.api.DefaultPaymentMethod) PaymentControlApiAbortException(org.killbill.billing.payment.core.sm.control.PaymentControlApiAbortException) PaymentControlApiException(org.killbill.billing.control.plugin.api.PaymentControlApiException) PaymentPluginApiException(org.killbill.billing.payment.plugin.api.PaymentPluginApiException) AccountApiException(org.killbill.billing.account.api.AccountApiException) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) PaymentPluginApi(org.killbill.billing.payment.plugin.api.PaymentPluginApi) PaymentMethodInfoPlugin(org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin) DefaultPaymentMethodInfoPlugin(org.killbill.billing.payment.provider.DefaultPaymentMethodInfoPlugin) AccountApiException(org.killbill.billing.account.api.AccountApiException) PaymentMethod(org.killbill.billing.payment.api.PaymentMethod) DefaultPaymentMethod(org.killbill.billing.payment.api.DefaultPaymentMethod) List(java.util.List) ArrayList(java.util.ArrayList) ImmutableList(com.google.common.collect.ImmutableList) UUID(java.util.UUID)

Example 24 with PaymentApiException

use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.

the class PaymentMethodProcessor method buildDefaultPaymentMethod.

private PaymentMethod buildDefaultPaymentMethod(final PaymentMethodModelDao paymentMethodModelDao, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
    final PaymentMethodPlugin paymentMethodPlugin;
    if (withPluginInfo) {
        try {
            final PaymentPluginApi pluginApi = getPaymentPluginApi(paymentMethodModelDao.getPluginName());
            paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), properties, tenantContext);
        } catch (final PaymentPluginApiException e) {
            throw new PaymentApiException(e, ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
        }
    } else {
        paymentMethodPlugin = null;
    }
    return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
}
Also used : PaymentPluginApi(org.killbill.billing.payment.plugin.api.PaymentPluginApi) PaymentPluginApiException(org.killbill.billing.payment.plugin.api.PaymentPluginApiException) DefaultNoOpPaymentMethodPlugin(org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin) PaymentMethodPlugin(org.killbill.billing.payment.api.PaymentMethodPlugin) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) DefaultPaymentMethod(org.killbill.billing.payment.api.DefaultPaymentMethod)

Example 25 with PaymentApiException

use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.

the class PaymentProcessor method findTransactionToCompleteAndRunSanityChecks.

private PaymentTransactionModelDao findTransactionToCompleteAndRunSanityChecks(final PaymentModelDao paymentModelDao, final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, final PaymentStateContext paymentStateContext, final InternalCallContext internalCallContext) throws PaymentApiException {
    final Collection<PaymentTransactionModelDao> completionCandidates = new LinkedList<PaymentTransactionModelDao>();
    for (final PaymentTransactionModelDao paymentTransactionModelDao : paymentTransactionsForCurrentPayment) {
        // Check if we already have a transaction for that id or key
        if (!(paymentStateContext.getTransactionId() != null && paymentTransactionModelDao.getId().equals(paymentStateContext.getTransactionId())) && !(paymentStateContext.getPaymentTransactionExternalKey() != null && paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()))) {
            // Sanity: if not, prevent multiple PENDING transactions for initial calls (cannot be enforced by the state machine unfortunately)
            if ((paymentTransactionModelDao.getTransactionType() == TransactionType.AUTHORIZE || paymentTransactionModelDao.getTransactionType() == TransactionType.PURCHASE || paymentTransactionModelDao.getTransactionType() == TransactionType.CREDIT) && paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING) {
                throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentTransactionModelDao.getTransactionType(), paymentModelDao.getStateName());
            } else {
                continue;
            }
        }
        // Sanity: if we already have a transaction for that id or key, the transaction type must match
        if (paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
            throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "transactionType", String.format("%s doesn't match existing transaction type %s", paymentStateContext.getTransactionType(), paymentTransactionModelDao.getTransactionType()));
        }
        // UNKNOWN transactions are potential candidates, we'll invoke the Janitor first though
        if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING || paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
            completionCandidates.add(paymentTransactionModelDao);
        }
    }
    Preconditions.checkState(Iterables.size(completionCandidates) <= 1, "There should be at most one completion candidate");
    return Iterables.<PaymentTransactionModelDao>getLast(completionCandidates, null);
}
Also used : PaymentTransactionModelDao(org.killbill.billing.payment.dao.PaymentTransactionModelDao) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) LinkedList(java.util.LinkedList)

Aggregations

PaymentApiException (org.killbill.billing.payment.api.PaymentApiException)57 UUID (java.util.UUID)20 Test (org.testng.annotations.Test)19 PaymentTransactionModelDao (org.killbill.billing.payment.dao.PaymentTransactionModelDao)14 Account (org.killbill.billing.account.api.Account)12 Payment (org.killbill.billing.payment.api.Payment)12 PaymentAttemptModelDao (org.killbill.billing.payment.dao.PaymentAttemptModelDao)11 BigDecimal (java.math.BigDecimal)10 OperationException (org.killbill.automaton.OperationException)10 PaymentModelDao (org.killbill.billing.payment.dao.PaymentModelDao)9 Predicate (com.google.common.base.Predicate)7 PaymentControlApiException (org.killbill.billing.control.plugin.api.PaymentControlApiException)7 PluginDispatcherReturnType (org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType)7 PaymentPluginApi (org.killbill.billing.payment.plugin.api.PaymentPluginApi)7 LinkedList (java.util.LinkedList)6 AccountApiException (org.killbill.billing.account.api.AccountApiException)6 Invoice (org.killbill.billing.invoice.api.Invoice)6 PluginProperty (org.killbill.billing.payment.api.PluginProperty)6 PaymentPluginApiException (org.killbill.billing.payment.plugin.api.PaymentPluginApiException)6 DateTime (org.joda.time.DateTime)5