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/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);