killbill-aplcache
Changes
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java 30(+0 -30)
catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java 66(+66 -0)
invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java 43(+40 -3)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 407(+310 -97)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java 235(+233 -2)
invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java 83(+35 -48)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java 13(+10 -3)
payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java 37(+37 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java 32(+27 -5)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java 44(+33 -11)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java 4(+3 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java 47(+47 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java 11(+10 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java 42(+38 -4)
payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java 167(+144 -23)
payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java 13(+10 -3)
payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java 106(+106 -0)
payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java 4(+4 -0)
Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
index f7fb6b3..ff36bb8 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
@@ -21,26 +21,18 @@ package org.killbill.billing.beatrix.integration.overdue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.UUID;
-import java.util.concurrent.Callable;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.beatrix.integration.BeatrixIntegrationModule;
import org.killbill.billing.beatrix.integration.TestIntegrationBase;
import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.entitlement.api.BlockingState;
-import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
-import org.killbill.billing.overdue.OverdueService;
import org.killbill.billing.overdue.config.DefaultOverdueConfig;
-import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.payment.api.PaymentMethodPlugin;
import org.killbill.billing.payment.api.TestPaymentMethodPluginBase;
import org.killbill.xmlloader.XMLLoader;
-import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
-import static com.jayway.awaitility.Awaitility.await;
-import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.Assert.assertNotNull;
public abstract class TestOverdueBase extends TestIntegrationBase {
@@ -78,26 +70,4 @@ public abstract class TestOverdueBase extends TestIntegrationBase {
protected void checkODState(final String expected) {
checkODState(expected, account.getId());
}
-
- protected void checkODState(final String expected, final UUID accountId) {
- try {
- // This will test the overdue notification queue: when we move the clock, the overdue system
- // should get notified to refresh its state.
- // Calling explicitly refresh here (overdueApi.refreshOverdueStateFor(account)) would not fully
- // test overdue.
- // Since we're relying on the notification queue, we may need to wait a bit (hence await()).
- await().atMost(10, SECONDS).until(new Callable<Boolean>() {
- @Override
- public Boolean call() throws Exception {
- final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
- final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
- return expected.equals(stateName);
- }
- });
- } catch (final Exception e) {
- final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
- final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
- Assert.assertEquals(stateName, expected, "Got exception: " + e.toString());
- }
- }
}
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 bab10ab..7697a94 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
@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.Callable;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -56,6 +57,8 @@ import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.Entitlement;
import org.killbill.billing.entitlement.api.EntitlementApi;
@@ -76,11 +79,14 @@ import org.killbill.billing.lifecycle.api.Lifecycle;
import org.killbill.billing.lifecycle.glue.BusModule;
import org.killbill.billing.mock.MockAccountBuilder;
import org.killbill.billing.osgi.config.OSGIConfig;
+import org.killbill.billing.overdue.OverdueService;
import org.killbill.billing.overdue.api.OverdueApi;
import org.killbill.billing.overdue.api.OverdueConfig;
import org.killbill.billing.overdue.caching.OverdueConfigCache;
import org.killbill.billing.overdue.listener.OverdueListener;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.payment.api.AdminPaymentApi;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentApiException;
@@ -112,6 +118,7 @@ import org.killbill.billing.util.tag.Tag;
import org.killbill.bus.api.PersistentBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
@@ -126,6 +133,8 @@ import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Stage;
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
@@ -200,6 +209,9 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
protected PaymentApi paymentApi;
@Inject
+ protected AdminPaymentApi adminPaymentApi;
+
+ @Inject
protected EntitlementApi entitlementApi;
@Inject
@@ -349,6 +361,28 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
assertTrue(ctd.toDateTime(testTimeZone).toLocalDate().compareTo(new LocalDate(chargeThroughDate.getYear(), chargeThroughDate.getMonthOfYear(), chargeThroughDate.getDayOfMonth())) == 0);
}
+ protected void checkODState(final String expected, final UUID accountId) {
+ try {
+ // This will test the overdue notification queue: when we move the clock, the overdue system
+ // should get notified to refresh its state.
+ // Calling explicitly refresh here (overdueApi.refreshOverdueStateFor(account)) would not fully
+ // test overdue.
+ // Since we're relying on the notification queue, we may need to wait a bit (hence await()).
+ await().atMost(10, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
+ return expected.equals(stateName);
+ }
+ });
+ } catch (final Exception e) {
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
+ Assert.assertEquals(stateName, expected, "Got exception: " + e.toString());
+ }
+ }
+
protected DefaultSubscriptionBase subscriptionDataFromSubscription(final SubscriptionBase sub) {
return (DefaultSubscriptionBase) sub;
}
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 7444840..25c4902 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
@@ -17,6 +17,8 @@
package org.killbill.billing.beatrix.integration;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
@@ -24,8 +26,6 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
-import javax.annotation.Nullable;
-
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
@@ -46,12 +46,17 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
+import org.killbill.billing.overdue.config.DefaultOverdueConfig;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentOptions;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.killbill.xmlloader.XMLLoader;
+import org.mockito.Mockito;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.tweak.HandleCallback;
import org.testng.Assert;
@@ -68,8 +73,6 @@ import static org.testng.Assert.assertTrue;
public class TestInvoicePayment extends TestIntegrationBase {
-
-
@Test(groups = "slow")
public void testCancellationEOTWithInvoiceItemAdjustmentsOnInvoiceWithMultipleItems() throws Exception {
final int billingDay = 1;
@@ -129,12 +132,8 @@ public class TestInvoicePayment extends TestIntegrationBase {
Assert.assertEquals(fourthInvoice.getInvoiceItems().size(), 1);
invoiceChecker.checkInvoice(account.getId(), 4, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2016, 11, 1), new LocalDate(2016, 12, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-
-
}
-
-
@Test(groups = "slow")
public void testPartialPaymentByPaymentPlugin() throws Exception {
// 2012-05-01T00:03:42.000Z
@@ -600,6 +599,415 @@ public class TestInvoicePayment extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testWithPendingPaymentThenSuccess() throws Exception {
+ // Verify integration with Overdue in that particular test
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ paymentPlugin.makeNextPaymentPending();
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // INVOICE_PAYMENT_ERROR is sent for PENDING payments
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Verify account transitions to OD1 while payment is PENDING
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+
+ // Transition the payment to success
+ final List<String> paymentControlPluginNames = ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ final PaymentOptions paymentOptions = Mockito.mock(PaymentOptions.class);
+ Mockito.when(paymentOptions.getPaymentControlPluginNames()).thenReturn(paymentControlPluginNames);
+
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ paymentApi.notifyPendingTransactionOfStateChangedWithPaymentControl(account, payments.get(0).getTransactions().get(0).getId(), true, paymentOptions, callContext);
+ assertListenerStatus();
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertTrue(invoice2.getPayments().get(0).isSuccess());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+ }
+
+ @Test(groups = "slow")
+ public void testWithPendingPaymentThenFailure() throws Exception {
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ paymentPlugin.makeNextPaymentPending();
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // INVOICE_PAYMENT_ERROR is sent for PENDING payments
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Transition the payment to failure
+ final List<String> paymentControlPluginNames = ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ final PaymentOptions paymentOptions = Mockito.mock(PaymentOptions.class);
+ Mockito.when(paymentOptions.getPaymentControlPluginNames()).thenReturn(paymentControlPluginNames);
+
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ paymentApi.notifyPendingTransactionOfStateChangedWithPaymentControl(account, payments.get(0).getTransactions().get(0).getId(), false, paymentOptions, callContext);
+ assertListenerStatus();
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertEquals(invoice2, invoice1);
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(accountBalance1) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ // Note that because notifyPendingTransactionOfStateChangedWithPaymentControl is considered an API call, no retry will be attempted
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "ABORTED");
+ }
+
+ @Test(groups = "slow")
+ public void testWithSuccessfulPaymentFixedToFailure() throws Exception {
+ // Verify integration with Overdue in that particular test
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertTrue(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Transition the payment to failure
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ adminPaymentApi.fixPaymentTransactionState(payments.get(0), payments.get(0).getTransactions().get(0), TransactionStatus.PAYMENT_FAILURE, null, null, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertEquals(invoice2.getPayments().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoice2.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice2.getPayments().get(0).isSuccess());
+ assertNotNull(invoice2.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ // Note that because fixPaymentTransactionState is considered an API call, no retry will be attempted
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "ABORTED");
+
+ // Verify account transitions to OD1
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+ }
+
+ @Test(groups = "slow")
+ public void testWithFailedPaymentFixedToSuccess() throws Exception {
+ // Verify integration with Overdue in that particular test
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ paymentPlugin.makeNextPaymentFailWithError();
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 2);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "RETRIED");
+ assertEquals(payments.get(0).getPaymentAttempts().get(1).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(1).getStateName(), "SCHEDULED");
+
+ // Verify account transitions to OD1
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+
+ // Transition the payment to success
+ final PaymentTransaction existingPaymentTransaction = payments.get(0).getTransactions().get(0);
+ final PaymentTransaction updatedPaymentTransaction = Mockito.mock(PaymentTransaction.class);
+ Mockito.when(updatedPaymentTransaction.getId()).thenReturn(existingPaymentTransaction.getId());
+ Mockito.when(updatedPaymentTransaction.getExternalKey()).thenReturn(existingPaymentTransaction.getExternalKey());
+ Mockito.when(updatedPaymentTransaction.getTransactionType()).thenReturn(existingPaymentTransaction.getTransactionType());
+ Mockito.when(updatedPaymentTransaction.getProcessedAmount()).thenReturn(new BigDecimal("249.95"));
+ Mockito.when(updatedPaymentTransaction.getProcessedCurrency()).thenReturn(existingPaymentTransaction.getCurrency());
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ adminPaymentApi.fixPaymentTransactionState(payments.get(0), updatedPaymentTransaction, TransactionStatus.SUCCESS, null, null, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertEquals(invoice2.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice2.getPayments().get(0).getCurrency(), Currency.USD);
+ assertTrue(invoice2.getPayments().get(0).isSuccess());
+ assertNotNull(invoice2.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+ }
+
+ @Test(groups = "slow")
public void testWithIncompletePaymentAttempt() throws Exception {
// 2012-05-01T00:03:42.000Z
clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index 433126a..e8a3a99 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -200,6 +200,11 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
validateCollection(catalog, errors, initialPhases);
finalPhase.validate(catalog, errors);
+
+ if (product == null) {
+ errors.add(new ValidationError(String.format("Invalid product for plan '%s'", name), catalog.getCatalogURI(), DefaultProduct.class, ""));
+ }
+
return errors;
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
index 78b4554..4a034f5 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
@@ -16,18 +16,31 @@
package org.killbill.billing.catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
+import org.killbill.xmlloader.ValidationException;
+import org.killbill.xmlloader.XMLLoader;
import org.testng.Assert;
import org.testng.annotations.Test;
-import org.killbill.billing.catalog.api.CatalogApiException;
-import org.killbill.billing.catalog.api.PhaseType;
-
import com.google.common.collect.ImmutableList;
+import com.google.common.io.Resources;
public class TestStandaloneCatalog extends CatalogTestSuiteNoDB {
@Test(groups = "fast")
+ public void testLoadCatalogWithPlanInvalidProduct() throws Exception {
+ try {
+ XMLLoader.getObjectFromString(Resources.getResource("CatalogWithPlanInvalidProduct.xml").toExternalForm(), StandaloneCatalog.class);
+ Assert.fail();
+ } catch (final ValidationException e) {
+ Assert.assertEquals(e.getErrors().size(), 1);
+ Assert.assertEquals(e.getErrors().get(0).getDescription(), "Invalid product for plan 'standard'");
+ }
+ }
+
+ @Test(groups = "fast")
public void testFindPhase() throws CatalogApiException {
final DefaultPlanPhase phaseTrial1 = new MockPlanPhase().setPhaseType(PhaseType.TRIAL);
final DefaultPlanPhase phaseTrial2 = new MockPlanPhase().setPhaseType(PhaseType.TRIAL);
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
new file mode 100644
index 0000000..2a70ca6
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.xmlloader.XMLLoader;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.io.Resources;
+
+public class TestStandaloneCatalogWithPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCreatePlanNoProduct() throws Exception {
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+ priceOverride,
+ internalCallContext.getTenantRecordId(),
+ internalCallContextFactory);
+
+ try {
+ final PlanSpecifier specWithNullProduct = new PlanSpecifier(null, BillingPeriod.ANNUAL, "DEFAULT");
+ standaloneCatalogWithPriceOverride.createOrFindCurrentPlan(specWithNullProduct, null);
+ Assert.fail();
+ } catch (final CatalogApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.CAT_NULL_PRODUCT_NAME.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCreatePlanInvalidProduct() throws Exception {
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+ priceOverride,
+ internalCallContext.getTenantRecordId(),
+ internalCallContextFactory);
+
+ try {
+ final PlanSpecifier specWithNullProduct = new PlanSpecifier("INVALID", BillingPeriod.ANNUAL, "DEFAULT");
+ standaloneCatalogWithPriceOverride.createOrFindCurrentPlan(specWithNullProduct, null);
+ Assert.fail();
+ } catch (final CatalogApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PRODUCT.getCode());
+ }
+ }
+}
diff --git a/catalog/src/test/resources/CatalogWithPlanInvalidProduct.xml b/catalog/src/test/resources/CatalogWithPlanInvalidProduct.xml
new file mode 100644
index 0000000..55c036a
--- /dev/null
+++ b/catalog/src/test/resources/CatalogWithPlanInvalidProduct.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ Copyright 2014-2016 Groupon, Inc
+ ~ Copyright 2014-2016 The Billing Project, LLC
+ ~
+ ~ The Billing Project licenses this file to you under the Apache License, version 2.0
+ ~ (the "License"); you may not use this file except in compliance with the
+ ~ License. You may obtain a copy of the License at:
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ ~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ ~ License for the specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="CatalogSchema.xsd ">
+
+ <effectiveDate>2013-02-08T00:00:00+00:00</effectiveDate>
+ <catalogName>CatalogWithPlanInvalidProduct</catalogName>
+
+ <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
+
+ <currencies>
+ <currency>USD</currency>
+ </currencies>
+
+ <products>
+ <product name="Standard">
+ <category>BASE</category>
+ </product>
+ </products>
+
+ <rules></rules>
+
+ <plans>
+ <plan name="standard">
+ <!-- Note the typo: the product name matches the plan name, so there is an XML ID - but it doesn't match the product name -->
+ <product>standard</product>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>NO_BILLING_PERIOD</billingPeriod>
+ </recurring>
+ </finalPhase>
+ </plan>
+ </plans>
+
+ <priceLists>
+ <defaultPriceList name="DEFAULT">
+ <plans>
+ <plan>standard</plan>
+ </plans>
+ </defaultPriceList>
+ </priceLists>
+</catalog>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index d7eb9fd..90f868b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -100,7 +100,6 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
dao.notifyOfPaymentInit(new InvoicePaymentModelDao(invoicePayment), context);
}
-
@Override
public void recordPaymentAttemptCompletion(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final String transactionExternalKey, final DateTime paymentDate, final boolean success, final InternalCallContext context) throws InvoiceApiException {
final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, transactionExternalKey, success);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 908e709..0baf924 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -270,6 +270,9 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
@Override
public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
+ final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate());
+
// Group all new external charges on the same invoice (per currency)
final Map<Currency, Invoice> newInvoicesForExternalCharges = new HashMap<Currency, Invoice>();
final Map<UUID, Invoice> existingInvoicesForExternalCharges = new HashMap<UUID, Invoice>();
@@ -282,7 +285,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
final Currency currency = charge.getCurrency();
if (newInvoicesForExternalCharges.get(currency) == null) {
final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
- final Invoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency, status);
+ final Invoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
newInvoicesForExternalCharges.put(currency, newInvoiceForExternalCharge);
}
invoiceForExternalCharge = newInvoicesForExternalCharges.get(currency);
@@ -348,11 +351,14 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
@Override
public List<Invoice> prepareInvoices() throws InvoiceApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
+ final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate());
+
// Create an invoice for that credit if it doesn't exist
final Invoice invoiceForCredit;
if (invoiceId == null) {
final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
- invoiceForCredit = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency, status);
+ invoiceForCredit = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
} else {
invoiceForCredit = getInvoiceAndCheckCurrency(invoiceId, currency, context);
if (InvoiceStatus.COMMITTED.equals(invoiceForCredit.getStatus())) {
@@ -468,9 +474,8 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
@Override
public UUID createMigrationInvoice(final UUID accountId, final LocalDate targetDate, final Iterable<InvoiceItem> items, final CallContext context) {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
- final LocalDate createdDate = internalCallContext.toLocalDate(internalCallContext.getCreatedDate());
- final InvoiceModelDao migrationInvoice = new InvoiceModelDao(accountId, createdDate, targetDate, items.iterator().next().getCurrency(), true);
-
+ final LocalDate invoiceDate = internalCallContext.toLocalDate(internalCallContext.getCreatedDate());
+ final InvoiceModelDao migrationInvoice = new InvoiceModelDao(accountId, invoiceDate, targetDate, items.iterator().next().getCurrency(), true);
final List<InvoiceItemModelDao> itemModelDaos = ImmutableList.copyOf(Iterables.transform(items, new Function<InvoiceItem, InvoiceItemModelDao>() {
@Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
index acb4dd0..d519cc0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
@@ -53,6 +53,20 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
}
@Override
+ public boolean isSanitySafetyBoundEnabled() {
+ return staticConfig.isSanitySafetyBoundEnabled();
+ }
+
+ @Override
+ public boolean isSanitySafetyBoundEnabled(final InternalTenantContext tenantContext) {
+ final String result = getStringTenantConfig("isSanitySafetyBoundEnabled", tenantContext);
+ if (result != null) {
+ return Boolean.parseBoolean(result);
+ }
+ return isSanitySafetyBoundEnabled();
+ }
+
+ @Override
public int getMaxDailyNumberOfItemsSafetyBound() {
return staticConfig.getMaxDailyNumberOfItemsSafetyBound();
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 204a0e5..c452e53 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -732,13 +732,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
notifyOfPaymentCompletionInternal(invoicePayment, false, context);
}
-
@Override
public void notifyOfPaymentCompletion(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
notifyOfPaymentCompletionInternal(invoicePayment, true, context);
}
- public void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
+ private void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
@@ -764,7 +763,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
if (existingAttempt == null) {
transactional.create(invoicePayment, context);
- } else if (!existingAttempt.getSuccess()) {
+ } else {
transactional.updateAttempt(existingAttempt.getRecordId(),
invoicePayment.getPaymentId().toString(),
invoicePayment.getPaymentDate().toDate(),
@@ -1120,34 +1119,41 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// create child and parent invoices
- final DateTime effectiveDate = childAccountContext.getCreatedDate();
+ final DateTime childCreatedDate = childAccountContext.getCreatedDate();
final BigDecimal accountCBA = getAccountCBA(childAccount.getId(), childAccountContext);
// create external charge to child account
- final Invoice invoiceForExternalCharge = new DefaultInvoice(childAccount.getId(), effectiveDate.toLocalDate(),
- effectiveDate.toLocalDate(),
- childAccount.getCurrency(), InvoiceStatus.COMMITTED);
+ final LocalDate childInvoiceDate = childAccountContext.toLocalDate(childAccountContext.getCreatedDate());
+ final Invoice invoiceForExternalCharge = new DefaultInvoice(childAccount.getId(),
+ childInvoiceDate,
+ childCreatedDate.toLocalDate(),
+ childAccount.getCurrency(),
+ InvoiceStatus.COMMITTED);
final String chargeDescription = "Charge to move credit from child to parent account";
final InvoiceItem externalChargeItem = new ExternalChargeInvoiceItem(UUIDs.randomUUID(),
- effectiveDate,
+ childCreatedDate,
invoiceForExternalCharge.getId(),
childAccount.getId(),
null,
chargeDescription,
- effectiveDate.toLocalDate(),
+ childCreatedDate.toLocalDate(),
accountCBA,
childAccount.getCurrency());
invoiceForExternalCharge.addInvoiceItem(externalChargeItem);
// create credit to parent account
- final Invoice invoiceForCredit = new DefaultInvoice(childAccount.getParentAccountId(), effectiveDate.toLocalDate(), effectiveDate.toLocalDate(),
- childAccount.getCurrency(), InvoiceStatus.COMMITTED);
+ final LocalDate parentInvoiceDate = parentAccountContext.toLocalDate(parentAccountContext.getCreatedDate());
+ final Invoice invoiceForCredit = new DefaultInvoice(childAccount.getParentAccountId(),
+ parentInvoiceDate,
+ childCreatedDate.toLocalDate(),
+ childAccount.getCurrency(),
+ InvoiceStatus.COMMITTED);
final String creditDescription = "Credit migrated from child account " + childAccount.getId();
final InvoiceItem creditItem = new CreditAdjInvoiceItem(UUIDs.randomUUID(),
- effectiveDate,
+ childCreatedDate,
invoiceForCredit.getId(),
childAccount.getParentAccountId(),
- effectiveDate.toLocalDate(),
+ childCreatedDate.toLocalDate(),
creditDescription,
// Note! The amount is negated here!
accountCBA.negate(),
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
index 589d1c7..8043b07 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
@@ -78,10 +78,6 @@ public class InvoiceModelDao extends EntityModelDaoBase implements EntityModelDa
this(UUIDs.randomUUID(), null, accountId, null, invoiceDate, targetDate, currency, migrated, status, false);
}
- public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency) {
- this(UUIDs.randomUUID(), null, accountId, null, invoiceDate, targetDate, currency, false, InvoiceStatus.COMMITTED, false);
- }
-
public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final Currency currency, final InvoiceStatus status, final boolean isParentInvoice) {
this(UUIDs.randomUUID(), invoiceDate.toDateTimeAtCurrentTime(), accountId, null, invoiceDate, null, currency, false, status, isParentInvoice);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index f35087b..1e64bb6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -76,7 +76,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
validateTargetDate(targetDate, context);
final LocalDate adjustedTargetDate = adjustTargetDate(existingInvoices, targetDate);
- final LocalDate invoiceDate = context.toLocalDate(clock.getUTCNow());
+ final LocalDate invoiceDate = context.toLocalDate(context.getCreatedDate());
final Invoice invoice = new DefaultInvoice(account.getId(), invoiceDate, adjustedTargetDate, targetCurrency);
final UUID invoiceId = invoice.getId();
final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
index 6dc31d6..eaa69f8 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -20,6 +20,7 @@ package org.killbill.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -40,6 +41,7 @@ import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvalidDateSequenceException;
@@ -59,6 +61,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
+import com.google.common.collect.Range;
import com.google.inject.Inject;
import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
@@ -107,7 +110,7 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
accountItemTree.mergeWithProposedItems(proposedItems);
final List<InvoiceItem> resultingItems = accountItemTree.getResultingItemList();
- safetyBound(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
return resultingItems;
}
@@ -403,8 +406,42 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
}
}
- // Trigger an exception if we create too many subscriptions for a subscription on a given day
- private void safetyBound(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+ @VisibleForTesting
+ void safetyBounds(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+ // Trigger an exception if we detect the creation of similar items for a given subscription
+ // See https://github.com/killbill/killbill/issues/664
+ if (config.isSanitySafetyBoundEnabled(internalCallContext)) {
+ final Map<UUID, Multimap<LocalDate, InvoiceItem>> fixedItemsPerDateAndSubscription = new HashMap<UUID, Multimap<LocalDate, InvoiceItem>>();
+ final Map<UUID, Multimap<Range<LocalDate>, InvoiceItem>> recurringItemsPerServicePeriodAndSubscription = new HashMap<UUID, Multimap<Range<LocalDate>, InvoiceItem>>();
+ for (final InvoiceItem resultingItem : resultingItems) {
+ if (resultingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
+ if (fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()) == null) {
+ fixedItemsPerDateAndSubscription.put(resultingItem.getSubscriptionId(), LinkedListMultimap.<LocalDate, InvoiceItem>create());
+ }
+ fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()).put(resultingItem.getStartDate(), resultingItem);
+
+ final Collection<InvoiceItem> resultingInvoiceItems = fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()).get(resultingItem.getStartDate());
+ if (resultingInvoiceItems.size() > 1) {
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED Multiple FIXED items for subscriptionId='%s', startDate='%s', resultingItems=%s",
+ resultingItem.getSubscriptionId(), resultingItem.getStartDate(), resultingInvoiceItems));
+ }
+ } else if (resultingItem.getInvoiceItemType() == InvoiceItemType.RECURRING) {
+ if (recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()) == null) {
+ recurringItemsPerServicePeriodAndSubscription.put(resultingItem.getSubscriptionId(), LinkedListMultimap.<Range<LocalDate>, InvoiceItem>create());
+ }
+ final Range<LocalDate> interval = Range.<LocalDate>closedOpen(resultingItem.getStartDate(), resultingItem.getEndDate());
+ recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()).put(interval, resultingItem);
+
+ final Collection<InvoiceItem> resultingInvoiceItems = recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()).get(interval);
+ if (resultingInvoiceItems.size() > 1) {
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED Multiple RECURRING items for subscriptionId='%s', startDate='%s', endDate='%s', resultingItems=%s",
+ resultingItem.getSubscriptionId(), resultingItem.getStartDate(), resultingItem.getEndDate(), resultingInvoiceItems));
+ }
+ }
+ }
+ }
+
+ // Trigger an exception if we create too many invoice items for a subscription on a given day
if (config.getMaxDailyNumberOfItemsSafetyBound(internalCallContext) == -1) {
// Safety bound disabled
return;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 01d03b7..cd2c7da 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -723,7 +723,6 @@ public class InvoiceDispatcher {
BigDecimal childInvoiceAmount = InvoiceCalculatorUtils.computeChildInvoiceAmount(childInvoice.getCurrency(), childInvoice.getInvoiceItems());
InvoiceModelDao draftParentInvoice = invoiceDao.getParentDraftInvoice(account.getParentAccountId(), parentContext);
- final DateTime today = clock.getNow(account.getTimeZone());
final String description = account.getExternalKey().concat(" summary");
if (draftParentInvoice != null) {
@@ -737,7 +736,7 @@ public class InvoiceDispatcher {
}
// new item when the parent invoices does not have this child item yet
- final ParentInvoiceItem newParentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
+ final ParentInvoiceItem newParentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), context.getCreatedDate(), draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
draftParentInvoice.addInvoiceItem(new InvoiceItemModelDao(newParentInvoiceItem));
List<InvoiceModelDao> invoices = new ArrayList<InvoiceModelDao>();
@@ -748,8 +747,9 @@ public class InvoiceDispatcher {
return;
}
- draftParentInvoice = new InvoiceModelDao(account.getParentAccountId(), today.toLocalDate(), account.getCurrency(), InvoiceStatus.DRAFT, true);
- InvoiceItem parentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
+ final LocalDate invoiceDate = context.toLocalDate(context.getCreatedDate());
+ draftParentInvoice = new InvoiceModelDao(account.getParentAccountId(), invoiceDate, account.getCurrency(), InvoiceStatus.DRAFT, true);
+ final InvoiceItem parentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), context.getCreatedDate(), draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
draftParentInvoice.addInvoiceItem(new InvoiceItemModelDao(parentInvoiceItem));
// build account date time zone
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
index 6db52ff..5b200f4 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
@@ -73,7 +73,6 @@ public class DefaultInvoice extends EntityBase implements Invoice, Cloneable {
this(invoiceId, null, accountId, invoiceNumber, invoiceDate, targetDate, currency, currency, isMigrationInvoice, false, status, false, null);
}
-
// This CTOR is used to return an existing invoice and must include everything (items, payments, tags,..)
public DefaultInvoice(final InvoiceModelDao invoiceModelDao) {
this(invoiceModelDao.getId(), invoiceModelDao.getCreatedDate(), invoiceModelDao.getAccountId(),
@@ -95,7 +94,6 @@ public class DefaultInvoice extends EntityBase implements Invoice, Cloneable {
}));
}
- // Used to create a new parent invoice
public DefaultInvoice(final UUID accountId, final LocalDate invoiceDate, final Currency currency) {
this(UUID.randomUUID(), null, accountId, null, invoiceDate, null, currency, currency, false, false, InvoiceStatus.DRAFT, true, null);
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index b26dbbb..1bf8600 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -818,7 +818,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"));
final String description = UUID.randomUUID().toString();
- final InvoiceModelDao invoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceModelDao invoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD, false);
final InvoiceItemModelDao externalCharge = new InvoiceItemModelDao(new ExternalChargeInvoiceItem(invoiceForExternalCharge.getId(), accountId, bundleId, description, clock.getUTCToday(), new BigDecimal("15.0"), Currency.USD));
invoiceForExternalCharge.addInvoiceItem(externalCharge);
final InvoiceItemModelDao charge = invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceForExternalCharge), context).get(0);
@@ -1704,7 +1704,6 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceModelDao invoiceModelDao;
if (invoiceId == null) {
invoiceModelDao = new InvoiceModelDao(accountId, effectiveDate, effectiveDate, Currency.USD, false, InvoiceStatus.DRAFT);
-
} else {
invoiceModelDao = invoiceDao.getById(invoiceId, context);
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index e104024..e6eb41f 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -123,6 +123,16 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
}
@Override
+ public boolean isSanitySafetyBoundEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isSanitySafetyBoundEnabled(final InternalTenantContext tenantContext) {
+ return true;
+ }
+
+ @Override
public int getMaxDailyNumberOfItemsSafetyBound() {
return 10;
}
@@ -252,18 +262,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 10, 31));
}
- private SubscriptionBase createSubscription() {
- return createSubscription(UUID.randomUUID(), UUID.randomUUID());
- }
-
- private SubscriptionBase createSubscription(final UUID subscriptionId, final UUID bundleId) {
- final SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
- Mockito.when(sub.getId()).thenReturn(subscriptionId);
- Mockito.when(sub.getBundleId()).thenReturn(bundleId);
-
- return sub;
- }
-
@Test(groups = "fast")
public void testSimpleWithTimeZone() throws InvoiceApiException, CatalogApiException {
final SubscriptionBase sub = createSubscription();
@@ -842,65 +840,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
}
- private MockPlanPhase createMockThirtyDaysPlanPhase(@Nullable final BigDecimal recurringRate) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.THIRTY_DAYS);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase() {
- return new MockPlanPhase(null, null, BillingPeriod.MONTHLY);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.MONTHLY);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.MONTHLY, phaseType);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
- @Nullable final BigDecimal fixedCost,
- final PhaseType phaseType) {
- final MockInternationalPrice recurringPrice = (recurringRate == null) ? null : new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD));
- final MockInternationalPrice fixedPrice = (fixedCost == null) ? null : new MockInternationalPrice(new DefaultPrice(fixedCost, Currency.USD));
-
- return new MockPlanPhase(recurringPrice, fixedPrice, BillingPeriod.MONTHLY, phaseType);
- }
-
- private MockPlanPhase createMockAnnualPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.ANNUAL, phaseType);
- }
-
- private BillingEvent createBillingEvent(final UUID subscriptionId, final UUID bundleId, final LocalDate startDate,
- final Plan plan, final PlanPhase planPhase, final int billCycleDayLocal) throws CatalogApiException {
- final SubscriptionBase sub = createSubscription(subscriptionId, bundleId);
- final Currency currency = Currency.USD;
-
- return invoiceUtil.createMockBillingEvent(null, sub, startDate.toDateTimeAtStartOfDay(), plan, planPhase,
- planPhase.getFixed().getPrice() == null ? null : planPhase.getFixed().getPrice().getPrice(currency),
- planPhase.getRecurring().getRecurringPrice() == null ? null : planPhase.getRecurring().getRecurringPrice().getPrice(currency),
- currency, planPhase.getRecurring().getBillingPeriod(),
- billCycleDayLocal, BillingMode.IN_ADVANCE, "Test", 1L, SubscriptionBaseTransitionType.CREATE);
- }
-
- private void testInvoiceGeneration(final UUID accountId, final BillingEventSet events, final List<Invoice> existingInvoices,
- final LocalDate targetDate, final int expectedNumberOfItems,
- final BigDecimal expectedAmount) throws InvoiceApiException {
- final Currency currency = Currency.USD;
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
- final Invoice invoice = invoiceWithMetadata.getInvoice();
- assertNotNull(invoice);
- assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
- existingInvoices.add(invoice);
-
- distributeItems(existingInvoices);
- assertEquals(invoice.getBalance(), KillBillMoney.of(expectedAmount, invoice.getCurrency()));
- }
-
@Test(groups = "fast")
public void testWithFullRepairInvoiceGeneration() throws CatalogApiException, InvoiceApiException {
final LocalDate april25 = new LocalDate(2012, 4, 25);
@@ -1110,32 +1049,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(newInvoice);
}
- private void distributeItems(final List<Invoice> invoices) {
- final Map<UUID, Invoice> invoiceMap = new HashMap<UUID, Invoice>();
-
- for (final Invoice invoice : invoices) {
- invoiceMap.put(invoice.getId(), invoice);
- }
-
- for (final Invoice invoice : invoices) {
- final Iterator<InvoiceItem> itemIterator = invoice.getInvoiceItems().iterator();
- final UUID invoiceId = invoice.getId();
-
- while (itemIterator.hasNext()) {
- final InvoiceItem item = itemIterator.next();
-
- if (!item.getInvoiceId().equals(invoiceId)) {
- final Invoice thisInvoice = invoiceMap.get(item.getInvoiceId());
- if (thisInvoice == null) {
- throw new NullPointerException();
- }
- thisInvoice.addInvoiceItem(item);
- itemIterator.remove();
- }
- }
- }
- }
-
@Test(groups = "fast")
public void testAutoInvoiceOffAccount() throws Exception {
final MockBillingEventSet events = new MockBillingEventSet();
@@ -1158,6 +1071,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(invoiceWithMetadata.getInvoice());
}
+ @Test(groups = "fast")
public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceApiException {
final Currency currency = Currency.USD;
final List<Invoice> invoices = new ArrayList<Invoice>();
@@ -1257,6 +1171,305 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(invoice2);
}
+ // Complex but plausible scenario, with multiple same-day changes, to verify bounds are not triggered
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testMultipleDailyChangesDoNotTriggerBounds() throws InvoiceApiException, CatalogApiException {
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId1 = UUID.randomUUID();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ Invoice invoice;
+
+ final Plan plan1 = new MockPlan("plan1");
+ final PlanPhase plan1Phase1 = createMockMonthlyPlanPhase(null, EIGHT, PhaseType.TRIAL);
+ final PlanPhase plan1Phase2 = createMockMonthlyPlanPhase(TWELVE, PhaseType.DISCOUNT);
+ final LocalDate plan1StartDate = invoiceUtil.buildDate(2011, 1, 5);
+ final LocalDate plan1PhaseChangeDate = invoiceUtil.buildDate(2011, 4, 5);
+
+ final Plan plan2 = new MockPlan("plan2");
+ final PlanPhase plan2Phase1 = createMockMonthlyPlanPhase(null, TWENTY, PhaseType.TRIAL);
+ final PlanPhase plan2Phase2 = createMockMonthlyPlanPhase(THIRTY, PhaseType.DISCOUNT);
+ final PlanPhase plan2Phase3 = createMockMonthlyPlanPhase(FORTY, PhaseType.EVERGREEN);
+ final PlanPhase plan2Phase4 = createMockMonthlyPlanPhase();
+ final LocalDate plan2PhaseChangeToEvergreenDate = invoiceUtil.buildDate(2011, 6, 5);
+ final LocalDate plan2CancelDate = invoiceUtil.buildDate(2011, 6, 5);
+
+ // On 1/5/2011, start TRIAL on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, EIGHT);
+ invoice = invoices.get(0);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(EIGHT), 0);
+
+ // On 1/5/2011, change to TRIAL on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan2, plan2Phase1, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, TWENTY);
+ assertEquals(invoices.get(0), invoice);
+ invoice = invoices.get(1);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWENTY), 0);
+
+ // On 1/5/2011, change back to TRIAL on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+
+ // We don't repair FIXED items and one already exists for that date - nothing to generate
+ testNullInvoiceGeneration(events, invoices, plan1StartDate);
+
+ // On 4/5/2011, phase change to DISCOUNT on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 1, TWELVE);
+ assertEquals(invoices.get(1), invoice);
+ invoice = invoices.get(2);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+
+ // On 4/5/2011, change to DISCOUNT on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan2, plan2Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("18"));
+ assertEquals(invoices.get(2), invoice);
+ invoice = invoices.get(3);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(2).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(TWELVE.negate()), 0);
+
+ // On 4/5/2011, change back to DISCOUNT on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("-18"));
+ assertEquals(invoices.get(3), invoice);
+ invoice = invoices.get(4);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(3).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(THIRTY.negate()), 0);
+
+ // On 4/5/2011, change back to DISCOUNT on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan2, plan2Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("18"));
+ assertEquals(invoices.get(4), invoice);
+ invoice = invoices.get(5);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(4).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(TWELVE.negate()), 0);
+
+ // On 6/5/2011, phase change to EVERGREEN on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan2PhaseChangeToEvergreenDate, plan2, plan2Phase3, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan2PhaseChangeToEvergreenDate, 2, new BigDecimal("70"));
+ assertEquals(invoices.get(5), invoice);
+ invoice = invoices.get(6);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 7, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(FORTY), 0);
+
+ // On 6/5/2011, cancel subscription
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan2CancelDate, plan2, plan2Phase4, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan2PhaseChangeToEvergreenDate, 1, FORTY.negate());
+ assertEquals(invoices.get(6), invoice);
+ invoice = invoices.get(7);
+ assertEquals(invoice.getInvoiceItems().get(0).getLinkedItemId(), invoices.get(6).getInvoiceItems().get(1).getId());
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 7, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(FORTY.negate()), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testBuggyBillingEventsDoNotImpactInvoicing() throws InvoiceApiException, CatalogApiException {
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId1 = UUID.randomUUID();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ Invoice invoice;
+
+ final Plan plan1 = new MockPlan("plan1");
+ final PlanPhase plan1Phase1 = createMockMonthlyPlanPhase(null, EIGHT, PhaseType.TRIAL);
+ final PlanPhase plan1Phase2 = createMockMonthlyPlanPhase(TWELVE, PhaseType.EVERGREEN);
+ final LocalDate plan1StartDate = invoiceUtil.buildDate(2011, 1, 5);
+ final LocalDate plan1PhaseChangeDate = invoiceUtil.buildDate(2011, 2, 5);
+
+ // To simulate a bug, duplicate the billing events
+ for (int i = 0; i < 10; i++) {
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+ }
+ assertEquals(events.size(), 20);
+
+ // Fix for https://github.com/killbill/killbill/issues/467 will prevent duplicate fixed items
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, EIGHT);
+ invoice = invoices.get(0);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(EIGHT), 0);
+
+ // Intermediate billing intervals associated with recurring items will be less than a day, so only one recurring item will be generated
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 1, TWELVE);
+ invoice = invoices.get(1);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 2, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 3, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+ }
+
+ private Long totalOrdering = 1L;
+
+ private MockPlanPhase createMockThirtyDaysPlanPhase(@Nullable final BigDecimal recurringRate) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.THIRTY_DAYS);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase() {
+ return new MockPlanPhase(null, null, BillingPeriod.MONTHLY);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.MONTHLY);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
+ @Nullable final BigDecimal fixedCost,
+ final PhaseType phaseType) {
+ final MockInternationalPrice recurringPrice = (recurringRate == null) ? null : new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD));
+ final MockInternationalPrice fixedPrice = (fixedCost == null) ? null : new MockInternationalPrice(new DefaultPrice(fixedCost, Currency.USD));
+
+ return new MockPlanPhase(recurringPrice, fixedPrice, BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private MockPlanPhase createMockAnnualPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.ANNUAL, phaseType);
+ }
+
+ private SubscriptionBase createSubscription() {
+ return createSubscription(UUID.randomUUID(), UUID.randomUUID());
+ }
+
+ private SubscriptionBase createSubscription(final UUID subscriptionId, final UUID bundleId) {
+ final SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
+ Mockito.when(sub.getId()).thenReturn(subscriptionId);
+ Mockito.when(sub.getBundleId()).thenReturn(bundleId);
+
+ return sub;
+ }
+
+ private BillingEvent createBillingEvent(final UUID subscriptionId, final UUID bundleId, final LocalDate startDate,
+ final Plan plan, final PlanPhase planPhase, final int billCycleDayLocal) throws CatalogApiException {
+ final SubscriptionBase sub = createSubscription(subscriptionId, bundleId);
+ final Currency currency = Currency.USD;
+
+ return invoiceUtil.createMockBillingEvent(null, sub, startDate.toDateTimeAtStartOfDay(), plan, planPhase,
+ planPhase.getFixed().getPrice() == null ? null : planPhase.getFixed().getPrice().getPrice(currency),
+ planPhase.getRecurring().getRecurringPrice() == null ? null : planPhase.getRecurring().getRecurringPrice().getPrice(currency),
+ currency, planPhase.getRecurring().getBillingPeriod(),
+ billCycleDayLocal, BillingMode.IN_ADVANCE, "Test", totalOrdering++, SubscriptionBaseTransitionType.CREATE);
+ }
+
+ private void testInvoiceGeneration(final UUID accountId, final BillingEventSet events, final List<Invoice> existingInvoices,
+ final LocalDate targetDate, final int expectedNumberOfItems,
+ final BigDecimal expectedAmount) throws InvoiceApiException {
+ final Currency currency = Currency.USD;
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+ assertNotNull(invoice);
+ assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
+ existingInvoices.add(invoice);
+
+ distributeItems(existingInvoices);
+ assertEquals(invoice.getBalance(), KillBillMoney.of(expectedAmount, invoice.getCurrency()));
+ }
+
+ private void testNullInvoiceGeneration(final BillingEventSet events, final List<Invoice> existingInvoices, final LocalDate targetDate) throws InvoiceApiException {
+ final Currency currency = Currency.USD;
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+ assertNull(invoice);
+ }
+
+ private void distributeItems(final List<Invoice> invoices) {
+ final Map<UUID, Invoice> invoiceMap = new HashMap<UUID, Invoice>();
+
+ for (final Invoice invoice : invoices) {
+ invoiceMap.put(invoice.getId(), invoice);
+ }
+
+ for (final Invoice invoice : invoices) {
+ final Iterator<InvoiceItem> itemIterator = invoice.getInvoiceItems().iterator();
+ final UUID invoiceId = invoice.getId();
+
+ while (itemIterator.hasNext()) {
+ final InvoiceItem item = itemIterator.next();
+
+ if (!item.getInvoiceId().equals(invoiceId)) {
+ final Invoice thisInvoice = invoiceMap.get(item.getInvoiceId());
+ if (thisInvoice == null) {
+ throw new NullPointerException();
+ }
+ thisInvoice.addInvoiceItem(item);
+ itemIterator.remove();
+ }
+ }
+ }
+ }
+
private void printDetailInvoice(final Invoice invoice) {
log.info("-------------------- START DETAIL ----------------------");
log.info("Invoice " + invoice.getId() + ": BALANCE = " + invoice.getBalance()
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 5d81097..7f19ff7 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -19,6 +19,7 @@ package org.killbill.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -55,6 +56,9 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
@@ -78,7 +82,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
}
}
-
@Test(groups = "fast")
public void testIsSameDayAndSameSubscriptionWithNullPrevEvent() {
@@ -286,7 +289,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
}
@Test(groups = "fast")
- public void testSafetyBounds() throws InvoiceApiException {
+ public void testSafetyBoundsTooManyInvoiceItemsForGivenSubscriptionAndInvoiceDate() throws InvoiceApiException {
final int threshold = 15;
final LocalDate startDate = new LocalDate("2016-01-01");
@@ -373,4 +376,232 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
}
}
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDate() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(null, price, BillingPeriod.NO_BILLING_PERIOD, PhaseType.TRIAL);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ amount,
+ null,
+ account.getCurrency(),
+ BillingPeriod.NO_BILLING_PERIOD,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a bunch of fixed items for that subscription and start date (simulate bad data on disk)
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ for (int i = 0; i < 20; i++) {
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ "Buggy fixed item",
+ startDate,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+ }
+
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ // There will be one proposed, but because it will match one of ones in the existing list and we don't repair, it won't be returned
+ assertEquals(generatedItems.size(), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testSubscriptionAlreadyDoubleBilledForServicePeriod() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a bunch of recurring items for that subscription and service period (bad data on disk leading to double billing)
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ for (int i = 0; i < 20; i++) {
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate.plusMonths(i), account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ // Set random dates to verify it doesn't impact double billing detection
+ startDate.plusMonths(i).toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusMonths(1),
+ amount,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+ }
+
+ try {
+ // There will be one proposed item but the tree will refuse the merge because of the bad state on disk
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final IllegalStateException e) {
+ assertTrue(e.getMessage().startsWith("Double billing detected"));
+ }
+ }
+
+ // Simulate a bug in the generator where two fixed items for the same day and subscription end up in the resulting items
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDatePostMerge() throws InvoiceApiException {
+ final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final Collection<InvoiceItem> resultingItems = new LinkedList<InvoiceItem>();
+ final InvoiceItem fixedPriceInvoiceItem = new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ "description",
+ startDate,
+ BigDecimal.ONE,
+ account.getCurrency());
+ resultingItems.add(fixedPriceInvoiceItem);
+ resultingItems.add(fixedPriceInvoiceItem);
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+
+ resultingItems.clear();
+ for (int i = 0; i < 2; i++) {
+ resultingItems.add(new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ "description",
+ startDate,
+ // Amount shouldn't have any effect
+ BigDecimal.ONE.add(new BigDecimal(i)),
+ account.getCurrency()));
+ }
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+ }
+
+ // Simulate a bug in the generator where two recurring items for the same service period and subscription end up in the resulting items
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyRecurringInvoiceItemsForGivenSubscriptionAndServicePeriodPostMerge() throws InvoiceApiException {
+ final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final Collection<InvoiceItem> resultingItems = new LinkedList<InvoiceItem>();
+ final InvoiceItem recurringInvoiceItem = new RecurringInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ startDate,
+ startDate.plusMonths(1),
+ BigDecimal.ONE,
+ BigDecimal.ONE,
+ account.getCurrency());
+ resultingItems.add(recurringInvoiceItem);
+ resultingItems.add(recurringInvoiceItem);
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+
+ resultingItems.clear();
+ for (int i = 0; i < 2; i++) {
+ resultingItems.add(new RecurringInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ startDate,
+ startDate.plusMonths(1),
+ // Amount shouldn't have any effect
+ BigDecimal.TEN,
+ BigDecimal.ONE,
+ account.getCurrency()));
+ }
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
index 34b3004..f072706 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
@@ -366,7 +366,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
public void testProcessedCurrencyExists() throws Exception {
// Use InvoiceModelDao to build the invoice to be able to set the processedCurrency (No suitable CTOR for DefaultInvoice on purpose)
- final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BRL);
+ final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BRL, false);
invoiceModelDao.setProcessedCurrency(Currency.USD);
final Invoice invoice = new DefaultInvoice(invoiceModelDao);
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
index 18f1feb..9ebad50 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
@@ -17,30 +17,40 @@
package org.killbill.billing.payment.api;
+import java.util.List;
+
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.payment.core.PaymentTransactionInfoPluginConverter;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentAttemptTask;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
+import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.PaymentConfig;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPaymentApi {
private final PaymentStateMachineHelper paymentSMHelper;
+ private final IncompletePaymentAttemptTask incompletePaymentAttemptTask;
private final PaymentDao paymentDao;
private final InternalCallContextFactory internalCallContextFactory;
@Inject
public DefaultAdminPaymentApi(final PaymentConfig paymentConfig,
final PaymentStateMachineHelper paymentSMHelper,
+ final IncompletePaymentAttemptTask incompletePaymentAttemptTask,
final PaymentDao paymentDao,
final InternalCallContextFactory internalCallContextFactory) {
super(paymentConfig, internalCallContextFactory);
this.paymentSMHelper = paymentSMHelper;
+ this.incompletePaymentAttemptTask = incompletePaymentAttemptTask;
this.paymentDao = paymentDao;
this.internalCallContextFactory = internalCallContextFactory;
}
@@ -111,5 +121,20 @@ public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPayme
paymentTransaction.getGatewayErrorCode(),
paymentTransaction.getGatewayErrorMsg(),
internalCallContext);
+
+ // If there is a payment attempt associated with that transaction, we need to update it as well
+ final List<PaymentAttemptModelDao> paymentAttemptsModelDao = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransaction.getExternalKey(), internalCallContext);
+ final PaymentAttemptModelDao paymentAttemptModelDao = Iterables.<PaymentAttemptModelDao>tryFind(paymentAttemptsModelDao,
+ new Predicate<PaymentAttemptModelDao>() {
+ @Override
+ public boolean apply(final PaymentAttemptModelDao input) {
+ return paymentTransaction.getId().equals(input.getTransactionId());
+ }
+ }).orNull();
+ if (paymentAttemptModelDao != null) {
+ // We can re-use the logic from IncompletePaymentAttemptTask as it is doing very similar work (i.e. run the completion part of
+ // the state machine to call the plugins and update the attempt in the right terminal state)
+ incompletePaymentAttemptTask.doIteration(paymentAttemptModelDao);
+ }
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index b4bb5f4..c98a029 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -722,7 +722,50 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
@Override
public Payment notifyPendingTransactionOfStateChangedWithPaymentControl(final Account account, final UUID paymentTransactionId, final boolean isSuccess, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
- throw new IllegalStateException("Not implemented");
+ final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
+ if (paymentControlPluginNames.isEmpty()) {
+ return notifyPendingTransactionOfStateChanged(account, paymentTransactionId, isSuccess, callContext);
+ }
+
+ checkNotNullParameter(account, "account");
+ checkNotNullParameter(paymentTransactionId, "paymentTransactionId");
+
+ final String transactionType = "NOTIFY_STATE_CHANGE";
+ Payment payment = null;
+ PaymentTransaction paymentTransaction = null;
+ PaymentApiException exception = null;
+ try {
+ logEnterAPICall(log, transactionType, account, null, null, paymentTransactionId, null, null, null, null, null, paymentControlPluginNames);
+
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+ payment = pluginControlPaymentProcessor.notifyPendingPaymentOfStateChanged(IS_API_PAYMENT, account, paymentTransactionId, isSuccess, paymentControlPluginNames, callContext, internalCallContext);
+
+ paymentTransaction = Iterables.<PaymentTransaction>tryFind(payment.getTransactions(),
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getId().equals(paymentTransactionId);
+ }
+ }).orNull();
+ return payment;
+ } catch (final PaymentApiException e) {
+ exception = e;
+ throw e;
+ } finally {
+ logExitAPICall(log,
+ transactionType,
+ account,
+ payment != null ? payment.getPaymentMethodId() : null,
+ payment != null ? payment.getId() : null,
+ paymentTransaction != null ? paymentTransaction.getId() : null,
+ paymentTransaction != null ? paymentTransaction.getProcessedAmount() : null,
+ paymentTransaction != null ? paymentTransaction.getProcessedCurrency() : null,
+ payment != null ? payment.getExternalKey() : null,
+ paymentTransaction != null ? paymentTransaction.getExternalKey() : null,
+ paymentTransaction != null ? paymentTransaction.getTransactionStatus() : null,
+ paymentControlPluginNames,
+ exception);
+ }
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
index 20a0655..26bce39 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
@@ -142,9 +142,22 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
try {
priorCallResult = controlPluginRunner.executePluginPriorCalls(account,
paymentMethodId,
- null, null, null, null,
- PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
- null, null, true, paymentControlPluginNames, properties, callContext);
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.HPP,
+ null,
+ HPPType.BUILD_FORM_DESCRIPTOR,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ properties,
+ callContext);
} catch (final PaymentControlApiAbortException e) {
throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_API_ABORTED, e.getPluginName());
@@ -156,16 +169,42 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
final T result = callback.doPaymentGatewayApiOperation(priorCallResult.getAdjustedPaymentMethodId(), priorCallResult.getAdjustedPluginProperties());
controlPluginRunner.executePluginOnSuccessCalls(account,
paymentMethodId,
- null, null, null, null, null,
- PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
- null, null, null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext);
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.HPP,
+ null,
+ HPPType.BUILD_FORM_DESCRIPTOR,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ priorCallResult.getAdjustedPluginProperties(),
+ callContext);
return PluginDispatcher.createPluginDispatcherReturnType(result);
} catch (final PaymentApiException e) {
controlPluginRunner.executePluginOnFailureCalls(account,
paymentMethodId,
- null, null, null, null,
- PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
- null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext);
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.HPP,
+ null,
+ HPPType.BUILD_FORM_DESCRIPTOR,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ priorCallResult.getAdjustedPluginProperties(),
+ callContext);
throw e;
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
index a6b0780..71fee5f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
@@ -100,6 +100,10 @@ abstract class CompletionTaskBase<T> implements Runnable {
}
}
+ public synchronized void start() {
+ this.isStopped = false;
+ }
+
public synchronized void stop() {
this.isStopped = true;
}
@@ -119,15 +123,22 @@ abstract class CompletionTaskBase<T> implements Runnable {
}
protected <T> T doJanitorOperationWithAccountLock(final JanitorIterationCallback callback, final InternalTenantContext internalTenantContext) {
+ try {
+ return tryToDoJanitorOperationWithAccountLock(callback, internalTenantContext);
+ } catch (final LockFailedException e) {
+ log.warn("Error locking accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
+ }
+ return null;
+ }
+
+ protected <T> T tryToDoJanitorOperationWithAccountLock(final JanitorIterationCallback callback, final InternalTenantContext internalTenantContext) throws LockFailedException {
GlobalLock lock = null;
try {
final ImmutableAccountData account = accountInternalApi.getImmutableAccountDataByRecordId(internalTenantContext.getAccountRecordId(), internalTenantContext);
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), paymentConfig.getMaxGlobalLockRetries());
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getId().toString(), paymentConfig.getMaxGlobalLockRetries());
return callback.doIteration();
- } catch (AccountApiException e) {
+ } catch (final AccountApiException e) {
log.warn("Error retrieving accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
- } catch (LockFailedException e) {
- log.warn("Error locking accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
} finally {
if (lock != null) {
lock.release();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
index a33e4e0..568ec94 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
@@ -117,17 +117,7 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
log.warn("Found {} transactions for paymentAttempt {}", filteredTransactions.size(), attempt.getId());
}
final PaymentTransactionModelDao transaction = filteredTransactions.isEmpty() ? null : filteredTransactions.get(0);
-
-
- // In those 3 cases (null transaction, PLUGIN_FAILURE and PAYMENT_FAILURE), we are taking a *shortcut* but this is incorrect; ideally we should call back the priorCall
- // control plugins to decide what to do:
- // * For null transaction and PLUGIN_FAILURE something went wrong before we could even make the payment, so possibly we should inform the control plugin
- // and retry
- // * For PAYMENT_FAILURE, the payment went through but was denied by the gateway, and so this is a different case where a control plugin may want to retry
- //
- if (transaction == null ||
- transaction.getTransactionStatus() == TransactionStatus.PLUGIN_FAILURE ||
- transaction.getTransactionStatus() == TransactionStatus.PAYMENT_FAILURE) {
+ if (transaction == null) {
log.info("Moving attemptId='{}' to ABORTED", attempt.getId());
paymentDao.updatePaymentAttempt(attempt.getId(), attempt.getTransactionId(), "ABORTED", internalCallContext);
return;
@@ -139,43 +129,40 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
return;
}
- // On SUCCESS, PENDING state we complete the payment control state machine, allowing to call the control plugin onSuccessCall API.
- if (transaction.getTransactionStatus() == TransactionStatus.SUCCESS ||
- transaction.getTransactionStatus() == TransactionStatus.PENDING) {
-
- try {
- log.info("Moving attemptId='{}' to SUCCESS", attempt.getId());
-
- final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
- final boolean isApiPayment = true; // unclear
- final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(),
- isApiPayment,
- transaction.getPaymentId(),
- attempt.getPaymentExternalKey(),
- transaction.getTransactionExternalKey(),
- transaction.getTransactionType(),
- account,
- attempt.getPaymentMethodId(),
- transaction.getAmount(),
- transaction.getCurrency(),
- PluginPropertySerializer.deserialize(attempt.getPluginProperties()),
- internalCallContext,
- callContext);
-
- paymentStateContext.setAttemptId(attempt.getId()); // Normally set by leavingState Callback
- paymentStateContext.setPaymentTransactionModelDao(transaction); // Normally set by raw state machine
- //
- // Will rerun the state machine with special callbacks to only make the executePluginOnSuccessCalls call
- // to the PaymentControlPluginApi plugin and transition the state.
- //
- pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
- } catch (final AccountApiException e) {
- log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
- } catch (final PluginPropertySerializerException e) {
- log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
- } catch (final PaymentApiException e) {
- log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
- }
+ try {
+ log.info("Completing attemptId='{}'", attempt.getId());
+
+ final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
+ final boolean isApiPayment = true; // unclear
+ final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(),
+ isApiPayment,
+ null,
+ transaction.getPaymentId(),
+ attempt.getPaymentExternalKey(),
+ transaction.getId(),
+ transaction.getTransactionExternalKey(),
+ transaction.getTransactionType(),
+ account,
+ attempt.getPaymentMethodId(),
+ transaction.getAmount(),
+ transaction.getCurrency(),
+ PluginPropertySerializer.deserialize(attempt.getPluginProperties()),
+ internalCallContext,
+ callContext);
+
+ paymentStateContext.setAttemptId(attempt.getId()); // Normally set by leavingState Callback
+ paymentStateContext.setPaymentTransactionModelDao(transaction); // Normally set by raw state machine
+ //
+ // Will rerun the state machine with special callbacks to only make the executePluginOnSuccessCalls / executePluginOnFailureCalls calls
+ // to the PaymentControlPluginApi plugin and transition the state.
+ //
+ pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
+ } catch (final AccountApiException e) {
+ log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
+ } catch (final PluginPropertySerializerException e) {
+ log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
+ } catch (final PaymentApiException e) {
+ log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index 5fd6dd2..72f050f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -51,6 +51,7 @@ import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.LockFailedException;
import org.killbill.notificationq.api.NotificationEvent;
import org.killbill.notificationq.api.NotificationQueue;
import org.skife.config.TimeSpan;
@@ -93,12 +94,19 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
}
public void processNotification(final JanitorNotificationKey notificationKey, final UUID userToken, final Long accountRecordId, final long tenantRecordId) {
+ try {
+ tryToProcessNotification(notificationKey, userToken, accountRecordId, tenantRecordId);
+ } catch (final LockFailedException e) {
+ log.warn("Error locking accountRecordId='{}', will attempt to retry later", accountRecordId, e);
+ insertNewNotificationForUnresolvedTransactionIfNeeded(notificationKey.getUuidKey(), notificationKey.getAttemptNumber(), userToken, accountRecordId, tenantRecordId);
+ }
+ }
+ public void tryToProcessNotification(final JanitorNotificationKey notificationKey, final UUID userToken, final Long accountRecordId, final long tenantRecordId) throws LockFailedException {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId);
- doJanitorOperationWithAccountLock(new JanitorIterationCallback() {
+ tryToDoJanitorOperationWithAccountLock(new JanitorIterationCallback() {
@Override
public Void doIteration() {
-
// State may have changed since we originally retrieved with no lock
final PaymentTransactionModelDao rehydratedPaymentTransaction = paymentDao.getPaymentTransaction(notificationKey.getUuidKey(), internalTenantContext);
@@ -139,7 +147,6 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
return null;
}
}, internalTenantContext);
-
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
index 30456c4..cb340c6 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
@@ -24,19 +24,10 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.joda.time.DateTime;
-import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.events.PaymentInternalEvent;
-import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.core.PaymentExecutors;
-import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper;
-import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
-import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
-import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.glue.DefaultPaymentService;
-import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
-import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.PaymentConfig;
-import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
import org.killbill.notificationq.api.NotificationEvent;
import org.killbill.notificationq.api.NotificationQueue;
@@ -60,55 +51,28 @@ public class Janitor {
private final NotificationQueueService notificationQueueService;
private final PaymentConfig paymentConfig;
private final PaymentExecutors paymentExecutors;
- private final Clock clock;
- private final PaymentDao paymentDao;
- private final InternalCallContextFactory internalCallContextFactory;
- private final PaymentStateMachineHelper paymentStateMachineHelper;
- private final PaymentControlStateMachineHelper retrySMHelper;
- private final AccountInternalApi accountInternalApi;
- private final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
- private final GlobalLocker locker;
- private final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner;
-
-
-
- private IncompletePaymentAttemptTask incompletePaymentAttemptTask;
- private IncompletePaymentTransactionTask incompletePaymentTransactionTask;
+ private final IncompletePaymentAttemptTask incompletePaymentAttemptTask;
+ private final IncompletePaymentTransactionTask incompletePaymentTransactionTask;
private NotificationQueue janitorQueue;
private ScheduledExecutorService janitorExecutor;
private volatile boolean isStopped;
@Inject
- public Janitor(final InternalCallContextFactory internalCallContextFactory,
- final PaymentDao paymentDao,
- final Clock clock,
- final PaymentStateMachineHelper paymentStateMachineHelper,
- final PaymentControlStateMachineHelper retrySMHelper,
- final AccountInternalApi accountInternalApi,
- final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner,
- final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
+ public Janitor(final IncompletePaymentAttemptTask incompletePaymentAttemptTask,
+ final IncompletePaymentTransactionTask incompletePaymentTransactionTask,
final GlobalLocker locker,
final PaymentConfig paymentConfig,
final NotificationQueueService notificationQueueService,
final PaymentExecutors paymentExecutors) {
+ this.incompletePaymentAttemptTask = incompletePaymentAttemptTask;
+ this.incompletePaymentTransactionTask = incompletePaymentTransactionTask;
this.notificationQueueService = notificationQueueService;
this.paymentExecutors = paymentExecutors;
this.paymentConfig = paymentConfig;
- this.internalCallContextFactory = internalCallContextFactory;
- this.paymentDao = paymentDao;
- this.clock = clock;
- this.pluginControlledPaymentAutomatonRunner = pluginControlledPaymentAutomatonRunner;
- this.paymentStateMachineHelper = paymentStateMachineHelper;
- this.retrySMHelper = retrySMHelper;
- this.accountInternalApi = accountInternalApi;
- this.pluginRegistry = pluginRegistry;
- this.locker = locker;
-
}
-
public void initialize() throws NotificationQueueAlreadyExists {
janitorQueue = notificationQueueService.createNotificationQueue(DefaultPaymentService.SERVICE_NAME,
QUEUE_NAME,
@@ -128,36 +92,16 @@ public class Janitor {
}
);
- this.incompletePaymentAttemptTask = new IncompletePaymentAttemptTask(internalCallContextFactory,
- paymentConfig,
- paymentDao,
- clock,
- paymentStateMachineHelper,
- retrySMHelper,
- accountInternalApi,
- pluginControlledPaymentAutomatonRunner,
- pluginRegistry,
- locker);
-
- this.incompletePaymentTransactionTask = new IncompletePaymentTransactionTask(internalCallContextFactory,
- paymentConfig,
- paymentDao,
- clock,
- paymentStateMachineHelper,
- retrySMHelper,
- accountInternalApi,
- pluginRegistry,
- locker);
-
-
incompletePaymentTransactionTask.attachJanitorQueue(janitorQueue);
incompletePaymentAttemptTask.attachJanitorQueue(janitorQueue);
}
public void start() {
-
this.isStopped = false;
+ incompletePaymentAttemptTask.start();
+ incompletePaymentTransactionTask.start();
+
janitorExecutor = paymentExecutors.getJanitorExecutorService();
janitorQueue.startQueue();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
index 34e881b..ebc4fe5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
@@ -110,7 +110,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
return dispatchWithExceptionHandling(account,
paymentPluginServiceName,
new CallableWithAccountLock<UUID, PaymentApiException>(locker,
- account.getExternalKey(),
+ account.getId(),
paymentConfig,
new DispatcherCallback<PluginDispatcherReturnType<UUID>, PaymentApiException>() {
@@ -403,7 +403,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context)
throws PaymentApiException {
try {
- new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
+ new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<Void> doOperation() throws PaymentApiException {
@@ -444,7 +444,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
public void setDefaultPaymentMethod(final Account account, final UUID paymentMethodId, final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context)
throws PaymentApiException {
try {
- new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
+ new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<Void> doOperation() throws PaymentApiException {
@@ -511,7 +511,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
}
try {
- final PluginDispatcherReturnType<List<PaymentMethod>> result = new WithAccountLock<List<PaymentMethod>, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback<PluginDispatcherReturnType<List<PaymentMethod>>, PaymentApiException>() {
+ final PluginDispatcherReturnType<List<PaymentMethod>> result = new WithAccountLock<List<PaymentMethod>, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<List<PaymentMethod>>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<List<PaymentMethod>> doOperation() throws PaymentApiException {
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 6719b7c..3acf35f 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
@@ -369,16 +369,20 @@ public class PaymentProcessor extends ProcessorBase {
return;
}
- final PaymentAttemptModelDao lastPaymentAttempt = attempts.get(attempts.size() - 1);
+ final PaymentAttemptModelDao lastPaymentAttempt = attempts.get(attempts.size() - 1);
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(lastPaymentAttempt.getAccountId(), callContext);
+ cancelScheduledPaymentTransaction(lastPaymentAttempt.getId(), internalCallContext);
+ }
+
+ public void cancelScheduledPaymentTransaction(final UUID lastPaymentAttemptId, final InternalCallContext internalCallContext) throws PaymentApiException {
try {
final NotificationQueue retryQueue = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, DefaultRetryService.QUEUE_NAME);
final List<NotificationEventWithMetadata<NotificationEvent>> notificationEventWithMetadatas =
retryQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationEventWithMetadatas) {
- if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttempt.getId())) {
+ if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttemptId)) {
retryQueue.removeNotification(notificationEvent.getRecordId());
break;
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
index 22e12ff..736fa84 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
@@ -46,6 +46,7 @@ import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner.
import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dao.PluginPropertySerializer;
import org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
@@ -198,6 +199,42 @@ public class PluginControlPaymentProcessor extends ProcessorBase {
callContext, internalCallContext);
}
+ public Payment notifyPendingPaymentOfStateChanged(final boolean isApiPayment, final Account account, final UUID paymentTransactionId, final boolean isSuccess, final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentTransactionId, internalCallContext);
+ final List<PaymentAttemptModelDao> attempts = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey(), internalCallContext);
+ final PaymentAttemptModelDao attempt = Iterables.find(attempts,
+ new Predicate<PaymentAttemptModelDao>() {
+ @Override
+ public boolean apply(final PaymentAttemptModelDao input) {
+ return input.getTransactionId().equals(paymentTransactionId);
+ }
+ });
+
+ final Iterable<PluginProperty> pluginProperties;
+ try {
+ pluginProperties = PluginPropertySerializer.deserialize(attempt.getPluginProperties());
+ } catch (final PluginPropertySerializerException e) {
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, String.format("Unable to deserialize payment attemptId='%s' properties", attempt.getId()));
+ }
+
+ return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
+ isSuccess,
+ paymentTransactionModelDao.getTransactionType(),
+ ControlOperation.NOTIFICATION_OF_STATE_CHANGE,
+ account,
+ attempt.getPaymentMethodId(),
+ paymentTransactionModelDao.getPaymentId(),
+ attempt.getPaymentExternalKey(),
+ paymentTransactionId,
+ paymentTransactionModelDao.getTransactionExternalKey(),
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ pluginProperties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
public Payment createChargeback(final boolean isApiPayment, final Account account, final UUID paymentId, final String transactionExternalKey, final BigDecimal amount, final Currency currency,
final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
index a18340b..5866bcc 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
@@ -163,23 +163,23 @@ public abstract class ProcessorBase {
public static class CallableWithAccountLock<ReturnType, ExceptionType extends Exception> implements Callable<PluginDispatcherReturnType<ReturnType>> {
private final GlobalLocker locker;
- private final String accountExternalKey;
+ private final UUID accountId;
private final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback;
private final PaymentConfig paymentConfig;
public CallableWithAccountLock(final GlobalLocker locker,
- final String accountExternalKey,
+ final UUID accountId,
final PaymentConfig paymentConfig,
final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback) {
this.locker = locker;
- this.accountExternalKey = accountExternalKey;
+ this.accountId = accountId;
this.callback = callback;
this.paymentConfig = paymentConfig;
}
@Override
public PluginDispatcherReturnType<ReturnType> call() throws ExceptionType, LockFailedException {
- return new WithAccountLock<ReturnType, ExceptionType>(paymentConfig).processAccountWithLock(locker, accountExternalKey, callback);
+ return new WithAccountLock<ReturnType, ExceptionType>(paymentConfig).processAccountWithLock(locker, accountId, callback);
}
}
@@ -191,11 +191,11 @@ public abstract class ProcessorBase {
this.paymentConfig = paymentConfig;
}
- public PluginDispatcherReturnType<ReturnType> processAccountWithLock(final GlobalLocker locker, final String accountExternalKey, final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback)
+ public PluginDispatcherReturnType<ReturnType> processAccountWithLock(final GlobalLocker locker, final UUID accountId, final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback)
throws ExceptionType, LockFailedException {
GlobalLock lock = null;
try {
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountExternalKey, paymentConfig.getMaxGlobalLockRetries());
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), paymentConfig.getMaxGlobalLockRetries());
return callback.doOperation();
} finally {
if (lock != null) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
index fd8eed7..9ba0ab2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
@@ -22,19 +22,22 @@ import java.util.List;
import org.killbill.automaton.OperationException;
import org.killbill.automaton.OperationResult;
import org.killbill.billing.control.plugin.api.PaymentApiType;
+import org.killbill.billing.control.plugin.api.PaymentControlContext;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback;
import org.killbill.billing.payment.core.sm.control.ControlPluginRunner.DefaultPaymentControlContext;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dispatcher.PluginDispatcher;
import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType;
-import org.killbill.billing.control.plugin.api.PaymentControlContext;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.commons.locker.GlobalLocker;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
//
// Used from AttemptCompletionTask to resume an incomplete payment that went through control API.
@@ -54,11 +57,11 @@ public class CompletionControlOperation extends OperationControlCallback {
@Override
public OperationResult doOperationCallback() throws OperationException {
-
final List<String> controlPluginNameList = paymentStateControlContext.getPaymentControlPluginNames();
final String controlPluginNames = JOINER.join(controlPluginNameList);
return dispatchWithAccountLockAndTimeout(controlPluginNames, new DispatcherCallback<PluginDispatcherReturnType<OperationResult>, OperationException>() {
+
@Override
public PluginDispatcherReturnType<OperationResult> doOperation() throws OperationException {
final PaymentTransactionModelDao transaction = paymentStateContext.getPaymentTransactionModelDao();
@@ -78,15 +81,34 @@ public class CompletionControlOperation extends OperationControlCallback {
transaction.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentStateContext.getCallContext());
+ try {
+ final Payment result = doCallSpecificOperationCallback();
+ ((PaymentStateControlContext) paymentStateContext).setResult(result);
+
+ final boolean success = transaction.getTransactionStatus() == TransactionStatus.SUCCESS || transaction.getTransactionStatus() == TransactionStatus.PENDING;
+ if (success) {
+ executePluginOnSuccessCalls(paymentStateControlContext.getPaymentControlPluginNames(), updatedPaymentControlContext);
+
+ // Remove scheduled retry, if any
+ paymentProcessor.cancelScheduledPaymentTransaction(paymentStateControlContext.getAttemptId(), paymentStateControlContext.getInternalCallContext());
- executePluginOnSuccessCalls(paymentStateControlContext.getPaymentControlPluginNames(), updatedPaymentControlContext);
- return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS);
+ return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS);
+ } else {
+ throw new OperationException(null, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext));
+ }
+ } catch (final PaymentApiException e) {
+ // Wrap PaymentApiException, and throw a new OperationException with an ABORTED/FAILURE state based on the retry result.
+ throw new OperationException(e, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext));
+ } catch (final RuntimeException e) {
+ // Attempts to set the retry date in context if needed.
+ throw new OperationException(e, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext));
+ }
}
});
}
@Override
protected Payment doCallSpecificOperationCallback() throws PaymentApiException {
- return null;
+ return paymentProcessor.getPayment(paymentStateContext.getPaymentId(), false, false, ImmutableList.<PluginProperty>of(), paymentStateContext.getCallContext(), paymentStateContext.getInternalCallContext());
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
index 1ee36fc..d212925 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
@@ -62,12 +62,15 @@ public class ControlPluginRunner {
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
+ final UUID paymentTransactionId,
final String paymentTransactionExternalKey,
final PaymentApiType paymentApiType,
final TransactionType transactionType,
final HPPType hppType,
final BigDecimal amount,
final Currency currency,
+ final BigDecimal processedAmount,
+ final Currency processedCurrency,
final boolean isApiPayment,
final List<String> paymentControlPluginNames,
final Iterable<PluginProperty> pluginProperties,
@@ -85,12 +88,15 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
+ paymentTransactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
hppType,
amount,
currency,
+ processedAmount,
+ processedCurrency,
isApiPayment,
callContext);
@@ -124,12 +130,15 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
+ paymentTransactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
hppType,
inputAmount,
inputCurrency,
+ processedAmount,
+ processedCurrency,
isApiPayment,
callContext);
}
@@ -143,7 +152,7 @@ public class ControlPluginRunner {
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
- final UUID transactionId,
+ final UUID paymentTransactionId,
final String paymentTransactionExternalKey,
final PaymentApiType paymentApiType,
final TransactionType transactionType,
@@ -162,7 +171,7 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
- transactionId,
+ paymentTransactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
@@ -201,12 +210,15 @@ public class ControlPluginRunner {
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
+ final UUID transactionId,
final String paymentTransactionExternalKey,
final PaymentApiType paymentApiType,
final TransactionType transactionType,
final HPPType hppType,
final BigDecimal amount,
final Currency currency,
+ final BigDecimal processedAmount,
+ final Currency processedCurrency,
final boolean isApiPayment,
final List<String> paymentControlPluginNames,
final Iterable<PluginProperty> pluginProperties,
@@ -217,12 +229,15 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
+ transactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
hppType,
amount,
currency,
+ processedAmount,
+ processedCurrency,
isApiPayment,
callContext);
@@ -273,15 +288,22 @@ public class ControlPluginRunner {
private final Currency processedCurrency;
private final boolean isApiPayment;
- public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, final String transactionExternalKey,
- final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType, final BigDecimal amount, final Currency currency,
- final boolean isApiPayment, final CallContext callContext) {
- this(account, paymentMethodId, attemptId, paymentId, paymentExternalKey, null, transactionExternalKey, paymentApiType, transactionType, hppType, amount, currency, null, null, isApiPayment, callContext);
- }
-
- public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, @Nullable final UUID transactionId, final String transactionExternalKey,
- final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType,
- final BigDecimal amount, final Currency currency, @Nullable final BigDecimal processedAmount, @Nullable final Currency processedCurrency, final boolean isApiPayment, final CallContext callContext) {
+ public DefaultPaymentControlContext(final Account account,
+ final UUID paymentMethodId,
+ final UUID attemptId,
+ @Nullable final UUID paymentId,
+ final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String transactionExternalKey,
+ final PaymentApiType paymentApiType,
+ final TransactionType transactionType,
+ final HPPType hppType,
+ final BigDecimal amount,
+ final Currency currency,
+ @Nullable final BigDecimal processedAmount,
+ @Nullable final Currency processedCurrency,
+ final boolean isApiPayment,
+ final CallContext callContext) {
super(callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(), callContext.getComments(), callContext.getUserToken(), callContext.getCreatedDate(), callContext.getUpdatedDate());
this.account = account;
this.paymentMethodId = paymentMethodId;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
index dcd1707..4b35883 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
@@ -89,7 +89,9 @@ public class DefaultControlInitiated implements LeavingStateCallback {
}
if (paymentTransactionModelDao != null) {
- stateContext.setPaymentTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey());
+ stateContext.setPaymentTransactionModelDao(paymentTransactionModelDao);
+ stateContext.setProcessedAmount(paymentTransactionModelDao.getProcessedAmount());
+ stateContext.setProcessedCurrency(paymentTransactionModelDao.getProcessedCurrency());
} else if (stateContext.getPaymentTransactionExternalKey() == null) {
stateContext.setPaymentTransactionExternalKey(UUIDs.randomUUID().toString());
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java
new file mode 100644
index 0000000..de2bfbf
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core.sm.control;
+
+import org.killbill.automaton.OperationResult;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.dispatcher.PluginDispatcher;
+import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.commons.locker.GlobalLocker;
+
+public class NotificationOfStateChangeControlOperation extends OperationControlCallback {
+
+ public NotificationOfStateChangeControlOperation(final GlobalLocker locker,
+ final PluginDispatcher<OperationResult> paymentPluginDispatcher,
+ final PaymentConfig paymentConfig,
+ final PaymentStateControlContext paymentStateContext,
+ final PaymentProcessor paymentProcessor,
+ final ControlPluginRunner controlPluginRunner) {
+ super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner);
+ }
+
+ @Override
+ protected Payment doCallSpecificOperationCallback() throws PaymentApiException {
+ return paymentProcessor.notifyPendingPaymentOfStateChanged(paymentStateControlContext.getAccount(),
+ paymentStateControlContext.getTransactionId(),
+ paymentStateControlContext.isSuccess(),
+ paymentStateControlContext.getCallContext(),
+ paymentStateControlContext.getInternalCallContext());
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
index 4f05018..ce634c0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
@@ -91,12 +91,15 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
paymentStateControlContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
+ paymentStateContext.getTransactionId(),
paymentStateContext.getPaymentTransactionExternalKey(),
PaymentApiType.PAYMENT_TRANSACTION,
paymentStateContext.getTransactionType(),
null,
paymentStateContext.getAmount(),
paymentStateContext.getCurrency(),
+ paymentStateControlContext.getProcessedAmount(),
+ paymentStateControlContext.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentStateContext.getCallContext());
@@ -173,12 +176,15 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
paymentStateControlContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
+ paymentStateContext.getTransactionId(),
paymentStateContext.getPaymentTransactionExternalKey(),
PaymentApiType.PAYMENT_TRANSACTION,
paymentStateContext.getTransactionType(),
null,
paymentControlContextArg.getAmount(),
paymentControlContextArg.getCurrency(),
+ paymentControlContextArg.getProcessedAmount(),
+ paymentControlContextArg.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentControlPluginNames,
paymentStateContext.getProperties(),
@@ -212,7 +218,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
adjustStateContextPluginProperties(paymentStateContext, result.getAdjustedPluginProperties());
}
- private OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentControlContext paymentControlContext) {
+ protected OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentControlContext paymentControlContext) {
final DateTime retryDate = executePluginOnFailureCalls(paymentStateControlContext.getPaymentControlPluginNames(), paymentControlContext);
if (retryDate != null) {
((PaymentStateControlContext) paymentStateContext).setRetryDate(retryDate);
@@ -227,12 +233,15 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
paymentStateControlContext.getAttemptId(),
paymentControlContext.getPaymentId(),
paymentControlContext.getPaymentExternalKey(),
+ paymentControlContext.getTransactionId(),
paymentControlContext.getTransactionExternalKey(),
PaymentApiType.PAYMENT_TRANSACTION,
paymentControlContext.getTransactionType(),
null,
paymentControlContext.getAmount(),
paymentControlContext.getCurrency(),
+ paymentControlContext.getProcessedAmount(),
+ paymentControlContext.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentControlPluginNames,
paymentStateContext.getProperties(),
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
index 2841907..920c660 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
@@ -40,16 +40,30 @@ import com.google.common.collect.Iterables;
public class PaymentStateControlContext extends PaymentStateContext {
+ private final Boolean isSuccess;
+
private DateTime retryDate;
private List<String> paymentControlPluginNames;
private Payment result;
+ private BigDecimal processedAmount;
+ private Currency processedCurrency;
- public PaymentStateControlContext(@Nullable final List<String> paymentControlPluginNames, final boolean isApiPayment, @Nullable final UUID paymentId, final String paymentExternalKey,
- @Nullable final String paymentTransactionExternalKey, final TransactionType transactionType,
- final Account account, @Nullable final UUID paymentMethodId, final BigDecimal amount, final Currency currency,
+ public PaymentStateControlContext(@Nullable final List<String> paymentControlPluginNames,
+ final boolean isApiPayment,
+ final Boolean isSuccess,
+ @Nullable final UUID paymentId,
+ final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ @Nullable final String paymentTransactionExternalKey,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ final BigDecimal amount,
+ final Currency currency,
final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext, final CallContext callContext) {
- super(isApiPayment, paymentId, null, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
+ super(isApiPayment, paymentId, transactionId, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
this.paymentControlPluginNames = paymentControlPluginNames;
+ this.isSuccess = isSuccess;
}
public DateTime getRetryDate() {
@@ -72,6 +86,26 @@ public class PaymentStateControlContext extends PaymentStateContext {
this.result = result;
}
+ public Boolean isSuccess() {
+ return isSuccess;
+ }
+
+ public BigDecimal getProcessedAmount() {
+ return processedAmount;
+ }
+
+ public void setProcessedAmount(final BigDecimal processedAmount) {
+ this.processedAmount = processedAmount;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+ public void setProcessedCurrency(final Currency processedCurrency) {
+ this.processedCurrency = processedCurrency;
+ }
+
public PaymentTransaction getCurrentTransaction() {
if (result == null || result.getTransactions() == null) {
return null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
index 39060ad..3594cfc 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
@@ -64,7 +64,7 @@ public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOpe
try {
final Callable<PluginDispatcherReturnType<OperationResult>> task = new CallableWithAccountLock<OperationResult, ExceptionType>(locker,
- account.getExternalKey(),
+ account.getId(),
paymentConfig,
callback);
final OperationResult operationResult = PaymentPluginDispatcher.dispatchWithExceptionHandling(account, pluginNames, task, paymentPluginDispatcher);
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
index 975c4b8..d4b2b39 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
@@ -54,6 +54,7 @@ import org.killbill.billing.payment.core.sm.control.CreditControlOperation;
import org.killbill.billing.payment.core.sm.control.DefaultControlCompleted;
import org.killbill.billing.payment.core.sm.control.DefaultControlInitiated;
import org.killbill.billing.payment.core.sm.control.NoopControlInitiated;
+import org.killbill.billing.payment.core.sm.control.NotificationOfStateChangeControlOperation;
import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
import org.killbill.billing.payment.core.sm.control.PurchaseControlOperation;
import org.killbill.billing.payment.core.sm.control.RefundControlOperation;
@@ -83,7 +84,8 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
CREDIT,
PURCHASE,
REFUND,
- VOID
+ VOID,
+ NOTIFICATION_OF_STATE_CHANGE
}
protected final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry;
@@ -107,25 +109,138 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
this.paymentConfig = paymentConfig;
}
- public Payment run(final boolean isApiPayment, final TransactionType transactionType, final ControlOperation controlOperation, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
+ public Payment run(final boolean isApiPayment,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ return run(paymentControlStateMachineHelper.getInitialState(),
+ isApiPayment,
+ null,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ null,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
+ public Payment run(final boolean isApiPayment,
+ final Boolean isSuccess,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ return run(paymentControlStateMachineHelper.getInitialState(),
+ isApiPayment,
+ isSuccess,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ transactionId,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
+ public Payment run(final State state,
+ final boolean isApiPayment,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- return run(paymentControlStateMachineHelper.getInitialState(), isApiPayment, transactionType, controlOperation, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
- amount, currency, properties, paymentControlPluginNames, callContext, internalCallContext);
+ return run(state,
+ isApiPayment,
+ null,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ null,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
}
- public Payment run(final State state, final boolean isApiPayment, final TransactionType transactionType, final ControlOperation controlOperation, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
+ public Payment run(final State state,
+ final boolean isApiPayment,
+ final Boolean isSuccess,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- final PaymentStateControlContext paymentStateContext = createContext(isApiPayment, transactionType, account, paymentMethodId,
- paymentId, paymentExternalKey,
+ final PaymentStateControlContext paymentStateContext = createContext(isApiPayment,
+ isSuccess,
+ transactionType,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ transactionId,
paymentTransactionExternalKey,
- amount, currency,
- properties, paymentControlPluginNames, callContext, internalCallContext);
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
try {
final OperationCallback callback = createOperationCallback(controlOperation, paymentStateContext);
final LeavingStateCallback leavingStateCallback = new DefaultControlInitiated(this, paymentStateContext, paymentDao, paymentControlStateMachineHelper.getInitialState(), paymentControlStateMachineHelper.getRetriedState(), transactionType);
@@ -159,23 +274,26 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
} catch (final MissingEntryException e) {
throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
} catch (final OperationException e) {
- if (e.getCause() == null) {
- throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
- } else if (e.getCause() instanceof PaymentApiException) {
+ if (e.getCause() instanceof PaymentApiException) {
throw (PaymentApiException) e.getCause();
- } else {
- throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
+ // If the control plugin tries to pass us back a PaymentApiException we throw it
+ } else if (e.getCause() instanceof PaymentControlApiException && e.getCause().getCause() instanceof PaymentApiException) {
+ throw (PaymentApiException) e.getCause().getCause();
+ } else if (e.getCause() != null || paymentStateContext.getResult() == null) {
+ throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
}
}
+ // If the result is set (and cause is null), that means we created a Payment but the associated transaction status is 'XXX_FAILURE',
+ // we don't throw, and return the failed Payment instead to be consistent with what happens when we don't go through control api.
return paymentStateContext.getResult();
}
@VisibleForTesting
- PaymentStateControlContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
+ PaymentStateControlContext createContext(final boolean isApiPayment, final Boolean isSuccess, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId, @Nullable final String paymentExternalKey,@Nullable final UUID transactionId, final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> properties,
final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- return new PaymentStateControlContext(paymentControlPluginNames, isApiPayment, paymentId, paymentExternalKey, paymentTransactionExternalKey, transactionType, account,
+ return new PaymentStateControlContext(paymentControlPluginNames, isApiPayment, isSuccess, paymentId, paymentExternalKey, transactionId, paymentTransactionExternalKey, transactionType, account,
paymentMethodId, amount, currency, properties, internalCallContext, callContext);
}
@@ -207,6 +325,9 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
case CHARGEBACK_REVERSAL:
callback = new ChargebackReversalControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner);
break;
+ case NOTIFICATION_OF_STATE_CHANGE:
+ callback = new NotificationOfStateChangeControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner);
+ break;
default:
throw new IllegalStateException("Unsupported control operation " + controlOperation);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
index 01b15f1..258c365 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
@@ -19,6 +19,7 @@ package org.killbill.billing.payment.dispatcher;
import java.util.Map;
import java.util.Random;
+import java.util.UUID;
import java.util.concurrent.Callable;
import org.apache.shiro.mgt.SecurityManager;
@@ -44,7 +45,12 @@ public class CallableWithRequestData<T> implements Callable<T> {
final Subject subject,
final Map<String, String> mdcContextMap,
final Callable<T> delegate) {
- this.requestData = requestData;
+ if (requestData == null) {
+ // To make locks re-entrant (for the Janitor), we need a request id
+ this.requestData = new RequestData(UUID.randomUUID().toString());
+ } else {
+ this.requestData = requestData;
+ }
this.random = random;
this.securityManager = securityManager;
this.subject = subject;
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
index 9b066d3..0a31eee 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -41,6 +41,8 @@ import org.killbill.billing.payment.core.PaymentGatewayProcessor;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentAttemptTask;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
import org.killbill.billing.payment.core.janitor.Janitor;
import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
@@ -127,6 +129,8 @@ public class PaymentModule extends KillBillModule {
}
protected void installProcessors(final PaymentConfig paymentConfig) {
+ bind(IncompletePaymentAttemptTask.class).asEagerSingleton();
+ bind(IncompletePaymentTransactionTask.class).asEagerSingleton();
bind(PaymentProcessor.class).asEagerSingleton();
bind(PluginControlPaymentProcessor.class).asEagerSingleton();
bind(PaymentGatewayProcessor.class).asEagerSingleton();
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 b8bc30f..81185d4 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
@@ -76,6 +76,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
@@ -176,7 +177,13 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
log.warn("processedCurrency='{}' of invoice paymentId='{}' doesn't match invoice currency='{}', assuming it is a full payment", paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
invoicePaymentAmount = paymentControlContext.getAmount();
}
- log.debug("Notifying invoice of successful paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
+
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentControlContext.getTransactionId(), internalContext);
+ // If it's not SUCCESS, it is PENDING
+ final boolean success = paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS;
+ log.debug("Notifying invoice of {} paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", success ? "successful" : "pending", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
+
+ // For PENDING payments, the attempt will be kept as unsuccessful and an InvoicePaymentErrorInternalEvent sent on the bus (e.g. for Overdue)
invoiceApi.recordPaymentAttemptCompletion(invoiceId,
invoicePaymentAmount,
paymentControlContext.getCurrency(),
@@ -184,7 +191,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
paymentControlContext.getPaymentId(),
paymentControlContext.getTransactionExternalKey(),
paymentControlContext.getCreatedDate(),
- true,
+ success,
internalContext);
}
break;
@@ -355,7 +362,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
// but onSuccessCall callback never gets called (leaving the place for a double payment if user retries the operation)
//
invoiceApi.recordPaymentAttemptInit(invoice.getId(),
- BigDecimal.ZERO,
+ MoreObjects.firstNonNull(paymentControlPluginContext.getAmount(), BigDecimal.ZERO),
paymentControlPluginContext.getCurrency(),
paymentControlPluginContext.getCurrency(),
// Likely to be null, but we don't care as we use the transactionExternalKey
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
index 15d8dac..4ad4a59 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
@@ -38,10 +38,7 @@ import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.payment.retry.DefaultFailureCallResult;
import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
-import org.killbill.commons.request.Request;
-import org.killbill.commons.request.RequestData;
import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -91,14 +88,6 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
},
testPaymentControlPluginApi);
-
- // Required for re-entrant locks to work
- Request.setPerThreadRequestData(new RequestData(UUID.randomUUID().toString()));
- }
-
- @AfterMethod(groups = "slow")
- public void tearDown() throws Exception {
- Request.resetPerThreadRequestData();
}
// Verify Payment control API can be used to change the paymentMethodId on the fly and this is reflected in the created Payment.
@@ -111,6 +100,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
final Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, BigDecimal.TEN, Currency.USD, UUID.randomUUID().toString(),
UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
Assert.assertEquals(payment.getPaymentMethodId(), newPaymentMethodId);
+
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.TEN,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -126,6 +122,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
@@ -133,6 +136,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -148,6 +158,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
@@ -161,6 +178,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -187,6 +211,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnSuccessForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
@@ -196,6 +227,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -222,6 +260,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnFailureForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
pendingPluginProperties, PAYMENT_OPTIONS, callContext);
@@ -236,6 +281,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -251,6 +303,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
@@ -273,6 +332,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
@@ -312,6 +378,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnSuccessForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
@@ -346,6 +419,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnFailureForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
Assert.fail();
@@ -381,6 +461,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -409,8 +496,14 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
- }
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+ }
@Test(groups = "slow")
public void testCreateAuthSuccessCapturePendingNoControlCompleteWithControl() throws PaymentApiException {
@@ -444,6 +537,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -483,6 +583,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -496,6 +603,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createCapture(account, payment.getId(), payment.getAuthAmount(), payment.getCurrency(), UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
@@ -522,6 +636,193 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 2);
Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+
+ verifyOnSuccessForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+ }
+
+ private void verifyPriorAndOnSuccess(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency());
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnSuccess(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallPaymentId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency());
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnSuccessForFollowOnTransaction(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency());
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyPriorAndOnFailure(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency(), processedCurrency);
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnFailure(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallPaymentId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency(), processedCurrency);
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnFailureForFollowOnTransaction(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency(), processedCurrency);
+
+ testPaymentControlPluginApi.resetActualValues();
}
public static class TestPaymentControlPluginApi implements PaymentControlPluginApi {
@@ -530,12 +831,112 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
private UUID newPaymentMethodId;
+ private UUID actualPriorCallPaymentId;
+ private String actualPriorCallPaymentExternalKey;
+ private UUID actualPriorCallTransactionId;
+ private String actualPriorCallTransactionExternalKey;
+ private BigDecimal actualPriorCallProcessedAmount;
+ private Currency actualPriorCallProcessedCurrency;
+
+ private UUID actualOnSuccessCallPaymentId;
+ private String actualOnSuccessCallPaymentExternalKey;
+ private UUID actualOnSuccessCallTransactionId;
+ private String actualOnSuccessCallTransactionExternalKey;
+ private BigDecimal actualOnSuccessCallProcessedAmount;
+ private Currency actualOnSuccessCallProcessedCurrency;
+
+ private UUID actualOnFailureCallPaymentId;
+ private String actualOnFailureCallPaymentExternalKey;
+ private UUID actualOnFailureCallTransactionId;
+ private String actualOnFailureCallTransactionExternalKey;
+ private BigDecimal actualOnFailureCallProcessedAmount;
+ private Currency actualOnFailureCallProcessedCurrency;
+
public void setNewPaymentMethodId(final UUID newPaymentMethodId) {
this.newPaymentMethodId = newPaymentMethodId;
}
+ public UUID getActualPriorCallPaymentId() {
+ return actualPriorCallPaymentId;
+ }
+
+ public String getActualPriorCallPaymentExternalKey() {
+ return actualPriorCallPaymentExternalKey;
+ }
+
+ public UUID getActualPriorCallTransactionId() {
+ return actualPriorCallTransactionId;
+ }
+
+ public String getActualPriorCallTransactionExternalKey() {
+ return actualPriorCallTransactionExternalKey;
+ }
+
+ public BigDecimal getActualPriorCallProcessedAmount() {
+ return actualPriorCallProcessedAmount;
+ }
+
+ public Currency getActualPriorCallProcessedCurrency() {
+ return actualPriorCallProcessedCurrency;
+ }
+
+ public UUID getActualOnSuccessCallPaymentId() {
+ return actualOnSuccessCallPaymentId;
+ }
+
+ public String getActualOnSuccessCallPaymentExternalKey() {
+ return actualOnSuccessCallPaymentExternalKey;
+ }
+
+ public UUID getActualOnSuccessCallTransactionId() {
+ return actualOnSuccessCallTransactionId;
+ }
+
+ public String getActualOnSuccessCallTransactionExternalKey() {
+ return actualOnSuccessCallTransactionExternalKey;
+ }
+
+ public BigDecimal getActualOnSuccessCallProcessedAmount() {
+ return actualOnSuccessCallProcessedAmount;
+ }
+
+ public Currency getActualOnSuccessCallProcessedCurrency() {
+ return actualOnSuccessCallProcessedCurrency;
+ }
+
+ public UUID getActualOnFailureCallPaymentId() {
+ return actualOnFailureCallPaymentId;
+ }
+
+ public String getActualOnFailureCallPaymentExternalKey() {
+ return actualOnFailureCallPaymentExternalKey;
+ }
+
+ public UUID getActualOnFailureCallTransactionId() {
+ return actualOnFailureCallTransactionId;
+ }
+
+ public String getActualOnFailureCallTransactionExternalKey() {
+ return actualOnFailureCallTransactionExternalKey;
+ }
+
+ public BigDecimal getActualOnFailureCallProcessedAmount() {
+ return actualOnFailureCallProcessedAmount;
+ }
+
+ public Currency getActualOnFailureCallProcessedCurrency() {
+ return actualOnFailureCallProcessedCurrency;
+ }
+
@Override
public PriorPaymentControlResult priorCall(final PaymentControlContext context, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+ actualPriorCallPaymentId = context.getPaymentId();
+ actualPriorCallPaymentExternalKey = context.getPaymentExternalKey();
+ actualPriorCallTransactionId = context.getTransactionId();
+ actualPriorCallTransactionExternalKey = context.getTransactionExternalKey();
+ actualPriorCallProcessedAmount = context.getProcessedAmount();
+ actualPriorCallProcessedCurrency = context.getProcessedCurrency();
+
return new PriorPaymentControlResult() {
@Override
public boolean isAborted() {
@@ -566,12 +967,49 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
@Override
public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext context, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+ actualOnSuccessCallPaymentId = context.getPaymentId();
+ actualOnSuccessCallPaymentExternalKey = context.getPaymentExternalKey();
+ actualOnSuccessCallTransactionId = context.getTransactionId();
+ actualOnSuccessCallTransactionExternalKey = context.getTransactionExternalKey();
+ actualOnSuccessCallProcessedAmount = context.getProcessedAmount();
+ actualOnSuccessCallProcessedCurrency = context.getProcessedCurrency();
+
return new DefaultOnSuccessPaymentControlResult();
}
@Override
public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext context, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+ actualOnFailureCallPaymentId = context.getPaymentId();
+ actualOnFailureCallPaymentExternalKey = context.getPaymentExternalKey();
+ actualOnFailureCallTransactionId = context.getTransactionId();
+ actualOnFailureCallTransactionExternalKey = context.getTransactionExternalKey();
+ actualOnFailureCallProcessedAmount = context.getProcessedAmount();
+ actualOnFailureCallProcessedCurrency = context.getProcessedCurrency();
+
return new DefaultFailureCallResult(null);
}
+
+ public void resetActualValues() {
+ actualPriorCallPaymentId = null;
+ actualPriorCallPaymentExternalKey = null;
+ actualPriorCallTransactionId = null;
+ actualPriorCallTransactionExternalKey = null;
+ actualPriorCallProcessedAmount = null;
+ actualPriorCallProcessedCurrency = null;
+
+ actualOnSuccessCallPaymentId = null;
+ actualOnSuccessCallPaymentExternalKey = null;
+ actualOnSuccessCallTransactionId = null;
+ actualOnSuccessCallTransactionExternalKey = null;
+ actualOnSuccessCallProcessedAmount = null;
+ actualOnSuccessCallProcessedCurrency = null;
+
+ actualOnFailureCallPaymentId = null;
+ actualOnFailureCallPaymentExternalKey = null;
+ actualOnFailureCallTransactionId = null;
+ actualOnFailureCallTransactionExternalKey = null;
+ actualOnFailureCallProcessedAmount = null;
+ actualOnFailureCallProcessedCurrency = null;
+ }
}
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
new file mode 100644
index 0000000..5a137ac
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core.janitor;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.util.globallocker.LockerType;
+import org.killbill.commons.locker.GlobalLock;
+import org.killbill.commons.locker.LockFailedException;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestIncompletePaymentTransactionTaskWithDB extends PaymentTestSuiteWithEmbeddedDB {
+
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
+ private Account account;
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ super.beforeClass();
+
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ mockPaymentProviderPlugin.clear();
+ account = testHelper.createTestAccount(UUID.randomUUID().toString(), true);
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/675")
+ public void testHandleLockExceptions() throws PaymentApiException {
+ final Payment payment = paymentApi.createPurchase(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING.toString(), false)),
+ callContext);
+
+ final UUID transactionId = payment.getTransactions().get(0).getId();
+ final JanitorNotificationKey notificationKey = new JanitorNotificationKey(transactionId, incompletePaymentTransactionTask.getClass().toString(), 1);
+ final UUID userToken = UUID.randomUUID();
+
+ Assert.assertTrue(incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()).isEmpty());
+
+ GlobalLock lock = null;
+ try {
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getId().toString(), paymentConfig.getMaxGlobalLockRetries());
+
+ incompletePaymentTransactionTask.processNotification(notificationKey, userToken, internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+
+ final List<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+ Assert.assertFalse(futureNotifications.isEmpty());
+ Assert.assertEquals(futureNotifications.get(0).getUserToken(), userToken);
+ Assert.assertEquals(futureNotifications.get(0).getEvent().getClass(), JanitorNotificationKey.class);
+ final JanitorNotificationKey event = (JanitorNotificationKey) futureNotifications.get(0).getEvent();
+ Assert.assertEquals(event.getUuidKey(), transactionId);
+ Assert.assertEquals((int) event.getAttemptNumber(), 2);
+
+ // Based on config "15s,1m,3m,1h,1d,1d,1d,1d,1d"
+ Assert.assertTrue(futureNotifications.get(0).getEffectiveDate().compareTo(clock.getUTCNow().plusMinutes(1).plusSeconds(1)) < 0);
+ } catch (final LockFailedException e) {
+ Assert.fail();
+ } finally {
+ if (lock != null) {
+ lock.release();
+ }
+ }
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java
index e48f3b4..80cb640 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java
@@ -43,6 +43,7 @@ public class TestControlPluginRunner extends PaymentTestSuiteNoDB {
final UUID paymentMethodId = UUIDs.randomUUID();
final UUID paymentId = UUIDs.randomUUID();
final String paymentExternalKey = UUIDs.randomUUID().toString();
+ final UUID paymentTransactionId = UUIDs.randomUUID();
final String paymentTransactionExternalKey = UUIDs.randomUUID().toString();
final BigDecimal amount = BigDecimal.ONE;
final Currency currency = Currency.USD;
@@ -55,12 +56,15 @@ public class TestControlPluginRunner extends PaymentTestSuiteNoDB {
null,
paymentId,
paymentExternalKey,
+ paymentTransactionId,
paymentTransactionExternalKey,
PaymentApiType.PAYMENT_TRANSACTION,
TransactionType.AUTHORIZE,
null,
amount,
currency,
+ null,
+ null,
true,
paymentControlPluginNames,
pluginProperties,
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
index 386ed00..24ae2a7 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
@@ -74,13 +74,13 @@ public class MockRetryablePaymentAutomatonRunner extends PluginControlPaymentAut
}
@Override
- PaymentStateControlContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
+ PaymentStateControlContext createContext(final boolean isApiPayment, final Boolean isSuccess, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, @Nullable final UUID transactionId, final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount, @Nullable final Currency currency,
final Iterable<PluginProperty> properties,
final List<String> pluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
if (context == null) {
- return super.createContext(isApiPayment, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
+ return super.createContext(isApiPayment, isSuccess, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, transactionId, paymentTransactionExternalKey,
amount, currency, properties, pluginNames, callContext, internalCallContext);
} else {
return context;
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
index e34c632..405d5be 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
@@ -65,7 +65,7 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
@BeforeMethod(groups = "fast")
public void beforeMethod() throws Exception {
super.beforeMethod();
- Mockito.when(account.getExternalKey()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
}
@Test(groups = "fast")
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
index 4f3b429..752f40c 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
@@ -177,7 +177,9 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
new PaymentStateControlContext(ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME),
true,
null,
+ null,
paymentExternalKey,
+ null,
paymentTransactionExternalKey,
TransactionType.AUTHORIZE,
account,
@@ -720,7 +722,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
GlobalLock lock = null;
try {
// Grab lock so that operation later will fail...
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), 1);
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getId().toString(), 1);
mockRetryProviderPlugin
.setAborted(false)
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
index 73cc4ea..061bbc5 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
@@ -30,16 +30,20 @@ import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
+import org.killbill.billing.payment.core.janitor.Janitor;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.payment.glue.TestPaymentModuleWithEmbeddedDB;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.payment.retry.DefaultRetryService;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.bus.api.PersistentBus;
+import org.killbill.commons.locker.GlobalLocker;
import org.killbill.commons.profiling.Profiling;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
@@ -61,6 +65,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
@Inject
protected PaymentProcessor paymentProcessor;
@Inject
+ protected DefaultRetryService retryService;
+ @Inject
protected InvoiceInternalApi invoiceApi;
@Inject
protected OSGIServiceRegistration<PaymentPluginApi> registry;
@@ -88,6 +94,12 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
protected NonEntityDao nonEntityDao;
@Inject
protected StateMachineConfigCache stateMachineConfigCache;
+ @Inject
+ protected Janitor janitor;
+ @Inject
+ protected IncompletePaymentTransactionTask incompletePaymentTransactionTask;
+ @Inject
+ protected GlobalLocker locker;
@Override
protected KillbillConfigSource getConfigSource() {
@@ -113,10 +125,14 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
eventBus.start();
Profiling.resetPerThreadProfilingData();
clock.resetDeltaFromReality();
+
+ janitor.initialize();
+ janitor.start();
}
@AfterMethod(groups = "slow")
public void afterMethod() throws Exception {
+ janitor.stop();
eventBus.stop();
paymentExecutors.stop();
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index 1f81aeb..b86f476 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -73,6 +73,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
private final AtomicBoolean makeNextPaymentFailWithCancellation = new AtomicBoolean(false);
private final AtomicBoolean makeNextPaymentFailWithException = new AtomicBoolean(false);
private final AtomicBoolean makeAllPaymentsFailWithError = new AtomicBoolean(false);
+ private final AtomicBoolean makeNextPaymentPending = new AtomicBoolean(false);
private final AtomicInteger makePluginWaitSomeMilliseconds = new AtomicInteger(0);
private final AtomicReference<BigDecimal> overrideNextProcessedAmount = new AtomicReference<BigDecimal>();
private final AtomicReference<Currency> overrideNextProcessedCurrency = new AtomicReference<Currency>();
@@ -200,6 +201,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeAllPaymentsFailWithError.set(false);
makeNextPaymentFailWithError.set(false);
makeNextPaymentFailWithCancellation.set(false);
+ makeNextPaymentPending.set(false);
makePluginWaitSomeMilliseconds.set(0);
overrideNextProcessedAmount.set(null);
paymentMethods.clear();
@@ -212,6 +214,10 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeNextPaymentFailWithError.set(true);
}
+ public void makeNextPaymentPending() {
+ makeNextPaymentPending.set(true);
+ }
+
public void makeNextPaymentFailWithCancellation() {
makeNextPaymentFailWithCancellation.set(true);
}
@@ -432,6 +438,8 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
status = PaymentPluginStatus.ERROR;
} else if (makeNextPaymentFailWithCancellation.getAndSet(false)) {
status = PaymentPluginStatus.CANCELED;
+ } else if (makeNextPaymentPending.getAndSet(false)) {
+ status = PaymentPluginStatus.PENDING;
} else {
status = PaymentPluginStatus.PROCESSED;
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index c248d23..b8d044b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -101,8 +101,6 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
@Inject
protected NotificationQueueService notificationQueueService;
@Inject
- private Janitor janitor;
- @Inject
private PaymentBusEventHandler handler;
private MockPaymentProviderPlugin mockPaymentProviderPlugin;
@@ -123,15 +121,13 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
}
- @AfterClass(groups = "slow")
- protected void afterClass() throws Exception {
- }
-
@BeforeMethod(groups = "slow")
public void beforeMethod() throws Exception {
super.beforeMethod();
- janitor.initialize();
- janitor.start();
+
+ retryService.initialize();
+ retryService.start();
+
eventBus.register(handler);
testListener.reset();
eventBus.register(testListener);
@@ -143,9 +139,10 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
@AfterMethod(groups = "slow")
public void afterMethod() throws Exception {
+ retryService.stop();
+
testListener.assertListenerStatus();
- janitor.stop();
eventBus.unregister(handler);
eventBus.unregister(testListener);
super.afterMethod();
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
index 776621b..b2ac2b0 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
@@ -36,6 +36,16 @@ public interface InvoiceConfig extends KillbillConfig {
@Description("Maximum target date to consider when generating an invoice")
int getNumberOfMonthsInFuture(@Param("dummy") final InternalTenantContext tenantContext);
+ @Config("org.killbill.invoice.sanitySafetyBoundEnabled")
+ @Default("true")
+ @Description("Whether internal sanity checks to prevent mis- and double-billing are enabled")
+ boolean isSanitySafetyBoundEnabled();
+
+ @Config("org.killbill.invoice.sanitySafetyBoundEnabled")
+ @Default("true")
+ @Description("Whether internal sanity checks to prevent mis- and double-billing are enabled")
+ boolean isSanitySafetyBoundEnabled(@Param("dummy") final InternalTenantContext tenantContext);
+
@Config("org.killbill.invoice.maxDailyNumberOfItemsSafetyBound")
@Default("15")
@Description("Maximum daily number of invoice items to generate for a subscription id")