killbill-uncached

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index ae3536a..998ac96 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -375,7 +375,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
         accountInternalApi.removePaymentMethod(account.getId(), internalCallContext);
 
         // Create subscription
-        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR);
+        final Subscription baseSubscription = createSubscriptionAndCheckForCompletion(bundle.getId(), productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE);
 
         invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
         invoiceChecker.checkChargedThroughDate(baseSubscription.getId(), new LocalDate(2012, 5, 1), callContext);
diff --git a/payment/src/main/java/com/ning/billing/payment/bus/InvoiceHandler.java b/payment/src/main/java/com/ning/billing/payment/bus/InvoiceHandler.java
index 760378b..0fc54ff 100644
--- a/payment/src/main/java/com/ning/billing/payment/bus/InvoiceHandler.java
+++ b/payment/src/main/java/com/ning/billing/payment/bus/InvoiceHandler.java
@@ -66,8 +66,8 @@ public class InvoiceHandler {
             log.error("Failed to process invoice payment", e);
         } catch (PaymentApiException e) {
             // Log as error unless:
-            if (e.getCode() != ErrorCode.PAYMENT_NULL_INVOICE.getCode() /*  Nothing to left be paid*/ &&
-                e.getCode() != ErrorCode.PAYMENT_CREATE_PAYMENT.getCode() /* User payment error */) {
+            if (e.getCode() != ErrorCode.PAYMENT_NULL_INVOICE.getCode() /* Nothing left to be paid */ &&
+                    e.getCode() != ErrorCode.PAYMENT_CREATE_PAYMENT.getCode() /* User payment error */) {
                 log.error("Failed to process invoice payment {}", e.toString());
             }
         }
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
index eeb652f..5d79fb9 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
@@ -16,37 +16,24 @@
 
 package com.ning.billing.payment.core;
 
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-
-import com.ning.billing.payment.api.DefaultPayment;
-import com.ning.billing.payment.api.DefaultPaymentErrorEvent;
-import com.ning.billing.payment.api.DefaultPaymentInfoEvent;
-import com.ning.billing.payment.api.DefaultPaymentPluginErrorEvent;
-import com.ning.billing.payment.api.Payment;
-import com.ning.billing.payment.api.PaymentApiException;
-import com.ning.billing.payment.api.PaymentStatus;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.inject.name.Named;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.bus.api.PersistentBus;
+import com.ning.billing.clock.Clock;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.osgi.api.OSGIServiceRegistration;
+import com.ning.billing.payment.api.DefaultPayment;
+import com.ning.billing.payment.api.DefaultPaymentErrorEvent;
+import com.ning.billing.payment.api.DefaultPaymentInfoEvent;
+import com.ning.billing.payment.api.DefaultPaymentPluginErrorEvent;
+import com.ning.billing.payment.api.Payment;
+import com.ning.billing.payment.api.PaymentApiException;
+import com.ning.billing.payment.api.PaymentStatus;
 import com.ning.billing.payment.dao.PaymentAttemptModelDao;
 import com.ning.billing.payment.dao.PaymentDao;
 import com.ning.billing.payment.dao.PaymentModelDao;
@@ -60,7 +47,6 @@ import com.ning.billing.payment.retry.FailedPaymentRetryService.FailedPaymentRet
 import com.ning.billing.payment.retry.PluginFailureRetryService.PluginFailureRetryServiceScheduler;
 import com.ning.billing.util.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalTenantContext;
-import com.ning.billing.clock.Clock;
 import com.ning.billing.util.config.PaymentConfig;
 import com.ning.billing.util.events.BusInternalEvent;
 import com.ning.billing.util.events.PaymentErrorInternalEvent;
@@ -68,10 +54,21 @@ import com.ning.billing.util.globallocker.GlobalLocker;
 import com.ning.billing.util.svcapi.account.AccountInternalApi;
 import com.ning.billing.util.svcapi.invoice.InvoiceInternalApi;
 import com.ning.billing.util.svcapi.tag.TagInternalApi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.inject.name.Named;
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import static com.ning.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
 
@@ -169,45 +166,45 @@ public class PaymentProcessor extends ProcessorBase {
 
         try {
             voidPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<Void>(locker,
-                                                                                           account.getExternalKey(),
-                                                                                           new WithAccountLockCallback<Void>() {
-
-                                                                                               @Override
-                                                                                               public Void doOperation() throws PaymentApiException {
-
-                                                                                                   final List<PaymentModelDao> payments = paymentDao.getPaymentsForAccount(account.getId(), context);
-                                                                                                   final Collection<PaymentModelDao> paymentsToBeCompleted = Collections2.filter(payments, new Predicate<PaymentModelDao>() {
-                                                                                                       @Override
-                                                                                                       public boolean apply(final PaymentModelDao in) {
-                                                                                                           // Payments left in AUTO_PAY_OFF or for which we did not retry enough
-                                                                                                           return (in.getPaymentStatus() == PaymentStatus.AUTO_PAY_OFF ||
-                                                                                                                   in.getPaymentStatus() == PaymentStatus.PAYMENT_FAILURE ||
-                                                                                                                   in.getPaymentStatus() == PaymentStatus.PLUGIN_FAILURE ||
-                                                                                                                   in.getPaymentStatus() == PaymentStatus.UNKNOWN);
-                                                                                                       }
-                                                                                                   });
-                                                                                                   // Insert one retry event for each payment left in AUTO_PAY_OFF
-                                                                                                   for (PaymentModelDao cur : paymentsToBeCompleted) {
-                                                                                                       switch (cur.getPaymentStatus()) {
-                                                                                                           case AUTO_PAY_OFF:
-                                                                                                               autoPayoffRetryService.scheduleRetry(cur.getId(), clock.getUTCNow());
-                                                                                                               break;
-                                                                                                           case PAYMENT_FAILURE:
-                                                                                                               scheduleRetryOnPaymentFailure(cur.getId(), context);
-                                                                                                               break;
-                                                                                                           case PLUGIN_FAILURE:
-                                                                                                           case UNKNOWN:
-                                                                                                               scheduleRetryOnPluginFailure(cur.getId(), context);
-                                                                                                               break;
-                                                                                                           default:
-                                                                                                               // Impossible...
-                                                                                                               throw new RuntimeException("Unexpected case " + cur.getPaymentStatus());
-                                                                                                       }
-
-                                                                                                   }
-                                                                                                   return null;
-                                                                                               }
-                                                                                           }));
+                    account.getExternalKey(),
+                    new WithAccountLockCallback<Void>() {
+
+                        @Override
+                        public Void doOperation() throws PaymentApiException {
+
+                            final List<PaymentModelDao> payments = paymentDao.getPaymentsForAccount(account.getId(), context);
+                            final Collection<PaymentModelDao> paymentsToBeCompleted = Collections2.filter(payments, new Predicate<PaymentModelDao>() {
+                                @Override
+                                public boolean apply(final PaymentModelDao in) {
+                                    // Payments left in AUTO_PAY_OFF or for which we did not retry enough
+                                    return (in.getPaymentStatus() == PaymentStatus.AUTO_PAY_OFF ||
+                                            in.getPaymentStatus() == PaymentStatus.PAYMENT_FAILURE ||
+                                            in.getPaymentStatus() == PaymentStatus.PLUGIN_FAILURE ||
+                                            in.getPaymentStatus() == PaymentStatus.UNKNOWN);
+                                }
+                            });
+                            // Insert one retry event for each payment left in AUTO_PAY_OFF
+                            for (PaymentModelDao cur : paymentsToBeCompleted) {
+                                switch (cur.getPaymentStatus()) {
+                                    case AUTO_PAY_OFF:
+                                        autoPayoffRetryService.scheduleRetry(cur.getId(), clock.getUTCNow());
+                                        break;
+                                    case PAYMENT_FAILURE:
+                                        scheduleRetryOnPaymentFailure(cur.getId(), context);
+                                        break;
+                                    case PLUGIN_FAILURE:
+                                    case UNKNOWN:
+                                        scheduleRetryOnPluginFailure(cur.getId(), context);
+                                        break;
+                                    default:
+                                        // Impossible...
+                                        throw new RuntimeException("Unexpected case " + cur.getPaymentStatus());
+                                }
+
+                            }
+                            return null;
+                        }
+                    }));
         } catch (TimeoutException e) {
             throw new PaymentApiException(ErrorCode.UNEXPECTED_ERROR, "Unexpected timeout for payment creation (AUTO_PAY_OFF)");
         }
@@ -216,61 +213,74 @@ public class PaymentProcessor extends ProcessorBase {
     public Payment createPayment(final Account account, final UUID invoiceId, @Nullable final BigDecimal inputAmount,
                                  final InternalCallContext context, final boolean isInstantPayment, final boolean isExternalPayment)
             throws PaymentApiException {
-        // Use the special external payment plugin to handle external payments
-        final PaymentPluginApi plugin;
-        final UUID paymentMethodId;
-        try {
-            if (isExternalPayment) {
-                plugin = paymentMethodProcessor.getExternalPaymentProviderPlugin(account, context);
-                paymentMethodId = paymentMethodProcessor.getExternalPaymentMethod(account, context).getId();
-            } else {
-                plugin = getPaymentProviderPlugin(account, context);
-                paymentMethodId = account.getPaymentMethodId();
-            }
-        } catch (PaymentApiException e) {
-            // This event will be caught by overdue to refresh the overdue state, if needed.
-            // Note that at this point, we don't know the exact invoice balance (see getAndValidatePaymentAmount() below).
-            // This means that events will be posted for null and zero dollar invoices (e.g. trials).
-            final PaymentErrorInternalEvent event = new DefaultPaymentErrorEvent(account.getId(), invoiceId, null,
-                                                                                 ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.toString(),
-                                                                                 context.getAccountRecordId(),
-                                                                                 context.getTenantRecordId(),
-                                                                                 context.getUserToken()
-            );
-            postPaymentEvent(event, account.getId(), context);
-            throw e;
+        // If this is an external payment, retrieve the external payment method first.
+        // We need to do this without the lock, because getExternalPaymentProviderPlugin will acquire the lock
+        // if it needs to create an external payment method (for the first external payment for that account).
+        // We don't want to retrieve any other payment method here, because we need to validate the invoice amount first
+        // (to avoid throwing an exception if there is nothing to pay).
+        final PaymentPluginApi externalPaymentPlugin;
+        if (isExternalPayment) {
+            externalPaymentPlugin = paymentMethodProcessor.getExternalPaymentProviderPlugin(account, context);
+        } else {
+            externalPaymentPlugin = null;
         }
 
         try {
             return paymentPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<Payment>(locker,
-                                                                                                        account.getExternalKey(),
-                                                                                                        new WithAccountLockCallback<Payment>() {
-
-                                                                                                            @Override
-                                                                                                            public Payment doOperation() throws PaymentApiException {
-
-
-                                                                                                                try {
-                                                                                                                    final Invoice invoice = rebalanceAndGetInvoice(account.getId(), invoiceId, context);
-                                                                                                                    if (invoice == null || invoice.isMigrationInvoice()) {
-                                                                                                                        log.error("Received invoice for payment that is a migration invoice - don't know how to handle those yet: {}", invoice);
-                                                                                                                        return null;
-                                                                                                                    }
-
-                                                                                                                    final boolean isAccountAutoPayOff = isAccountAutoPayOff(account.getId(), context);
-                                                                                                                    setUnsaneAccount_AUTO_PAY_OFFWithAccountLock(account.getId(), paymentMethodId, isAccountAutoPayOff, context, isInstantPayment);
-
-                                                                                                                    final BigDecimal requestedAmount = getAndValidatePaymentAmount(invoice, inputAmount, isInstantPayment);
-                                                                                                                    if (!isInstantPayment && isAccountAutoPayOff) {
-                                                                                                                        return processNewPaymentForAutoPayOffWithAccountLocked(paymentMethodId, account, invoice, requestedAmount, context);
-                                                                                                                    } else {
-                                                                                                                        return processNewPaymentWithAccountLocked(paymentMethodId, plugin, account, invoice, requestedAmount, isInstantPayment, context);
-                                                                                                                    }
-                                                                                                                } catch (InvoiceApiException e) {
-                                                                                                                    throw new PaymentApiException(e);
-                                                                                                                }
-                                                                                                            }
-                                                                                                        }));
+                    account.getExternalKey(),
+                    new WithAccountLockCallback<Payment>() {
+
+                        @Override
+                        public Payment doOperation() throws PaymentApiException {
+
+                            try {
+                                // First, rebalance CBA and retrieve the latest version of the invoice
+                                final Invoice invoice = rebalanceAndGetInvoice(account.getId(), invoiceId, context);
+                                if (invoice == null || invoice.isMigrationInvoice()) {
+                                    log.error("Received invoice for payment that is a migration invoice - don't know how to handle those yet: {}", invoice);
+                                    return null;
+                                }
+
+                                // Second, validate the payment amount. We want to bail as early as possible if e.g. the balance is zero
+                                final BigDecimal requestedAmount = getAndValidatePaymentAmount(invoice, inputAmount, isInstantPayment);
+
+                                // Third, retrieve the payment method and associated plugin
+                                final PaymentPluginApi plugin;
+                                final UUID paymentMethodId;
+                                try {
+                                    // Use the special external payment plugin to handle external payments
+                                    if (isExternalPayment) {
+                                        plugin = externalPaymentPlugin;
+                                        paymentMethodId = paymentMethodProcessor.getExternalPaymentMethod(account, context).getId();
+                                    } else {
+                                        plugin = getPaymentProviderPlugin(account, context);
+                                        paymentMethodId = account.getPaymentMethodId();
+                                    }
+                                } catch (PaymentApiException e) {
+                                    // This event will be caught by overdue to refresh the overdue state, if needed.
+                                    // Note that at this point, we don't know the exact invoice balance (see getAndValidatePaymentAmount() below).
+                                    // This means that events will be posted for null and zero dollar invoices (e.g. trials).
+                                    final PaymentErrorInternalEvent event = new DefaultPaymentErrorEvent(account.getId(), invoiceId, null,
+                                            ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.toString(),
+                                            context.getAccountRecordId(), context.getTenantRecordId(),
+                                            context.getUserToken());
+                                    postPaymentEvent(event, account.getId(), context);
+                                    throw e;
+                                }
+
+                                final boolean isAccountAutoPayOff = isAccountAutoPayOff(account.getId(), context);
+                                setUnsaneAccount_AUTO_PAY_OFFWithAccountLock(account.getId(), paymentMethodId, isAccountAutoPayOff, context, isInstantPayment);
+
+                                if (!isInstantPayment && isAccountAutoPayOff) {
+                                    return processNewPaymentForAutoPayOffWithAccountLocked(paymentMethodId, account, invoice, requestedAmount, context);
+                                } else {
+                                    return processNewPaymentWithAccountLocked(paymentMethodId, plugin, account, invoice, requestedAmount, isInstantPayment, context);
+                                }
+                            } catch (InvoiceApiException e) {
+                                throw new PaymentApiException(e);
+                            }
+                        }
+                    }));
         } catch (TimeoutException e) {
             if (isInstantPayment) {
                 throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_TIMEOUT, account.getId(), invoiceId);
@@ -298,13 +308,13 @@ public class PaymentProcessor extends ProcessorBase {
 
         final PaymentModelDao lastPaymentForPaymentMethod = paymentDao.getLastPaymentForPaymentMethod(accountId, paymentMethodId, context);
         final boolean isLastPaymentForPaymentMethodBad = lastPaymentForPaymentMethod != null &&
-                                                         (lastPaymentForPaymentMethod.getPaymentStatus() == PaymentStatus.PLUGIN_FAILURE_ABORTED ||
-                                                          lastPaymentForPaymentMethod.getPaymentStatus() == PaymentStatus.UNKNOWN);
+                (lastPaymentForPaymentMethod.getPaymentStatus() == PaymentStatus.PLUGIN_FAILURE_ABORTED ||
+                        lastPaymentForPaymentMethod.getPaymentStatus() == PaymentStatus.UNKNOWN);
 
 
         if (isLastPaymentForPaymentMethodBad &&
-            !isInstantPayment &&
-            !isAccountAutoPayOff) {
+                !isInstantPayment &&
+                !isAccountAutoPayOff) {
             log.warn(String.format("Setting account %s into AUTO_PAY_OFF because of bad payment %s", accountId, lastPaymentForPaymentMethod.getId()));
             setAccountAutoPayOff(accountId, context);
         }
@@ -318,10 +328,10 @@ public class PaymentProcessor extends ProcessorBase {
             throw new PaymentApiException(ErrorCode.PAYMENT_NULL_INVOICE, invoice.getId());
         }
         if (isInstantPayment &&
-            inputAmount != null &&
-            invoice.getBalance().compareTo(inputAmount) < 0) {
+                inputAmount != null &&
+                invoice.getBalance().compareTo(inputAmount) < 0) {
             throw new PaymentApiException(ErrorCode.PAYMENT_AMOUNT_DENIED,
-                                          invoice.getId(), inputAmount.floatValue(), invoice.getBalance().floatValue());
+                    invoice.getId(), inputAmount.floatValue(), invoice.getBalance().floatValue());
         }
         final BigDecimal result = inputAmount != null ? inputAmount : invoice.getBalance();
         return result.setScale(2, RoundingMode.HALF_UP);
@@ -345,9 +355,9 @@ public class PaymentProcessor extends ProcessorBase {
     public void retryPaymentFromApi(final UUID paymentId, final InternalCallContext context) {
         log.info("Retrying payment " + paymentId + " time = " + clock.getUTCNow());
         retryFailedPaymentInternal(paymentId, context, PaymentStatus.UNKNOWN,
-                                   PaymentStatus.AUTO_PAY_OFF,
-                                   PaymentStatus.PAYMENT_FAILURE,
-                                   PaymentStatus.PLUGIN_FAILURE);
+                PaymentStatus.AUTO_PAY_OFF,
+                PaymentStatus.PAYMENT_FAILURE,
+                PaymentStatus.PLUGIN_FAILURE);
     }
 
     private void retryFailedPaymentInternal(final UUID paymentId, final InternalCallContext context, final PaymentStatus... expectedPaymentStates) {
@@ -369,42 +379,42 @@ public class PaymentProcessor extends ProcessorBase {
             final PaymentPluginApi plugin = getPaymentProviderPlugin(account, context);
 
             voidPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<Void>(locker,
-                                                                                           account.getExternalKey(),
-                                                                                           new WithAccountLockCallback<Void>() {
-
-                                                                                               @Override
-                                                                                               public Void doOperation() throws PaymentApiException {
-                                                                                                   try {
-                                                                                                       // Fetch again with account lock this time
-                                                                                                       final PaymentModelDao payment = paymentDao.getPayment(paymentId, context);
-                                                                                                       boolean foundExpectedState = false;
-                                                                                                       for (final PaymentStatus cur : expectedPaymentStates) {
-                                                                                                           if (payment.getPaymentStatus() == cur) {
-                                                                                                               foundExpectedState = true;
-                                                                                                               break;
-                                                                                                           }
-                                                                                                       }
-                                                                                                       if (!foundExpectedState) {
-                                                                                                           log.info("Aborted retry for payment {} because it is {} state", paymentId, payment.getPaymentStatus());
-                                                                                                           return null;
-                                                                                                       }
-
-                                                                                                       final Invoice invoice = rebalanceAndGetInvoice(payment.getAccountId(), payment.getInvoiceId(), context);
-                                                                                                       if (invoice == null || invoice.isMigrationInvoice()) {
-                                                                                                           return null;
-                                                                                                       }
-                                                                                                       if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
-                                                                                                           log.info("Aborted retry for payment {} because invoice has been paid", paymentId);
-                                                                                                           setTerminalStateOnRetryWithAccountLocked(account, invoice, payment, invoice.getBalance(), "Paid invoice", context);
-                                                                                                           return null;
-                                                                                                       }
-                                                                                                       processRetryPaymentWithAccountLocked(plugin, account, invoice, payment, invoice.getBalance(), context);
-                                                                                                       return null;
-                                                                                                   } catch (InvoiceApiException e) {
-                                                                                                       throw new PaymentApiException(e);
-                                                                                                   }
-                                                                                               }
-                                                                                           }));
+                    account.getExternalKey(),
+                    new WithAccountLockCallback<Void>() {
+
+                        @Override
+                        public Void doOperation() throws PaymentApiException {
+                            try {
+                                // Fetch again with account lock this time
+                                final PaymentModelDao payment = paymentDao.getPayment(paymentId, context);
+                                boolean foundExpectedState = false;
+                                for (final PaymentStatus cur : expectedPaymentStates) {
+                                    if (payment.getPaymentStatus() == cur) {
+                                        foundExpectedState = true;
+                                        break;
+                                    }
+                                }
+                                if (!foundExpectedState) {
+                                    log.info("Aborted retry for payment {} because it is {} state", paymentId, payment.getPaymentStatus());
+                                    return null;
+                                }
+
+                                final Invoice invoice = rebalanceAndGetInvoice(payment.getAccountId(), payment.getInvoiceId(), context);
+                                if (invoice == null || invoice.isMigrationInvoice()) {
+                                    return null;
+                                }
+                                if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
+                                    log.info("Aborted retry for payment {} because invoice has been paid", paymentId);
+                                    setTerminalStateOnRetryWithAccountLocked(account, invoice, payment, invoice.getBalance(), "Paid invoice", context);
+                                    return null;
+                                }
+                                processRetryPaymentWithAccountLocked(plugin, account, invoice, payment, invoice.getBalance(), context);
+                                return null;
+                            } catch (InvoiceApiException e) {
+                                throw new PaymentApiException(e);
+                            }
+                        }
+                    }));
         } catch (AccountApiException e) {
             log.error(String.format("Failed to retry payment for paymentId %s", paymentId), e);
         } catch (PaymentApiException e) {
@@ -510,19 +520,19 @@ public class PaymentProcessor extends ProcessorBase {
 
                     payment = paymentDao.getPayment(paymentInput.getId(), context);
                     invoiceApi.notifyOfPayment(invoice.getId(),
-                                               payment.getAmount(),
-                                               payment.getCurrency(),
-                                               payment.getId(),
-                                               payment.getEffectiveDate(),
-                                               context);
+                            payment.getAmount(),
+                            payment.getCurrency(),
+                            payment.getId(),
+                            payment.getEffectiveDate(),
+                            context);
 
                     // Create Bus event
                     event = new DefaultPaymentInfoEvent(account.getId(),
-                                                        invoice.getId(), payment.getId(), payment.getAmount(), payment.getPaymentNumber(), paymentStatus,
-                                                        payment.getEffectiveDate(),
-                                                        context.getAccountRecordId(),
-                                                        context.getTenantRecordId(),
-                                                        context.getUserToken()
+                            invoice.getId(), payment.getId(), payment.getAmount(), payment.getPaymentNumber(), paymentStatus,
+                            payment.getEffectiveDate(),
+                            context.getAccountRecordId(),
+                            context.getTenantRecordId(),
+                            context.getUserToken()
                     );
                     break;
 
@@ -538,10 +548,10 @@ public class PaymentProcessor extends ProcessorBase {
                     paymentDao.updateStatusAndEffectiveDateForPaymentWithAttempt(paymentInput.getId(), paymentStatus, clock.getUTCNow(), attemptInput.getId(), paymentPluginInfo.getGatewayErrorCode(), paymentPluginInfo.getGatewayError(), context);
 
                     log.info(String.format("Could not process payment for account %s, invoice %s, error = %s",
-                                           account.getId(), invoice.getId(), paymentPluginInfo.getGatewayError()));
+                            account.getId(), invoice.getId(), paymentPluginInfo.getGatewayError()));
 
                     event = new DefaultPaymentErrorEvent(account.getId(), invoice.getId(), paymentInput.getId(), paymentPluginInfo.getGatewayError(),
-                                                         context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()
+                            context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()
                     );
                     throw new PaymentApiException(ErrorCode.PAYMENT_CREATE_PAYMENT, account.getId(), paymentPluginInfo.getGatewayError());
 
@@ -586,7 +596,7 @@ public class PaymentProcessor extends ProcessorBase {
     private PaymentStatus scheduleRetryOnPaymentFailure(final UUID paymentId, final InternalTenantContext context) {
         final List<PaymentAttemptModelDao> allAttempts = paymentDao.getAttemptsForPayment(paymentId, context);
         final int retryAttempt = getNumberAttemptsInState(allAttempts,
-                                                          PaymentStatus.UNKNOWN, PaymentStatus.PAYMENT_FAILURE);
+                PaymentStatus.UNKNOWN, PaymentStatus.PAYMENT_FAILURE);
 
 
         final boolean isScheduledForRetry = failedPaymentRetryService.scheduleRetry(paymentId, retryAttempt);