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 ce07214..6719b7c 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
@@ -464,6 +464,16 @@ public class PaymentProcessor extends ProcessorBase {
throw new PaymentApiException(ErrorCode.PAYMENT_DIFFERENT_ACCOUNT_ID, paymentStateContext.getPaymentId());
}
+ // Note: the list needs to be modifiable for invokeJanitor
+ final Collection<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = new LinkedList<PaymentTransactionModelDao>(daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext()));
+ // Always invoke the Janitor first to get the latest state. The state machine will then
+ // prevent disallowed transitions in case the state couldn't be fixed (or if it's already in a final state).
+ if (runJanitor) {
+ final PaymentPluginApi plugin = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), internalCallContext);
+ final List<PaymentTransactionInfoPlugin> pluginTransactions = getPaymentTransactionInfoPlugins(plugin, paymentModelDao, properties, callContext);
+ paymentModelDao = invokeJanitor(paymentModelDao, paymentTransactionsForCurrentPayment, pluginTransactions, internalCallContext);
+ }
+
if (paymentStateContext.getPaymentTransactionExternalKey() != null) {
final List<PaymentTransactionModelDao> allPaymentTransactionsForKey = daoHelper.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), internalCallContext);
runSanityOnTransactionExternalKey(allPaymentTransactionsForKey, paymentStateContext, internalCallContext);
@@ -471,18 +481,9 @@ public class PaymentProcessor extends ProcessorBase {
if (paymentStateContext.getTransactionId() != null || paymentStateContext.getPaymentTransactionExternalKey() != null) {
// If a transaction id or key is passed, we are maybe completing an existing transaction (unless a new key was provided)
- final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext());
PaymentTransactionModelDao transactionToComplete = findTransactionToCompleteAndRunSanityChecks(paymentModelDao, paymentTransactionsForCurrentPayment, paymentStateContext, internalCallContext);
if (transactionToComplete != null) {
- // For completion calls, always invoke the Janitor first to get the latest state. The state machine will then
- // prevent disallowed transitions in case the state couldn't be fixed (or if it's already in a final state).
- if (runJanitor) {
- final PaymentPluginApi plugin = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), internalCallContext);
- final List<PaymentTransactionInfoPlugin> pluginTransactions = getPaymentTransactionInfoPlugins(plugin, paymentModelDao, properties, callContext);
- paymentModelDao = invokeJanitor(paymentModelDao, paymentTransactionsForCurrentPayment, pluginTransactions, internalCallContext);
- }
-
final UUID transactionToCompleteId = transactionToComplete.getId();
transactionToComplete = Iterables.<PaymentTransactionModelDao>find(paymentTransactionsForCurrentPayment,
new Predicate<PaymentTransactionModelDao>() {
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 92d8c17..af49a16 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
@@ -1489,7 +1489,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
final Payment completedPayment = createPayment(TransactionType.AUTHORIZE, initialPayment.getId(), initialPayment.getExternalKey(), transactionExternalKey, authAmount, PaymentPluginStatus.PROCESSED);
Assert.fail();
} catch (final PaymentApiException e) {
- Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
}
}
@@ -1507,7 +1507,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
final Payment completedPayment = createPayment(TransactionType.AUTHORIZE, initialPayment.getId(), initialPayment.getExternalKey(), transactionExternalKey, authAmount, PaymentPluginStatus.PROCESSED);
Assert.fail();
} catch (final PaymentApiException e) {
- Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
}
}
@@ -1715,14 +1715,15 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
final String paymentTransactionExternalKey = UUID.randomUUID().toString();
- final Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
try {
// Attempt to complete the payment
- createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
Assert.fail();
} catch (final PaymentApiException e) {
Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_OPERATION.getCode());
@@ -1730,6 +1731,161 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
+ public void testDoubleCaptureOnASuccessfulCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+ // 1st capture with success
+ Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ // 2nd capture request with identical transaction external key
+ try {
+ // Attempt to complete the payment
+ pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2); //should be 2
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ }
+
+ @Test(groups = "slow")
+ public void testDoubleCaptureOnAPotentiallySuccessfulCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+ // 1st capture with UNDEFINED
+ Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ // but actually successful
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(pendingPayment.getId(), pendingPayment.getTransactions().get(1).getId(), PaymentPluginStatus.PROCESSED);
+
+ // 2nd capture request with identical transaction external key
+ try {
+ createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ final Payment refreshedPayment = paymentApi.getPayment(pendingPayment.getId(), true, false, ImmutableList.<PluginProperty>of(), callContext);
+ Assert.assertEquals(refreshedPayment.getTransactions().size(), 2);
+ Assert.assertEquals(refreshedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(refreshedPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCaptureWithAPotentiallySuccessfulCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+ // 1st capture with UNDEFINED
+ Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ // but actually successful
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(pendingPayment.getId(), pendingPayment.getTransactions().get(1).getId(), PaymentPluginStatus.PROCESSED);
+
+ final String anotherPaymentTransactionExternalKey = UUID.randomUUID().toString();
+ // 2nd capture request with a different transaction external key
+ pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, anotherPaymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 3);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ }
+
+ @Test(groups = "slow")
+ public void testDoubleCaptureOnAPotentiallyFailedCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+ // 1st capture with UNDEFINED
+ Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ // but actually failed
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(pendingPayment.getId(), pendingPayment.getTransactions().get(1).getId(), PaymentPluginStatus.ERROR);
+
+ // 2nd capture request with identical transaction external key
+ pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 3);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(pendingPayment.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ }
+
+ @Test(groups = "slow")
+ public void testCaptureWithAPotentiallyFailedCapture() throws Exception {
+ final String paymentExternalKey = UUID.randomUUID().toString();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ final Payment authorization = createPayment(TransactionType.AUTHORIZE, null, paymentExternalKey, UUID.randomUUID().toString(), requestedAmount, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization);
+ Assert.assertEquals(authorization.getTransactions().size(), 1);
+ Assert.assertEquals(authorization.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String paymentTransactionExternalKey = UUID.randomUUID().toString();
+
+ // 1st capture with UNDEFINED
+ Payment pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, paymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.UNDEFINED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 2);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
+
+ // but actually failed
+ mockPaymentProviderPlugin.overridePaymentPluginStatus(pendingPayment.getId(), pendingPayment.getTransactions().get(1).getId(), PaymentPluginStatus.ERROR);
+
+ final String anotherPaymentTransactionExternalKey = UUID.randomUUID().toString();
+ // 2nd capture request with different transaction external key
+ pendingPayment = createPayment(TransactionType.CAPTURE, authorization.getId(), paymentExternalKey, anotherPaymentTransactionExternalKey, requestedAmount, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(pendingPayment.getTransactions().size(), 3);
+ Assert.assertEquals(pendingPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(pendingPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ Assert.assertEquals(pendingPayment.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ }
+
+ @Test(groups = "slow")
public void testCreatePurchaseWithTimeout() throws Exception {
final BigDecimal requestedAmount = BigDecimal.TEN;
final String paymentExternalKey = "ohhhh";