diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index 15447b6..fef7033 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -20,6 +20,7 @@ package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -467,11 +468,15 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
}
protected Payment refundPaymentAndCheckForCompletion(final Account account, final Payment payment, final NextEvent... events) {
+ return refundPaymentAndCheckForCompletion(account, payment, payment.getPurchasedAmount(), payment.getCurrency(), events);
+ }
+
+ protected Payment refundPaymentAndCheckForCompletion(final Account account, final Payment payment, final BigDecimal amount, final Currency currency, final NextEvent... events) {
return doCallAndCheckForCompletion(new Function<Void, Payment>() {
@Override
public Payment apply(@Nullable final Void input) {
try {
- return paymentApi.createRefundWithPaymentControl(account, payment.getId(), payment.getPurchasedAmount(), payment.getCurrency(), UUID.randomUUID().toString(),
+ return paymentApi.createRefundWithPaymentControl(account, payment.getId(), amount, currency, UUID.randomUUID().toString(),
PLUGIN_PROPERTIES, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
@@ -501,18 +506,21 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
}
protected Payment refundPaymentWithInvoiceItemAdjAndCheckForCompletion(final Account account, final Payment payment, final Map<UUID, BigDecimal> iias, final NextEvent... events) {
+ return refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment, payment.getPurchasedAmount(), payment.getCurrency(), iias, events);
+ }
+
+ protected Payment refundPaymentWithInvoiceItemAdjAndCheckForCompletion(final Account account, final Payment payment, final BigDecimal amount, final Currency currency, final Map<UUID, BigDecimal> iias, final NextEvent... events) {
return doCallAndCheckForCompletion(new Function<Void, Payment>() {
@Override
public Payment apply(@Nullable final Void input) {
-
- final List<PluginProperty> properties = new ArrayList<PluginProperty>();
+ final Collection<PluginProperty> properties = new ArrayList<PluginProperty>();
final PluginProperty prop1 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_REFUND_WITH_ADJUSTMENTS, "true", false);
properties.add(prop1);
final PluginProperty prop2 = new PluginProperty(InvoicePaymentControlPluginApi.PROP_IPCD_REFUND_IDS_WITH_AMOUNT_KEY, iias, false);
properties.add(prop2);
try {
- return paymentApi.createRefundWithPaymentControl(account, payment.getId(), payment.getPurchasedAmount(), payment.getCurrency(), UUID.randomUUID().toString(),
+ return paymentApi.createRefundWithPaymentControl(account, payment.getId(), amount, currency, UUID.randomUUID().toString(),
properties, PAYMENT_OPTIONS, refreshedCallContext());
} catch (final PaymentApiException e) {
fail(e.toString());
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 1db3a59..99bd238 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
@@ -18,7 +18,10 @@
package org.killbill.billing.beatrix.integration;
import java.math.BigDecimal;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
@@ -107,6 +110,82 @@ public class TestInvoicePayment extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testPartialRefunds() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final AccountData accountData = getAccountData(0);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 5, 1), callContext);
+
+ // 2012-05-31 => DAY 30 have to get out of trial {I0, P0}
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ Invoice invoice2 = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 31), new LocalDate(2012, 6, 30), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 30), callContext);
+
+ // Invoice is fully paid
+ Payment payment1 = paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 31), new BigDecimal("249.95"), TransactionStatus.SUCCESS, invoice2.getId(), Currency.USD));
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.ZERO), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger first partial refund ($1), no adjustment
+ payment1 = refundPaymentAndCheckForCompletion(account, payment1, BigDecimal.TEN, Currency.USD, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(BigDecimal.TEN), 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).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger second partial refund ($1), with item adjustment
+ final Map<UUID, BigDecimal> iias = new HashMap<UUID, BigDecimal>();
+ iias.put(invoice2.getInvoiceItems().get(0).getId(), BigDecimal.ONE);
+ payment1 = refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, BigDecimal.ONE, Currency.USD, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(new BigDecimal("11")), 0);
+ Assert.assertEquals(payment1.getTransactions().size(), 3);
+ Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getAmount().compareTo(BigDecimal.ONE), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getProcessedAmount().compareTo(BigDecimal.ONE), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+
+ // Trigger third partial refund ($10), with item adjustment
+ iias.put(invoice2.getInvoiceItems().get(0).getId(), BigDecimal.TEN);
+ payment1 = refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, BigDecimal.TEN, Currency.USD, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
+ Assert.assertEquals(payment1.getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getRefundedAmount().compareTo(new BigDecimal("21")), 0);
+ Assert.assertEquals(payment1.getTransactions().size(), 4);
+ Assert.assertEquals(payment1.getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(1).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getAmount().compareTo(BigDecimal.ONE), 0);
+ Assert.assertEquals(payment1.getTransactions().get(2).getProcessedAmount().compareTo(BigDecimal.ONE), 0);
+ Assert.assertEquals(payment1.getTransactions().get(3).getAmount().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(payment1.getTransactions().get(3).getProcessedAmount().compareTo(BigDecimal.TEN), 0);
+ invoice2 = invoiceUserApi.getInvoice(invoice2.getId(), callContext);
+ Assert.assertEquals(invoice2.getBalance().compareTo(BigDecimal.TEN), 0);
+ Assert.assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(invoice2.getBalance()), 0);
+ }
+
+ @Test(groups = "slow")
public void testPartialPaymentByPaymentPluginThenChargeback() throws Exception {
// 2012-05-01T00:03:42.000Z
clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index 61c2a51..7ccaba6 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -153,6 +153,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
final UUID invoiceId = getInvoiceId(pluginProperties);
existingInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
if (existingInvoicePayment != null && existingInvoicePayment.isSuccess()) {
+ // Only one successful purchase per payment (the invoice could be linked to multiple successful payments though)
log.info("onSuccessCall was already completed for payment purchase: " + paymentControlContext.getPaymentId());
} else {
final BigDecimal invoicePaymentAmount;
@@ -175,20 +176,16 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
break;
case REFUND:
- existingInvoicePayment = invoiceApi.getInvoicePaymentForRefund(paymentControlContext.getPaymentId(), internalContext);
- if (existingInvoicePayment != null) {
- log.info("onSuccessCall was already completed for payment refund: " + paymentControlContext.getPaymentId());
- } else {
- final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
- final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
- final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
- invoiceApi.createRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
- }
+ final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
+ final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
+ final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
+ invoiceApi.createRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
break;
case CHARGEBACK:
existingInvoicePayment = invoiceApi.getInvoicePaymentForChargeback(paymentControlContext.getPaymentId(), internalContext);
if (existingInvoicePayment != null) {
+ // We don't support partial chargebacks (yet?)
log.info("onSuccessCall was already completed for payment chargeback: " + paymentControlContext.getPaymentId());
} else {
final InvoicePayment linkedInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);