killbill-aplcache

payment: add support for chargebacks in different currencies Signed-off-by:

5/26/2016 10:04:54 AM

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