Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
index e27495f..c4b2195 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
@@ -321,7 +321,7 @@ public class TestInvoicePayment extends TestIntegrationBase {
// Trigger chargeback in the original currency
payment1 = createChargeBackAndCheckForCompletion(account, payment1, new BigDecimal("225.44"), Currency.EUR, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
- Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("24.51")), 0);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
Assert.assertEquals(payment1.getTransactions().size(), 2);
Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
Assert.assertEquals(payment1.getTransactions().get(0).getCurrency(), Currency.USD);
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
index 33b8730..7303f80 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
@@ -30,7 +30,10 @@ import org.joda.time.DateTime;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.entity.EntityBase;
+import com.google.common.base.Function;
import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -80,13 +83,77 @@ public class DefaultPayment extends EntityBase implements Payment {
}
}
- final BigDecimal chargebackAmount = getChargebackAmount(transactions);
-
- this.authAmount = getAmountForType(nonVoidedTransactions, TransactionType.AUTHORIZE);
- this.captureAmount = getAmountForType(nonVoidedTransactions, TransactionType.CAPTURE).add(chargebackAmount.negate()).max(BigDecimal.ZERO);
- this.purchasedAmount = getAmountForType(nonVoidedTransactions, TransactionType.PURCHASE).add(chargebackAmount.negate()).max(BigDecimal.ZERO);
- this.creditAmount = getAmountForType(nonVoidedTransactions, TransactionType.CREDIT);
- this.refundAmount = getAmountForType(nonVoidedTransactions, TransactionType.REFUND);
+ final Collection<PaymentTransaction> chargebackTransactions = getChargebackTransactions(transactions);
+ final Currency chargebackProcessedCurrency = getCurrencyForTransactions(chargebackTransactions, true);
+ final BigDecimal chargebackProcessedAmount = chargebackProcessedCurrency == null ? BigDecimal.ZERO : getAmountForTransactions(chargebackTransactions, true);
+ final Currency chargebackCurrency = getCurrencyForTransactions(chargebackTransactions, false);
+ final BigDecimal chargebackAmount = chargebackCurrency == null ? BigDecimal.ZERO : getAmountForTransactions(chargebackTransactions, false);
+
+ PaymentTransaction transactionToUseForCurrency = Iterables.<PaymentTransaction>getFirst(Iterables.<PaymentTransaction>filter(transactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return (transaction.getTransactionType() == TransactionType.AUTHORIZE ||
+ transaction.getTransactionType() == TransactionType.PURCHASE ||
+ transaction.getTransactionType() == TransactionType.CREDIT) &&
+ (TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus()) ||
+ TransactionStatus.PENDING.equals(transaction.getTransactionStatus()));
+ }
+ }), null);
+ if (transactionToUseForCurrency == null) {
+ // No successful one, take the last non-successful one then
+ transactionToUseForCurrency = Iterables.<PaymentTransaction>getLast(Iterables.<PaymentTransaction>filter(transactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getTransactionType() == TransactionType.AUTHORIZE ||
+ transaction.getTransactionType() == TransactionType.PURCHASE ||
+ transaction.getTransactionType() == TransactionType.CREDIT;
+ }
+ }), null);
+ }
+ this.currency = transactionToUseForCurrency == null ? null : transactionToUseForCurrency.getCurrency();
+
+ this.authAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.AUTHORIZE,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.captureAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.CAPTURE,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.purchasedAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.PURCHASE,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.creditAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.CREDIT,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ this.refundAmount = getAmountForTransactions(this.currency,
+ nonVoidedTransactions,
+ TransactionType.REFUND,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
this.isAuthVoided = Iterables.<PaymentTransaction>tryFind(voidedTransactions,
new Predicate<PaymentTransaction>() {
@@ -95,11 +162,9 @@ public class DefaultPayment extends EntityBase implements Payment {
return input.getTransactionType() == TransactionType.AUTHORIZE && TransactionStatus.SUCCESS.equals(input.getTransactionStatus());
}
}).isPresent();
-
- this.currency = !transactions.isEmpty() ? transactions.get(0).getCurrency() : null;
}
- private static BigDecimal getChargebackAmount(final Iterable<PaymentTransaction> transactions) {
+ private static Collection<PaymentTransaction> getChargebackTransactions(final Collection<PaymentTransaction> transactions) {
final Collection<String> successfulChargebackExternalKeys = new HashSet<String>();
for (final PaymentTransaction transaction : transactions) {
@@ -111,37 +176,112 @@ public class DefaultPayment extends EntityBase implements Payment {
}
}
- return getAmountForType(Iterables.<PaymentTransaction>filter(transactions, new Predicate<PaymentTransaction>() {
- @Override
- public boolean apply(final PaymentTransaction input) {
- return successfulChargebackExternalKeys.contains(input.getExternalKey());
- }
- }),
- TransactionType.CHARGEBACK);
+ return Collections2.<PaymentTransaction>filter(transactions, new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction input) {
+ return successfulChargebackExternalKeys.contains(input.getExternalKey());
+ }
+ });
}
- private static BigDecimal getAmountForType(final Iterable<PaymentTransaction> transactions, final TransactionType transactiontype) {
- BigDecimal result = BigDecimal.ZERO;
- BigDecimal processedResult = BigDecimal.ZERO;
- boolean shouldUseProcessedAmount = true;
-
- for (final PaymentTransaction transaction : transactions) {
- if (transaction.getTransactionType() != transactiontype || !TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus())) {
- continue;
+ private static BigDecimal getAmountForTransactions(final Currency paymentCurrency,
+ final Collection<PaymentTransaction> transactions,
+ final TransactionType transactiontype,
+ final Collection<PaymentTransaction> chargebackTransactions,
+ final BigDecimal chargebackProcessedAmount,
+ final Currency chargebackProcessedCurrency,
+ final BigDecimal chargebackAmount,
+ final Currency chargebackCurrency) {
+ final Collection<PaymentTransaction> candidateTransactions = Collections2.<PaymentTransaction>filter(transactions,
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getTransactionType() == transactiontype && TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus());
+ }
+ });
+
+ final boolean takeChargebacksIntoAccount = ImmutableList.<TransactionType>of(TransactionType.CAPTURE, TransactionType.PURCHASE).contains(transactiontype);
+ Currency currencyForTransactions = getCurrencyForTransactions(candidateTransactions, true);
+ if (currencyForTransactions == null || currencyForTransactions != paymentCurrency) {
+ currencyForTransactions = getCurrencyForTransactions(candidateTransactions, false);
+ if (currencyForTransactions == null) {
+ // Multiple currencies - cannot compute the total
+ return BigDecimal.ZERO;
+ } else if (currencyForTransactions != paymentCurrency) {
+ // Different currency than the main payment currency
+ return BigDecimal.ZERO;
+ } else {
+ final BigDecimal amountForTransactions = getAmountForTransactions(candidateTransactions, false);
+ return getAmountForTransactions(amountForTransactions,
+ takeChargebacksIntoAccount,
+ currencyForTransactions,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
}
+ } else {
+ final BigDecimal amountForTransactions = getAmountForTransactions(candidateTransactions, true);
+ return getAmountForTransactions(amountForTransactions,
+ takeChargebacksIntoAccount,
+ currencyForTransactions,
+ chargebackTransactions,
+ chargebackProcessedAmount,
+ chargebackProcessedCurrency,
+ chargebackAmount,
+ chargebackCurrency);
+ }
+ }
- result = result.add(transaction.getAmount());
+ private static BigDecimal getAmountForTransactions(final BigDecimal amountForTransactions,
+ final boolean takeChargebacksIntoAccount,
+ final Currency currencyForTransactions,
+ final Collection<PaymentTransaction> chargebackTransactions,
+ final BigDecimal chargebackProcessedAmount,
+ final Currency chargebackProcessedCurrency,
+ final BigDecimal chargebackAmount,
+ final Currency chargebackCurrency) {
+ if (!takeChargebacksIntoAccount) {
+ return amountForTransactions;
+ }
- shouldUseProcessedAmount = shouldUseProcessedAmount && transaction.getCurrency().equals(transaction.getProcessedCurrency()) && transaction.getProcessedAmount() != null;
- processedResult = shouldUseProcessedAmount ? processedResult.add(transaction.getProcessedAmount()) : BigDecimal.ZERO;
+ final BigDecimal chargebackAmountInCorrectCurrency;
+ if (currencyForTransactions == chargebackProcessedCurrency) {
+ chargebackAmountInCorrectCurrency = chargebackProcessedAmount;
+ } else if (currencyForTransactions == chargebackCurrency) {
+ chargebackAmountInCorrectCurrency = chargebackAmount;
+ } else if (!chargebackTransactions.isEmpty()) {
+ // Payment has chargebacks but in a different currency - zero-out the payment
+ chargebackAmountInCorrectCurrency = amountForTransactions;
+ } else {
+ chargebackAmountInCorrectCurrency = BigDecimal.ZERO;
+ }
+ return amountForTransactions.add(chargebackAmountInCorrectCurrency.negate()).max(BigDecimal.ZERO);
+ }
- // For multi-step AUTH, don't sum the individual transactions
- if (TransactionType.AUTHORIZE.equals(transactiontype)) {
- break;
+ private static BigDecimal getAmountForTransactions(final Iterable<PaymentTransaction> candidateTransactions, final boolean useProcessedValues) {
+ BigDecimal amount = BigDecimal.ZERO;
+ for (final PaymentTransaction transaction : candidateTransactions) {
+ if (useProcessedValues) {
+ amount = amount.add(transaction.getProcessedAmount());
+ } else {
+ amount = amount.add(transaction.getAmount());
}
}
+ return amount;
+ }
+
+ private static Currency getCurrencyForTransactions(final Collection<PaymentTransaction> candidateTransactions, final boolean useProcessedValues) {
+ final Collection<Currency> currencies = new HashSet<Currency>(Collections2.<PaymentTransaction, Currency>transform(candidateTransactions,
+ new Function<PaymentTransaction, Currency>() {
+ @Override
+ public Currency apply(final PaymentTransaction transaction) {
+ return useProcessedValues ? transaction.getProcessedCurrency() : transaction.getCurrency();
+ }
+ }));
- return shouldUseProcessedAmount ? processedResult : result;
+ return currencies.size() > 1 ? null : Iterables.<Currency>getFirst(currencies, null);
}
@Override
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java
index 7cf7830..6bf1743 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultPayment.java
@@ -33,7 +33,6 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testAmountsCaptureVoided() throws Exception {
final UUID paymentId = UUID.randomUUID();
- final String chargebackExternalKey = UUID.randomUUID().toString();
final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.AUTHORIZE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.CAPTURE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.VOID, TransactionStatus.SUCCESS, null));
@@ -47,7 +46,6 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
@Test(groups = "fast")
public void testAmountsCaptureVoidedAuthReversed() throws Exception {
final UUID paymentId = UUID.randomUUID();
- final String chargebackExternalKey = UUID.randomUUID().toString();
final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.AUTHORIZE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.CAPTURE, TransactionStatus.SUCCESS, BigDecimal.TEN),
buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.VOID, TransactionStatus.SUCCESS, null),
@@ -89,6 +87,22 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testAmountsCaptureChargebackReversedMultipleCurrencies() throws Exception {
+ final UUID paymentId = UUID.randomUUID();
+ final String chargebackExternalKey = UUID.randomUUID().toString();
+ final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.AUTHORIZE, TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.EUR),
+ buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.CAPTURE, TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.USD),
+ buildPaymentTransaction(paymentId, chargebackExternalKey, TransactionType.CHARGEBACK, TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.EUR),
+ buildPaymentTransaction(paymentId, chargebackExternalKey, TransactionType.CHARGEBACK, TransactionStatus.PAYMENT_FAILURE, BigDecimal.ONE, Currency.EUR));
+ final Payment payment = buildPayment(paymentId, transactions);
+ Assert.assertEquals(payment.getCurrency(), Currency.EUR);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ }
+
+ @Test(groups = "fast")
public void testAmountsCaptureChargebackReversedAndRefund() throws Exception {
final UUID paymentId = UUID.randomUUID();
final String chargebackExternalKey = UUID.randomUUID().toString();
@@ -118,6 +132,19 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testAmountsPurchaseChargebackDifferentCurrency() throws Exception {
+ final UUID paymentId = UUID.randomUUID();
+ final String chargebackExternalKey = UUID.randomUUID().toString();
+ final List<PaymentTransaction> transactions = ImmutableList.<PaymentTransaction>of(buildPaymentTransaction(paymentId, UUID.randomUUID().toString(), TransactionType.PURCHASE, TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.USD),
+ buildPaymentTransaction(paymentId, chargebackExternalKey, TransactionType.CHARGEBACK, TransactionStatus.SUCCESS, BigDecimal.ONE, Currency.EUR));
+ final Payment payment = buildPayment(paymentId, transactions);
+ Assert.assertEquals(payment.getAuthAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getCapturedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ }
+
+ @Test(groups = "fast")
public void testAmountsPurchaseChargebackReversed() throws Exception {
final UUID paymentId = UUID.randomUUID();
final String chargebackExternalKey = UUID.randomUUID().toString();
@@ -163,6 +190,10 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
}
private PaymentTransaction buildPaymentTransaction(final UUID paymentId, final String externalKey, final TransactionType transactionType, final TransactionStatus transactionStatus, final BigDecimal amount) {
+ return buildPaymentTransaction(paymentId, externalKey, transactionType, transactionStatus, amount, Currency.USD);
+ }
+
+ private PaymentTransaction buildPaymentTransaction(final UUID paymentId, final String externalKey, final TransactionType transactionType, final TransactionStatus transactionStatus, final BigDecimal amount, final Currency currency) {
return new DefaultPaymentTransaction(UUID.randomUUID(),
UUID.randomUUID(),
externalKey,
@@ -173,9 +204,9 @@ public class TestDefaultPayment extends PaymentTestSuiteNoDB {
clock.getUTCNow(),
transactionStatus,
amount,
- Currency.USD,
+ currency,
amount,
- Currency.USD,
+ currency,
null,
null,
null);