killbill-aplcache

Merge remote-tracking branch 'origin/master' into changes-for-0.15.2 Signed-off-by:

7/15/2015 7:51:10 AM

Changes

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

catalog/pom.xml 2(+1 -1)

currency/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

jaxrs/pom.xml 2(+1 -1)

junction/pom.xml 2(+1 -1)

NEWS 4(+4 -0)

overdue/pom.xml 2(+1 -1)

payment/pom.xml 2(+1 -1)

pom.xml 2(+1 -1)

profiles/pom.xml 2(+1 -1)

tenant/pom.xml 2(+1 -1)

usage/pom.xml 2(+1 -1)

util/pom.xml 2(+1 -1)

Details

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index 8dd4baf..248888c 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index 693edc4..24958d8 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-internal-api</artifactId>

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 0a7ab00..765fdf3 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index 5543d21..165e905 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>

currency/pom.xml 2(+1 -1)

diff --git a/currency/pom.xml b/currency/pom.xml
index 9fb4e1d..e03a711 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-currency</artifactId>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 6d0260b..7b012fa 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index 7646dad..987a318 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>

jaxrs/pom.xml 2(+1 -1)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index c365864..401b515 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-jaxrs</artifactId>

junction/pom.xml 2(+1 -1)

diff --git a/junction/pom.xml b/junction/pom.xml
index fca6924..6445dde 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-junction</artifactId>

NEWS 4(+4 -0)

diff --git a/NEWS b/NEWS
index e2d4501..c41cb0c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,7 @@
+0.15.1
+    See https://github.com/killbill/killbill/issues?q=milestone%3ARelease-0.15.1+is%3Aclosed
+    Also Add new support for entitlement plugin API
+
 0.15.0
     See https://github.com/killbill/killbill/issues?q=milestone%3ARelease-0.15.0+is%3Aclosed
 

overdue/pom.xml 2(+1 -1)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index 7d01545..1892035 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-overdue</artifactId>

payment/pom.xml 2(+1 -1)

diff --git a/payment/pom.xml b/payment/pom.xml
index 56ac663..45ce609 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 0af54d5..53e1606 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -322,7 +322,6 @@ public class PaymentProcessor extends ProcessorBase {
                                      final boolean shouldLockAccountAndDispatch, @Nullable final OperationResult overridePluginOperationResult,
                                      final Iterable<PluginProperty> properties,
                                      final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
-        validateUniqueTransactionExternalKey(paymentTransactionExternalKey, internalCallContext);
         final UUID nonNullPaymentId = paymentAutomatonRunner.run(isApiPayment,
                                                                  transactionType,
                                                                  account,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
index ae4e3a2..f8c2d67 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
@@ -164,27 +164,8 @@ public abstract class ProcessorBase {
         return internalCallContextFactory.createCallContext(context);
     }
 
-    protected void validateUniqueTransactionExternalKey(@Nullable final String transactionExternalKey, final InternalTenantContext tenantContext) throws PaymentApiException {
-        // If no key specified, system will allocate a unique one later.
-        if (transactionExternalKey == null) {
-            return;
-        }
-
-        final List<PaymentTransactionModelDao> transactions = paymentDao.getPaymentTransactionsByExternalKey(transactionExternalKey, tenantContext);
-        final PaymentTransactionModelDao transactionAlreadyExists = Iterables.tryFind(transactions, new Predicate<PaymentTransactionModelDao>() {
-            @Override
-            public boolean apply(final PaymentTransactionModelDao input) {
-                return input.getTransactionStatus() == TransactionStatus.SUCCESS;
-            }
-        }).orNull();
-        if (transactionAlreadyExists != null) {
-            throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, transactionExternalKey);
-        }
-    }
-
     // TODO Rename - there is no lock!
     public interface WithAccountLockCallback<PluginDispatcherReturnType, ExceptionType extends Exception> {
-
         public PluginDispatcherReturnType doOperation() throws ExceptionType;
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlCompleted.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlCompleted.java
index b8dc704..ea11242 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlCompleted.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlCompleted.java
@@ -17,6 +17,7 @@
 
 package org.killbill.billing.payment.core.sm.control;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.killbill.automaton.Operation.OperationCallback;
@@ -26,19 +27,27 @@ import org.killbill.automaton.State.EnteringStateCallback;
 import org.killbill.automaton.State.LeavingStateCallback;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
+import org.killbill.billing.payment.api.TransactionStatus;
 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 final PaymentStateControlContext paymentStateContext;
     private final RetryServiceScheduler retryServiceScheduler;
+    private final State retriedState;
+
     private PluginControlPaymentAutomatonRunner retryablePaymentAutomatonRunner;
 
     public DefaultControlCompleted(final PluginControlPaymentAutomatonRunner retryablePaymentAutomatonRunner, final PaymentStateControlContext paymentStateContext,
-                                   final RetryServiceScheduler retryServiceScheduler) {
+                                   final State retriedState, final RetryServiceScheduler retryServiceScheduler) {
         this.retryablePaymentAutomatonRunner = retryablePaymentAutomatonRunner;
         this.paymentStateContext = paymentStateContext;
+        this.retriedState = retriedState;
         this.retryServiceScheduler = retryServiceScheduler;
     }
 
@@ -50,9 +59,30 @@ public class DefaultControlCompleted implements EnteringStateCallback {
                                    null;
         retryablePaymentAutomatonRunner.getPaymentDao().updatePaymentAttempt(attempt.getId(), transactionId, state.getName(), paymentStateContext.getInternalCallContext());
 
-        if ("RETRIED".equals(state.getName())) {
+        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());
+                }
+            });
+        }
+    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java
index 8cdc6eb..9860f6b 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentEnteringStateCallback.java
@@ -52,6 +52,10 @@ public abstract class PaymentEnteringStateCallback implements EnteringStateCallb
     public void enteringState(final State newState, final Operation.OperationCallback operationCallback, final OperationResult operationResult, final LeavingStateCallback leavingStateCallback) {
         logger.debug("Entering state {} with result {}", newState.getName(), operationResult);
 
+        if (paymentStateContext.isSkipOperationForUnknownTransaction()) {
+            return;
+        }
+
         // If the transaction was not created -- for instance we had an exception in leavingState callback then we bail; if not, then update state:
         if (paymentStateContext.getPaymentTransactionModelDao() != null && paymentStateContext.getPaymentTransactionModelDao().getId() != null) {
             final PaymentTransactionInfoPlugin paymentInfoPlugin = paymentStateContext.getPaymentTransactionInfoPlugin();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
index 18dd659..6fb17d7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentLeavingStateCallback.java
@@ -17,17 +17,24 @@
 
 package org.killbill.billing.payment.core.sm.payments;
 
+import java.util.List;
+
 import org.killbill.automaton.OperationException;
 import org.killbill.automaton.State;
 import org.killbill.automaton.State.LeavingStateCallback;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.TransactionStatus;
 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.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
 public abstract class PaymentLeavingStateCallback implements LeavingStateCallback {
 
     private final Logger logger = LoggerFactory.getLogger(PaymentLeavingStateCallback.class);
@@ -51,16 +58,87 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
                 throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
             }
 
-            // If the transactionId has been specified, it means this is an operation in several stage (INIT -> PENDING -> XXX), so we don't need to create the row,
-            // but we do need to set the transactionModelDao in the context so enteringState logic can take place.
-            if (paymentStateContext.getTransactionId() == null) {
-                daoHelper.createNewPaymentTransaction();
-            } else {
+            //
+            // Extract existing transaction matching the transactionId if specified (for e.g notifyPendingTransactionOfStateChanged), or based on transactionExternalKey
+            //
+            final List<PaymentTransactionModelDao> existingPaymentTransactions;
+            if (paymentStateContext.getTransactionId() != null) {
                 final PaymentTransactionModelDao transactionModelDao = daoHelper.getPaymentDao().getPaymentTransaction(paymentStateContext.getTransactionId(), paymentStateContext.getInternalCallContext());
-                paymentStateContext.setPaymentTransactionModelDao(transactionModelDao);
+                existingPaymentTransactions = ImmutableList.of(transactionModelDao);
+            } else if (paymentStateContext.getPaymentTransactionExternalKey() != null) {
+                existingPaymentTransactions = daoHelper.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), paymentStateContext.getInternalCallContext());
+            } else {
+                existingPaymentTransactions = ImmutableList.of();
+            }
+
+            // Validate some constraints on the unicity of that paymentTransactionExternalKey
+            validateUniqueTransactionExternalKey(existingPaymentTransactions);
+
+            // Handle UNKNOWN cases, where we skip the whole state machine and let the getPayment (through Janitor) logic refresh the state.
+            final PaymentTransactionModelDao unknownPaymentTransaction = getUnknownPaymentTransaction(existingPaymentTransactions);
+            if (unknownPaymentTransaction != null) {
+                // Reset the attemptId on the existing paymentTransaction row since it is not accurate
+                unknownPaymentTransaction.setAttemptId(paymentStateContext.getAttemptId());
+                // Set the current paymentTransaction in the context (needed for the state machine logic)
+                paymentStateContext.setPaymentTransactionModelDao(unknownPaymentTransaction);
+                // Set special flag to bypass the state machine altogether (plugin will not be called, state will not be updated, no event will be sent unless state is fixed)
+                paymentStateContext.setSkipOperationForUnknownTransaction(true);
+                return;
+            }
+
+            // Handle PENDING cases, where we want to re-use the same transaction
+            final PaymentTransactionModelDao pendingPaymentTransaction = getPendingPaymentTransaction(existingPaymentTransactions);
+            if (pendingPaymentTransaction != null) {
+                // Set the current paymentTransaction in the context (needed for the state machine logic)
+                paymentStateContext.setPaymentTransactionModelDao(pendingPaymentTransaction);
+                return;
             }
+
+            // At this point we are left with PAYMENT_FAILURE, PLUGIN_FAILURE or nothing, and we validated the uniquess of the paymentTransactionExternalKey so we will create a new row
+            daoHelper.createNewPaymentTransaction();
+
         } catch (PaymentApiException e) {
             throw new OperationException(e);
         }
     }
+
+    protected PaymentTransactionModelDao getUnknownPaymentTransaction(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+        return Iterables.tryFind(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                return input.getTransactionStatus() == TransactionStatus.UNKNOWN;
+            }
+        }).orNull();
+    }
+
+    protected PaymentTransactionModelDao getPendingPaymentTransaction(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+        return Iterables.tryFind(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                return input.getTransactionStatus() == TransactionStatus.PENDING;
+            }
+        }).orNull();
+    }
+
+    protected void validateUniqueTransactionExternalKey(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+        // If no key specified, system will allocate a unique one later, there is nothing to check
+        if (paymentStateContext.getPaymentTransactionExternalKey() == null) {
+            return;
+        }
+
+        if (Iterables.any(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                // An existing transaction in a SUCCESS state
+                return input.getTransactionStatus() == TransactionStatus.SUCCESS ||
+                       // Or, an existing transaction for a different payment (to do really well, we should also check on paymentExternalKey which is not available here)
+                       (paymentStateContext.getPaymentId() != null && input.getPaymentId().compareTo(paymentStateContext.getPaymentId()) != 0) ||
+                       // Or, an existing transaction for a different account.
+                       (!input.getAccountRecordId().equals(paymentStateContext.getInternalCallContext().getAccountRecordId()));
+
+            }
+        })) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
+        }
+    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
index 6781ecd..6d57033 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/payments/PaymentOperation.java
@@ -66,6 +66,11 @@ public abstract class PaymentOperation extends OperationCallbackBase<PaymentTran
 
     @Override
     public OperationResult doOperationCallback() throws OperationException {
+
+        if (paymentStateContext.isSkipOperationForUnknownTransaction()) {
+            return OperationResult.SUCCESS;
+        }
+
         try {
             this.plugin = daoHelper.getPaymentProviderPlugin();
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
index 034fbc4..d9c097c 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
@@ -50,6 +50,7 @@ public class PaymentStateContext {
     protected String paymentTransactionExternalKey;
     protected Currency currency;
     protected Iterable<PluginProperty> properties;
+    protected boolean skipOperationForUnknownTransaction;
 
     // Can be updated later via paymentTransactionModelDao (e.g. for auth or purchase)
     protected final UUID paymentId;
@@ -94,6 +95,7 @@ public class PaymentStateContext {
         this.internalCallContext = internalCallContext;
         this.callContext = callContext;
         this.onLeavingStateExistingTransactions = ImmutableList.of();
+        this.skipOperationForUnknownTransaction = false;
     }
 
     public boolean isApiPayment() {
@@ -199,4 +201,12 @@ public class PaymentStateContext {
     public CallContext getCallContext() {
         return callContext;
     }
+
+    public boolean isSkipOperationForUnknownTransaction() {
+        return skipOperationForUnknownTransaction;
+    }
+
+    public void setSkipOperationForUnknownTransaction(final boolean skipOperationForUnknownTransaction) {
+        this.skipOperationForUnknownTransaction = skipOperationForUnknownTransaction;
+    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
index dfea014..d6a12c4 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
@@ -57,7 +57,7 @@ public class PaymentStateMachineHelper {
     private static final String VOID_SUCCESS = "VOID_SUCCESS";
     private static final String CHARGEBACK_SUCCESS = "CHARGEBACK_SUCCESS";
 
-    private static final String AUTHORIZE_PENDING = "AUTHORIZE_PENDING";
+    private static final String AUTHORIZE_PENDING = "AUTH_PENDING";
 
     private static final String AUTHORIZE_FAILED = "AUTH_FAILED";
     private static final String CAPTURE_FAILED = "CAPTURE_FAILED";
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
index 5878bd9..1f4a1ee 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
@@ -112,7 +112,7 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner 
         try {
             final OperationCallback callback = createOperationCallback(transactionType, paymentStateContext);
             final LeavingStateCallback leavingStateCallback = new DefaultControlInitiated(this, paymentStateContext, paymentDao, paymentControlStateMachineHelper.getInitialState(), paymentControlStateMachineHelper.getRetriedState(), transactionType);
-            final EnteringStateCallback enteringStateCallback = new DefaultControlCompleted(this, paymentStateContext, retryServiceScheduler);
+            final EnteringStateCallback enteringStateCallback = new DefaultControlCompleted(this, paymentStateContext, paymentControlStateMachineHelper.getRetriedState(), retryServiceScheduler);
 
             state.runOperation(paymentControlStateMachineHelper.getOperation(), callback, enteringStateCallback, leavingStateCallback);
         } catch (final MissingEntryException e) {
@@ -134,7 +134,7 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner 
         try {
             final OperationCallback callback = new CompletionControlOperation(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentControlPluginRegistry);
             final LeavingStateCallback leavingStateCallback = new NoopControlInitiated();
-            final EnteringStateCallback enteringStateCallback = new DefaultControlCompleted(this, paymentStateContext, retryServiceScheduler);
+            final EnteringStateCallback enteringStateCallback = new DefaultControlCompleted(this, paymentStateContext, paymentControlStateMachineHelper.getRetriedState(), retryServiceScheduler);
 
             paymentControlStateMachineHelper.getInitialState().runOperation(paymentControlStateMachineHelper.getOperation(), callback, enteringStateCallback, leavingStateCallback);
         } catch (final MissingEntryException e) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index e2877fc..63314b7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -348,10 +348,10 @@ public final class InvoicePaymentControlPluginApi implements PaymentRoutingPlugi
             case PAYMENT_FAILURE:
                 return getNextRetryDateForPaymentFailure(purchasedTransactions);
 
-            case UNKNOWN:
             case PLUGIN_FAILURE:
                 return getNextRetryDateForPluginFailure(purchasedTransactions);
 
+            case UNKNOWN:
             default:
                 return null;
         }
@@ -379,8 +379,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentRoutingPlugi
     private DateTime getNextRetryDateForPluginFailure(final List<PaymentTransactionModelDao> purchasedTransactions) {
 
         DateTime result = null;
-        // Not clear whether or not we should really include TransactionStatus.UNKNOWN
-        final int attemptsInState = getNumberAttemptsInState(purchasedTransactions, TransactionStatus.PLUGIN_FAILURE, TransactionStatus.UNKNOWN);
+        final int attemptsInState = getNumberAttemptsInState(purchasedTransactions, TransactionStatus.PLUGIN_FAILURE);
         final int retryAttempt = (attemptsInState - 1) >= 0 ? (attemptsInState - 1) : 0;
 
         if (retryAttempt < paymentConfig.getPluginFailureRetryMaxAttempts()) {
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index 88d421d..83ffc0d 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -524,7 +524,6 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
 
         final Payment initialPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(),
                                                                       UUID.randomUUID().toString(), UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
-
         try {
             paymentApi.createCapture(account, UUID.randomUUID(), requestedAmount, account.getCurrency(), UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
             Assert.fail("Expected capture to fail...");
@@ -579,6 +578,62 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         }
     }
 
+    @Test(groups = "slow")
+    public void testApiRetryWithUnknownPaymentTransaction() throws Exception {
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment badPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(),
+                                                                  paymentExternalKey, paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+
+        final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.AUTHORIZE).toString();
+        paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), badPayment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+                                                           badPayment.getTransactions().get(0).getId(), TransactionStatus.UNKNOWN, requestedAmount, account.getCurrency(),
+                                                           "eroor 64", "bad something happened", internalCallContext);
+
+        final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(),
+                                                               paymentExternalKey, paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+
+        Assert.assertEquals(payment.getId(), badPayment.getId());
+        Assert.assertEquals(payment.getExternalKey(), paymentExternalKey);
+        Assert.assertEquals(payment.getExternalKey(), paymentExternalKey);
+
+        Assert.assertEquals(payment.getTransactions().size(), 1);
+        Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+        Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+    }
+
+    // Example of a 3D secure payment for instance
+    @Test(groups = "slow")
+    public void testApiWithPendingPaymentTransaction() throws Exception {
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+
+        final String paymentExternalKey = UUID.randomUUID().toString();
+        final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+        final Payment pendingPayment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), null, requestedAmount, account.getCurrency(),
+                                                                      paymentExternalKey, paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+
+        final String paymentStateName = paymentSMHelper.getPendingStateForTransaction(TransactionType.AUTHORIZE).toString();
+        paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), pendingPayment.getId(), TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+                                                           pendingPayment.getTransactions().get(0).getId(), TransactionStatus.PENDING, requestedAmount, account.getCurrency(),
+                                                           null, null, internalCallContext);
+
+        final Payment payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), pendingPayment.getId(), requestedAmount, account.getCurrency(),
+                                                               paymentExternalKey, paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
+
+        Assert.assertEquals(payment.getId(), pendingPayment.getId());
+        Assert.assertEquals(payment.getExternalKey(), paymentExternalKey);
+        Assert.assertEquals(payment.getExternalKey(), paymentExternalKey);
+
+        Assert.assertEquals(payment.getTransactions().size(), 1);
+        Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+        Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+    }
+
     private List<PluginProperty> createPropertiesForInvoice(final Invoice invoice) {
         final List<PluginProperty> result = new ArrayList<PluginProperty>();
         result.add(new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_INVOICE_ID, invoice.getId().toString(), false));
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
index 0097eaf..36800f1 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPaymentLeavingStateCallback.java
@@ -18,12 +18,15 @@
 package org.killbill.billing.payment.core.sm;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.killbill.automaton.OperationException;
 import org.killbill.automaton.State;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
 import org.killbill.billing.payment.api.PaymentApiException;
@@ -45,6 +48,7 @@ public class TestPaymentLeavingStateCallback extends PaymentTestSuiteWithEmbedde
 
     private PaymentStateContext paymentStateContext;
     private PaymentLeavingStateTestCallback callback;
+    private Account account;
 
     @Test(groups = "slow")
     public void testLeaveStateForNewPayment() throws Exception {
@@ -70,8 +74,11 @@ public class TestPaymentLeavingStateCallback extends PaymentTestSuiteWithEmbedde
         setUp(paymentId);
 
         // Verify the payment has only one transaction
-        Assert.assertEquals(paymentDao.getTransactionsForPayment(paymentId, internalCallContext).size(), 1);
+        final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
+        Assert.assertEquals(transactions.size(), 1);
 
+        // Reset paymentExternalKey to something else
+        paymentStateContext.setPaymentTransactionExternalKey(UUID.randomUUID().toString());
         callback.leavingState(state);
 
         // Verify a new transaction was created
@@ -81,6 +88,61 @@ public class TestPaymentLeavingStateCallback extends PaymentTestSuiteWithEmbedde
         Assert.assertEquals(paymentDao.getTransactionsForPayment(paymentId, internalCallContext).size(), 2);
     }
 
+    @Test(groups = "slow", expectedExceptions = OperationException.class)
+    public void testLeaveStateForConflictingPaymentTransactionExternalKey() throws Exception {
+        final UUID paymentId = UUID.randomUUID();
+        setUp(paymentId);
+
+        // Verify the payment has only one transaction
+        final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
+        Assert.assertEquals(transactions.size(), 1);
+
+        final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.CAPTURE).toString();
+        paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), paymentId, TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+                                                           transactions.get(0).getId(), TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.BRL,
+                                                           "foo", "bar", internalCallContext);
+
+        // Will validate the validateUniqueTransactionExternalKey logic for when we reuse the same payment transactionExternalKey
+        callback.leavingState(state);
+
+    }
+
+    @Test(groups = "slow", expectedExceptions = OperationException.class)
+    public void testLeaveStateForConflictingPaymentTransactionExternalKeyAcrossAccounts() throws Exception {
+        final UUID paymentId = UUID.randomUUID();
+        setUp(paymentId);
+
+        // Verify the payment has only one transaction
+        final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(paymentId, internalCallContext);
+        Assert.assertEquals(transactions.size(), 1);
+
+        final String paymentStateName = paymentSMHelper.getErroredStateForTransaction(TransactionType.CAPTURE).toString();
+        paymentDao.updatePaymentAndTransactionOnCompletion(account.getId(), paymentId, TransactionType.AUTHORIZE, paymentStateName, paymentStateName,
+                                                           transactions.get(0).getId(), TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.BRL,
+                                                           "foo", "bar", internalCallContext);
+
+        final InternalCallContext internalCallContextForOtherAccount = new InternalCallContext(paymentStateContext.getInternalCallContext(), 123L);
+
+        paymentStateContext = new PaymentStateContext(true,
+                                                      paymentId,
+                                                      null,
+                                                      null,
+                                                      paymentStateContext.getPaymentExternalKey(),
+                                                      paymentStateContext.getPaymentTransactionExternalKey(),
+                                                      paymentStateContext.getTransactionType(),
+                                                      paymentStateContext.getAccount(),
+                                                      paymentStateContext.getPaymentMethodId(),
+                                                      paymentStateContext.getAmount(),
+                                                      paymentStateContext.getCurrency(),
+                                                      paymentStateContext.shouldLockAccountAndDispatch,
+                                                      paymentStateContext.overridePluginOperationResult,
+                                                      paymentStateContext.getProperties(),
+                                                      internalCallContextForOtherAccount,
+                                                      callContext);
+
+        callback.leavingState(state);
+    }
+
     private void verifyPaymentTransaction() {
         Assert.assertNotNull(paymentStateContext.getPaymentTransactionModelDao().getPaymentId());
         Assert.assertEquals(paymentStateContext.getPaymentTransactionModelDao().getTransactionExternalKey(), paymentStateContext.getPaymentTransactionExternalKey());
@@ -94,11 +156,12 @@ public class TestPaymentLeavingStateCallback extends PaymentTestSuiteWithEmbedde
     }
 
     private void setUp(@Nullable final UUID paymentId) throws Exception {
-        final Account account = Mockito.mock(Account.class);
+        account = Mockito.mock(Account.class);
         Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
         paymentStateContext = new PaymentStateContext(true,
                                                       paymentId,
-                                                      null, null,
+                                                      null,
+                                                      null,
                                                       UUID.randomUUID().toString(),
                                                       UUID.randomUUID().toString(),
                                                       TransactionType.CAPTURE,
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
index c761832..7109927 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -83,10 +83,12 @@ public class MockPaymentDao implements PaymentDao {
     @Override
     public PaymentAttemptModelDao insertPaymentAttemptWithProperties(final PaymentAttemptModelDao attempt, final InternalCallContext context) {
         attempt.setTenantRecordId(context.getTenantRecordId());
+        attempt.setAccountRecordId(context.getAccountRecordId());
 
         synchronized (this) {
             attempts.put(attempt.getId(), attempt);
             mockNonEntityDao.addTenantRecordIdMapping(attempt.getId(), context);
+            mockNonEntityDao.addAccountRecordIdMapping(attempt.getId(), context);
             return attempt;
         }
     }
@@ -177,15 +179,20 @@ public class MockPaymentDao implements PaymentDao {
 
     @Override
     public PaymentModelDao insertPaymentWithFirstTransaction(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
+
         payment.setTenantRecordId(context.getTenantRecordId());
         paymentTransaction.setTenantRecordId(context.getTenantRecordId());
+        payment.setAccountRecordId(context.getAccountRecordId());
+        paymentTransaction.setAccountRecordId(context.getAccountRecordId());
 
         synchronized (this) {
             payments.put(payment.getId(), payment);
             mockNonEntityDao.addTenantRecordIdMapping(payment.getId(), context);
+            mockNonEntityDao.addAccountRecordIdMapping((payment.getId()), context);
 
             transactions.put(paymentTransaction.getId(), paymentTransaction);
             mockNonEntityDao.addTenantRecordIdMapping(paymentTransaction.getId(), context);
+            mockNonEntityDao.addAccountRecordIdMapping((paymentTransaction.getId()), context);
         }
         return payment;
     }
@@ -193,10 +200,12 @@ public class MockPaymentDao implements PaymentDao {
     @Override
     public PaymentTransactionModelDao updatePaymentWithNewTransaction(final UUID paymentId, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
         paymentTransaction.setTenantRecordId(context.getTenantRecordId());
+        paymentTransaction.setAccountRecordId(context.getAccountRecordId());
 
         synchronized (this) {
             transactions.put(paymentTransaction.getId(), paymentTransaction);
             mockNonEntityDao.addTenantRecordIdMapping(paymentId, context);
+            mockNonEntityDao.addAccountRecordIdMapping((paymentTransaction.getId()), context);
         }
         return paymentTransaction;
     }
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java b/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java
index 4cf6eee..2aaa167 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java
@@ -137,6 +137,7 @@ public class TestPaymentHelper {
             Mockito.when(accountInternalApi.getAccountById(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(account);
             Mockito.when(accountInternalApi.getAccountByKey(Mockito.anyString(), Mockito.<InternalTenantContext>any())).thenReturn(account);
             mockNonEntityDao.addTenantRecordIdMapping(account.getId(), internalCallContext);
+            mockNonEntityDao.addAccountRecordIdMapping(account.getId(), internalCallContext);
         } else {
             account = accountApi.createAccount(accountData, context);
         }
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
index 8b5fcf9..2c46cfc 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
@@ -88,19 +88,10 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
         return payment;
     }
 
-    @Test(groups = "fast")
-    public void testFailedPluginWithOneSuccessfulRetry() throws Exception {
-        testSchedulesRetryInternal(1, true, FailureType.PLUGIN_EXCEPTION);
-    }
-
-    @Test(groups = "fast")
-    public void testFailedPluginWithLastRetrySuccess() throws Exception {
-        testSchedulesRetryInternal(paymentConfig.getPluginFailureRetryMaxAttempts(), true, FailureType.PLUGIN_EXCEPTION);
-    }
-
+    // PLUGIN_EXCEPTION will lead to UNKNOWN row that will not be retried by the plugin
     @Test(groups = "fast")
     public void testAbortedPlugin() throws Exception {
-        testSchedulesRetryInternal(paymentConfig.getPluginFailureRetryMaxAttempts(), false, FailureType.PLUGIN_EXCEPTION);
+        testSchedulesRetryInternal(0, false, FailureType.PLUGIN_EXCEPTION);
     }
 
     @Test(groups = "fast")
@@ -165,6 +156,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
                 setPaymentFailure(failureType);
             }
 
+
             moveClockForFailureType(failureType, curFailure);
             final int curFailureCondition = curFailure;
 
@@ -235,7 +227,7 @@ public class TestRetryService extends PaymentTestSuiteNoDB {
         if (failureType == FailureType.PAYMENT_FAILURE) {
             return paymentConfig.getPaymentFailureRetryDays().size();
         } else {
-            return paymentConfig.getPluginFailureRetryMaxAttempts();
+            return 0;
         }
     }
 

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index b217bcf..7d8a298 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
         <version>0.28-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
-    <version>0.15.1-SNAPSHOT</version>
+    <version>0.15.2-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index 2dc774c..a3889a2 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index eb72d48..ad52eb1 100644
--- a/profiles/killpay/pom.xml
+++ b/profiles/killpay/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killpay</artifactId>

profiles/pom.xml 2(+1 -1)

diff --git a/profiles/pom.xml b/profiles/pom.xml
index 7d26dc8..65b88bb 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles</artifactId>
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 8d2682a..ddcae7a 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-subscription</artifactId>

tenant/pom.xml 2(+1 -1)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index e874891..f4bb849 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-tenant</artifactId>

usage/pom.xml 2(+1 -1)

diff --git a/usage/pom.xml b/usage/pom.xml
index b8aa03b..7c1229d 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-usage</artifactId>

util/pom.xml 2(+1 -1)

diff --git a/util/pom.xml b/util/pom.xml
index fb16656..ddcb261 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.15.1-SNAPSHOT</version>
+        <version>0.15.2-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>
diff --git a/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java b/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
index 896240a..d7bfc00 100644
--- a/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
+++ b/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
@@ -34,11 +34,16 @@ import org.killbill.billing.util.dao.TableName;
 public class MockNonEntityDao implements NonEntityDao {
 
     private final Map<UUID, Long> tenantRecordIdMappings = new HashMap<UUID, Long>();
+    private final Map<UUID, Long> accountRecordIdMappings = new HashMap<UUID, Long>();
 
     public void addTenantRecordIdMapping(final UUID objectId, final InternalTenantContext context) {
         tenantRecordIdMappings.put(objectId, context.getTenantRecordId());
     }
 
+    public void addAccountRecordIdMapping(final UUID objectId, final InternalTenantContext context) {
+        accountRecordIdMappings.put(objectId, context.getAccountRecordId());
+    }
+
     @Override
     public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
         return null;
@@ -46,7 +51,7 @@ public class MockNonEntityDao implements NonEntityDao {
 
     @Override
     public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
-        return null;
+        return accountRecordIdMappings.get(objectId);
     }
 
     @Override