killbill-memoizeit

payment: Reopen #383 and change behavior to not allow to have

4/14/2016 9:49:48 PM

Details

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 9fec37d..831b349 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
@@ -18,6 +18,9 @@
 package org.killbill.billing.payment.core.sm.payments;
 
 import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
 
 import org.killbill.automaton.OperationException;
 import org.killbill.automaton.State;
@@ -25,6 +28,7 @@ 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.api.TransactionType;
 import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
 import org.killbill.billing.payment.core.sm.PaymentStateContext;
 import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
@@ -58,29 +62,38 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
                 throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, paymentStateContext.getAccount().getId());
             }
 
+            // If we were given a paymentId (or existing paymentExternalId -> effectivePaymentId) we first fetch existing transactions (required for sanity and handling PENDING transactions)
+            final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = paymentStateContext.getPaymentId() != null ?
+                                                                                          daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()) :
+                                                                                          ImmutableList.<PaymentTransactionModelDao>of();
+
             //
             // 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());
-                existingPaymentTransactions = ImmutableList.of(transactionModelDao);
-            } else if (paymentStateContext.getPaymentTransactionExternalKey() != null) {
-                existingPaymentTransactions = daoHelper.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), paymentStateContext.getInternalCallContext());
-            } else {
-                existingPaymentTransactions = ImmutableList.of();
-            }
+            final Iterable<PaymentTransactionModelDao> existingPaymentTransactionsForTransactionIdOrKey = filterExistingPaymentTransactionsForTransactionIdOrKey(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionId(), paymentStateContext.getPaymentTransactionExternalKey());
 
             // Validate the payment transactions belong to the right payment
-            validatePaymentIdAndTransactionType(existingPaymentTransactions);
+            validatePaymentIdAndTransactionType(existingPaymentTransactionsForTransactionIdOrKey);
 
             // Validate some constraints on the unicity of that paymentTransactionExternalKey
-            validateUniqueTransactionExternalKey(existingPaymentTransactions);
+            validateUniqueTransactionExternalKey(existingPaymentTransactionsForTransactionIdOrKey);
+
+            //
+            // Handle PENDING case:
+            // a) If we have a PENDING transaction for the same (payment transaction) key, this is a completion and we want to re-use the same transaction
+            // b) If we have a PENDING transaction for a different (payment transaction) key, and for an initial request (AUTH, PURCHASE, CREDIT), we FAIL the request
+            //   (unfortunately this cannot be caught by the state machine because the transition XXX_PENDING -> _SUCCESS needs to be allowed and this is irrespective of the keys)
+            // c) If we have a PENDING transaction for a different (payment transaction) key, and for other follow-up request  (CAPTURE, REFUND, ..), we ignore it and create a new transaction
+            //
+            final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType = filterPendingTransactionsForPaymentAndTransactionType(paymentTransactionsForCurrentPayment, paymentStateContext.getTransactionType());
 
-            // Handle PENDING cases, where we want to re-use the same transaction
-            final PaymentTransactionModelDao pendingPaymentTransaction = getPendingPaymentTransaction(existingPaymentTransactions);
+            // Case b)
+            validateUniqueInitialPendingTransaction(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getTransactionType(), paymentStateContext.getPaymentTransactionExternalKey());
+
+
+            final PaymentTransactionModelDao pendingPaymentTransaction = filterPendingTransactionsForTransactionKey(pendingTransactionsForPaymentAndTransactionType, paymentStateContext.getPaymentTransactionExternalKey());
             if (pendingPaymentTransaction != null) {
-                // Set the current paymentTransaction in the context (needed for the state machine logic)
+                // Case a) Set the current paymentTransaction in the context (needed for the state machine logic)
                 paymentStateContext.setPaymentTransactionModelDao(pendingPaymentTransaction);
                 return;
             }
@@ -93,25 +106,61 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
         }
     }
 
-    protected PaymentTransactionModelDao getUnknownPaymentTransaction(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
-        return Iterables.tryFind(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+    private void validateUniqueInitialPendingTransaction(final Iterable<PaymentTransactionModelDao> pendingTransactionsForPaymentAndTransactionType, final TransactionType transactionType, final String paymentTransactionExternalKey) {
+        if (transactionType != TransactionType.AUTHORIZE &&
+            transactionType != TransactionType.PURCHASE &&
+            transactionType != TransactionType.CREDIT) {
+            return;
+        }
+
+        final PaymentTransactionModelDao existingPendingTransactionForDifferentKey = Iterables.tryFind(pendingTransactionsForPaymentAndTransactionType, new Predicate<PaymentTransactionModelDao>() {
             @Override
             public boolean apply(final PaymentTransactionModelDao input) {
-                return input.getTransactionStatus() == TransactionStatus.UNKNOWN;
+                return !input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
             }
         }).orNull();
+        if (existingPendingTransactionForDifferentKey !=  null) {
+            // We are missing ErrorCode PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS (should be fixed in 0.17.0. See #525)
+            throw new RuntimeException(String.format("Failed to create another initial transaction for paymentId='%s' : Existing PENDING transactionId='%s'",
+                                                          existingPendingTransactionForDifferentKey.getPaymentId(), existingPendingTransactionForDifferentKey.getId()));
+        }
+    }
+
+    protected Iterable<PaymentTransactionModelDao> filterExistingPaymentTransactionsForTransactionIdOrKey(final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, @Nullable final UUID paymentTransactionId, @Nullable final String paymentTransactionExternalKey) throws PaymentApiException {
+        return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                if (paymentTransactionId != null && input.getId().equals(paymentTransactionId)) {
+                    return true;
+                }
+                if (paymentTransactionExternalKey != null && input.getTransactionExternalKey().equals(paymentTransactionExternalKey)) {
+                    return true;
+                }
+                return false;
+            }
+        });
+    }
+
+    protected Iterable<PaymentTransactionModelDao> filterPendingTransactionsForPaymentAndTransactionType(final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, final TransactionType transactionType) throws PaymentApiException {
+        return Iterables.filter(paymentTransactionsForCurrentPayment, new Predicate<PaymentTransactionModelDao>() {
+            @Override
+            public boolean apply(final PaymentTransactionModelDao input) {
+                return input.getTransactionStatus() == TransactionStatus.PENDING &&
+                       input.getTransactionType() == transactionType;
+            }
+        });
     }
 
-    protected PaymentTransactionModelDao getPendingPaymentTransaction(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
-        return Iterables.tryFind(existingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
+    protected PaymentTransactionModelDao filterPendingTransactionsForTransactionKey(final Iterable<PaymentTransactionModelDao> existingPendingPaymentTransactions, final String paymentTransactionExternalKey) throws PaymentApiException {
+        return Iterables.tryFind(existingPendingPaymentTransactions, new Predicate<PaymentTransactionModelDao>() {
             @Override
             public boolean apply(final PaymentTransactionModelDao input) {
-                return input.getTransactionStatus() == TransactionStatus.PENDING;
+                return input.getTransactionExternalKey().equals(paymentTransactionExternalKey);
             }
         }).orNull();
     }
 
-    protected void validateUniqueTransactionExternalKey(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+    protected void validateUniqueTransactionExternalKey(final Iterable<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;
@@ -134,7 +183,7 @@ public abstract class PaymentLeavingStateCallback implements LeavingStateCallbac
     }
 
     // At this point, the payment id should have been populated for follow-up transactions (see PaymentAutomationRunner#run)
-    protected void validatePaymentIdAndTransactionType(final List<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
+    protected void validatePaymentIdAndTransactionType(final Iterable<PaymentTransactionModelDao> existingPaymentTransactions) throws PaymentApiException {
         for (final PaymentTransactionModelDao paymentTransactionModelDao : existingPaymentTransactions) {
             if (!paymentTransactionModelDao.getPaymentId().equals(paymentStateContext.getPaymentId())) {
                 throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, paymentTransactionModelDao.getId(), "does not belong to payment " + paymentStateContext.getPaymentId());
diff --git a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
index 14146d6..7453caf 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
+++ b/payment/src/main/resources/org/killbill/billing/payment/PaymentStates.xml
@@ -456,12 +456,6 @@
         <linkStateMachine>
             <initialStateMachine>AUTHORIZE</initialStateMachine>
             <initialState>AUTH_SUCCESS</initialState>
-            <finalStateMachine>AUTHORIZE</finalStateMachine>
-            <finalState>AUTH_INIT</finalState>
-        </linkStateMachine>
-        <linkStateMachine>
-            <initialStateMachine>AUTHORIZE</initialStateMachine>
-            <initialState>AUTH_SUCCESS</initialState>
             <finalStateMachine>CAPTURE</finalStateMachine>
             <finalState>CAPTURE_INIT</finalState>
         </linkStateMachine>
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 42eafdc..7b9e416 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
@@ -1099,74 +1099,6 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         }
     }
 
-    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/371")
-    public void testApiWithDuplicatePendingPaymentTransaction() throws Exception {
-        final BigDecimal requestedAmount = BigDecimal.TEN;
-
-        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
-            final String payment1ExternalKey = UUID.randomUUID().toString();
-            final String payment1TransactionExternalKey = UUID.randomUUID().toString();
-            final String payment2ExternalKey = UUID.randomUUID().toString();
-            final String payment2TransactionExternalKey = UUID.randomUUID().toString();
-            final String payment3TransactionExternalKey = UUID.randomUUID().toString();
-
-            final Payment pendingPayment1 = createPayment(transactionType, null, payment1ExternalKey, payment1TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-            assertNotNull(pendingPayment1);
-            Assert.assertEquals(pendingPayment1.getExternalKey(), payment1ExternalKey);
-            Assert.assertEquals(pendingPayment1.getTransactions().size(), 1);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getExternalKey(), payment1TransactionExternalKey);
-            Assert.assertEquals(pendingPayment1.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-
-            // Attempt to create a second transaction for the same payment, but with a different transaction external key
-            final Payment pendingPayment2 = createPayment(transactionType, null, payment1ExternalKey, payment2TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-            assertNotNull(pendingPayment2);
-            Assert.assertEquals(pendingPayment2.getId(), pendingPayment1.getId());
-            Assert.assertEquals(pendingPayment2.getExternalKey(), payment1ExternalKey);
-            Assert.assertEquals(pendingPayment2.getTransactions().size(), 2);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getExternalKey(), payment1TransactionExternalKey);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getExternalKey(), payment2TransactionExternalKey);
-            Assert.assertEquals(pendingPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
-
-            try {
-                // Verify we cannot use the same transaction external key on a different payment if the payment id isn't specified
-                createPayment(transactionType, null, payment2ExternalKey, payment1TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-                Assert.fail();
-            } catch (final PaymentApiException e) {
-                Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
-            }
-
-            try {
-                // Verify we cannot use the same transaction external key on a different payment if the payment id isn't specified
-                createPayment(transactionType, null, payment2ExternalKey, payment2TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-                Assert.fail();
-            } catch (final PaymentApiException e) {
-                Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
-            }
-
-            // Attempt to create a second transaction for a different payment
-            final Payment pendingPayment3 = createPayment(transactionType, null, payment2ExternalKey, payment3TransactionExternalKey, requestedAmount, PaymentPluginStatus.PENDING);
-            assertNotNull(pendingPayment3);
-            Assert.assertNotEquals(pendingPayment3.getId(), pendingPayment1.getId());
-            Assert.assertEquals(pendingPayment3.getExternalKey(), payment2ExternalKey);
-            Assert.assertEquals(pendingPayment3.getTransactions().size(), 1);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getProcessedAmount().compareTo(requestedAmount), 0);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getCurrency(), account.getCurrency());
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getExternalKey(), payment3TransactionExternalKey);
-            Assert.assertEquals(pendingPayment3.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
-        }
-    }
-
     @Test(groups = "slow")
     public void testApiWithPendingPaymentTransaction() throws Exception {
         for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
@@ -1311,9 +1243,122 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         } catch (final PaymentApiException expected) {
             Assert.assertEquals(expected.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
         }
+    }
+
+    @Test(groups = "slow")
+    public void testSuccessfulInitialTransactionToSameTransaction() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment processedPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(processedPayment);
+            Assert.assertEquals(processedPayment.getTransactions().size(), 1);
+            Assert.assertEquals(processedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with different key => KB state machine should make the request fail as we don't allow
+            // multiple SUCCESS {AUTH, PURCHASE, CREDIT}
+            final String keyB = UUID.randomUUID().toString();
+            try {
+                createPayment(transactionType, processedPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
+                Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same different key should fail");
+            } catch (final PaymentApiException e) {
+            }
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => 3key constraint should make the request fail
+            try {
+                createPayment(transactionType, processedPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+                Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same transaction key should fail");
+            } catch (final PaymentApiException e) {
+            }
+        }
+    }
+
+
+    @Test(groups = "slow")
+    public void testPendingInitialTransactionToSameTransaction() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment pendingPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PENDING);
+            assertNotNull(pendingPayment);
+            Assert.assertEquals(pendingPayment.getTransactions().size(), 1);
+            Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with different key => KB state machine should make the request fail as we don't allow
+            // multiple SUCCESS {AUTH, PURCHASE, CREDIT}
+            final String keyB = UUID.randomUUID().toString();
+            try {
+                createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
+                Assert.fail("Retrying initial successful transaction (AUTHORIZE, PURCHASE, CREDIT) with same different key should fail");
+            } catch (final PaymentApiException e) {
+            }
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => That should work because we are completing the payment
+            final Payment completedPayment = createPayment(transactionType, pendingPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(completedPayment);
+            Assert.assertEquals(completedPayment.getId(), pendingPayment.getId());
+            Assert.assertEquals(completedPayment.getTransactions().size(), 1);
+        }
+    }
+
+
+    @Test(groups = "slow")
+    public void testFailedInitialTransactionToSameTransactionWithSameKey() throws Exception {
 
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment errorPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.ERROR);
+            assertNotNull(errorPayment);
+            Assert.assertEquals(errorPayment.getTransactions().size(), 1);
+            Assert.assertEquals(errorPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with same key => That should work because we are completing the payment
+            final Payment successfulPayment = createPayment(transactionType, errorPayment.getId(), paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(successfulPayment);
+            Assert.assertEquals(successfulPayment.getId(), errorPayment.getId());
+            Assert.assertEquals(successfulPayment.getTransactions().size(), 2);
+        }
     }
 
+
+    @Test(groups = "slow")
+    public void testFailedInitialTransactionToSameTransactionWithDifferentKey() throws Exception {
+
+        final BigDecimal requestedAmount = BigDecimal.TEN;
+        for (final TransactionType transactionType : ImmutableList.<TransactionType>of(TransactionType.AUTHORIZE, TransactionType.PURCHASE, TransactionType.CREDIT)) {
+
+            final String paymentExternalKey = UUID.randomUUID().toString();
+            final String keyA = UUID.randomUUID().toString();
+
+            final Payment errorPayment = createPayment(transactionType, null, paymentExternalKey, keyA, requestedAmount, PaymentPluginStatus.ERROR);
+            assertNotNull(errorPayment);
+            Assert.assertEquals(errorPayment.getTransactions().size(), 1);
+            Assert.assertEquals(errorPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+
+            // Attempt to create another {AUTH, PURCHASE, CREDIT} with different key => KB state machine should make the request fail as we don't allow
+            // multiple SUCCESS {AUTH, PURCHASE, CREDIT}
+            final String keyB = UUID.randomUUID().toString();
+            final Payment successfulPayment = createPayment(transactionType, errorPayment.getId(), paymentExternalKey, keyB, requestedAmount, PaymentPluginStatus.PROCESSED);
+            assertNotNull(successfulPayment);
+            Assert.assertEquals(successfulPayment.getId(), errorPayment.getId());
+            Assert.assertEquals(successfulPayment.getTransactions().size(), 2);
+        }
+    }
+
+
+
     private void verifyRefund(final Payment refund, final String paymentExternalKey, final String paymentTransactionExternalKey, final String refundTransactionExternalKey, final BigDecimal requestedAmount, final BigDecimal refundAmount, final TransactionStatus transactionStatus) {
         Assert.assertEquals(refund.getExternalKey(), paymentExternalKey);
         Assert.assertEquals(refund.getTransactions().size(), 2);