killbill-memoizeit

Changes

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

catalog/pom.xml 2(+1 -1)

currency/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

jaxrs/pom.xml 2(+1 -1)

junction/pom.xml 2(+1 -1)

NEWS 9(+5 -4)

overdue/pom.xml 2(+1 -1)

payment/pom.xml 2(+1 -1)

pom.xml 4(+2 -2)

profiles/pom.xml 2(+1 -1)

tenant/pom.xml 2(+1 -1)

usage/pom.xml 2(+1 -1)

util/pom.xml 2(+1 -1)

Details

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 606fabe..6ca1665 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -139,6 +139,7 @@ jobs:
             set +e
 
             mvn clean install -DskipTests=true
+            mkdir -p /tmp/test-results
             nohup ./bin/start-server -s > /tmp/test-results/killbill.log 2>&1 &
 
             mkdir -p /home/killbill/killbill-integration-tests
@@ -165,7 +166,6 @@ jobs:
             done
 
             set -e
-            mkdir -p /tmp/test-results
             bundle exec rake test:core | tee /tmp/test-results/test.txt 2>&1
       - store_test_results:
           path: /tmp/test-results

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index 8845d3d..3381927 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index 8d64cd1..c7a0a58 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-internal-api</artifactId>

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 19915ff..13276df 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>
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 91ee466..4b040d1 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
@@ -102,6 +102,7 @@ import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
 import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
 import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
 import org.killbill.billing.subscription.api.SubscriptionBaseService;
 import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
 import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
@@ -222,6 +223,8 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
     @Inject
     protected SubscriptionApi subscriptionApi;
 
+    @Inject
+    protected SubscriptionBaseInternalApi subscriptionBaseInternalApiApi;
 
     @Named(BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME)
     @Inject
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
index 17a7e68..f770663 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
@@ -18,6 +18,7 @@
 package org.killbill.billing.beatrix.integration;
 
 import java.math.BigDecimal;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.Callable;
@@ -29,16 +30,21 @@ import org.awaitility.Awaitility;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountData;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import org.killbill.billing.invoice.api.DefaultInvoiceService;
 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.model.ExternalChargeInvoiceItem;
@@ -61,6 +67,8 @@ import org.killbill.notificationq.api.NotificationQueueService;
 import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
 import org.killbill.queue.retry.RetryNotificationEvent;
 import org.killbill.queue.retry.RetryableService;
+import org.mockito.Mockito;
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -108,6 +116,12 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
     @BeforeMethod(groups = "slow")
     public void setUp() throws Exception {
         testInvoicePluginApi.additionalInvoiceItem = null;
+        testInvoicePluginApi.shouldAddTaxItem = true;
+        testInvoicePluginApi.isAborted = false;
+        testInvoicePluginApi.shouldUpdateDescription = false;
+        testInvoicePluginApi.rescheduleDate = null;
+        testInvoicePluginApi.wasRescheduled = false;
+        testInvoicePluginApi.invocationCount = 0;
     }
 
     @Test(groups = "slow")
@@ -141,6 +155,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
                                                                                    pluginLinkedItemId,
                                                                                    null);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 0);
+
         // Create original subscription (Trial PHASE) -> $0 invoice but plugin added one item
         final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
         invoiceChecker.checkInvoice(account.getId(), 1, callContext,
@@ -148,6 +164,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.EXTERNAL_CHARGE, BigDecimal.TEN));
         subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 1);
+
         final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext);
         assertEquals(invoices.size(), 1);
         final List<InvoiceItem> invoiceItems = invoices.get(0).getInvoiceItems();
@@ -174,6 +192,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
         accountChecker.checkAccount(account.getId(), accountData, callContext);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 0);
+
         // Create original subscription (Trial PHASE) -> $0 invoice.
         final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
         invoiceChecker.checkInvoice(account.getId(), 1, callContext,
@@ -181,6 +201,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.TAX, new BigDecimal("1.0")));
         subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 1);
+
         // Move to Evergreen PHASE
         busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
         clock.addDays(30);
@@ -189,6 +211,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")),
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), null, InvoiceItemType.TAX, new BigDecimal("1.0")));
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+
         final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext);
         assertEquals(invoices.size(), 2);
         final InvoiceItem recurringItem = Iterables.find(invoices.get(1).getInvoiceItems(),
@@ -231,6 +255,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")),
                                     new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 6, 1), InvoiceItemType.CBA_ADJ, BigDecimal.TEN.negate()));
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 3);
+
         final List<Invoice> refreshedInvoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext);
         final List<InvoiceItem> invoiceItems = refreshedInvoices.get(1).getInvoiceItems();
         final InvoiceItem invoiceItemAdjustment = Iterables.tryFind(invoiceItems, new Predicate<InvoiceItem>() {
@@ -247,6 +273,256 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
     }
 
     @Test(groups = "slow")
+    public void testAborted() throws Exception {
+        testInvoicePluginApi.shouldAddTaxItem = false;
+
+        // We take april as it has 30 days (easier to play with BCD)
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(new LocalDate(2012, 4, 1));
+
+        final AccountData accountData = getAccountData(1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 0);
+
+        // Create original subscription (Trial PHASE) -> $0 invoice
+        final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 1);
+
+        // Abort invoice runs
+        testInvoicePluginApi.isAborted = true;
+
+        // Move to Evergreen PHASE
+        busHandler.pushExpectedEvents(NextEvent.PHASE);
+        clock.addDays(30);
+        assertListenerStatus();
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 1);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+
+        // No notification, so by default, the account will not be re-invoiced
+        clock.addMonths(1);
+        assertListenerStatus();
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 1);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+
+        // No notification, so by default, the account will not be re-invoiced
+        clock.addMonths(1);
+        assertListenerStatus();
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 1);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+
+        // Re-enable invoicing
+        testInvoicePluginApi.isAborted = false;
+
+        // Trigger a manual invoice run
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), null, callContext);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 7, 1), new LocalDate(2012, 8, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 3);
+
+        // Invoicing resumes
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 8, 1), new LocalDate(2012, 9, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 4);
+    }
+
+    @Test(groups = "slow")
+    public void testUpdateDescription() throws Exception {
+        testInvoicePluginApi.shouldAddTaxItem = false;
+        testInvoicePluginApi.shouldUpdateDescription = true;
+
+        // We take april as it has 30 days (easier to play with BCD)
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(new LocalDate(2012, 4, 1));
+
+        final AccountData accountData = getAccountData(1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        // Create original subscription (Trial PHASE) -> $0 invoice but plugin added one item
+        final Entitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        final Invoice firstInvoice = invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+                                                                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+        checkInvoiceDescriptions(firstInvoice);
+
+        // Move to Evergreen PHASE
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+        final Invoice secondInvoice = invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+                                                                  new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+        checkInvoiceDescriptions(secondInvoice);
+
+        // Cancel START_OF_TERM to make sure odd items like CBA are updated too
+        busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.INVOICE);
+        bpSubscription.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE,
+                                                                        BillingActionPolicy.START_OF_TERM,
+                                                                        ImmutableList.<PluginProperty>of(),
+                                                                        callContext);
+        assertListenerStatus();
+        final Invoice thirdInvoice = invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+                                                                         new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("29.95").negate()),
+                                                                         new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("29.95")));
+        checkInvoiceDescriptions(thirdInvoice);
+    }
+
+    private void checkInvoiceDescriptions(final Invoice invoice) {
+        for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+            assertEquals(invoiceItem.getDescription(), String.format("[plugin] %s", invoiceItem.getId()));
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testRescheduledViaNotification() throws Exception {
+        testInvoicePluginApi.shouldAddTaxItem = false;
+
+        // We take april as it has 30 days (easier to play with BCD)
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(new LocalDate(2012, 4, 1));
+
+        final AccountData accountData = getAccountData(1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 0);
+
+        // Create original subscription (Trial PHASE) -> $0 invoice
+        final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 1);
+        Assert.assertFalse(testInvoicePluginApi.wasRescheduled);
+
+        // Reschedule invoice generation
+        final DateTime utcNow = clock.getUTCNow();
+        testInvoicePluginApi.rescheduleDate = new DateTime(2012, 5, 2, utcNow.getHourOfDay(), utcNow.getMinuteOfHour(), utcNow.getSecondOfMinute(), DateTimeZone.UTC);
+
+        // Move to Evergreen PHASE
+        busHandler.pushExpectedEvents(NextEvent.PHASE);
+        clock.addDays(30);
+        assertListenerStatus();
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 1);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+        Assert.assertFalse(testInvoicePluginApi.wasRescheduled);
+
+        // PHASE invoice has been rescheduled, reset rescheduleDate
+        testInvoicePluginApi.rescheduleDate = null;
+
+        // Move one day
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addDays(1);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 3);
+        Assert.assertTrue(testInvoicePluginApi.wasRescheduled);
+
+        // Invoicing resumes as expected
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 4);
+        Assert.assertFalse(testInvoicePluginApi.wasRescheduled);
+    }
+
+    @Test(groups = "slow")
+    public void testRescheduledViaAPI() throws Exception {
+        testInvoicePluginApi.shouldAddTaxItem = false;
+
+        // We take april as it has 30 days (easier to play with BCD)
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(new LocalDate(2012, 4, 1));
+
+        final AccountData accountData = getAccountData(1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 0);
+
+        // Create original subscription (Trial PHASE) -> $0 invoice
+        final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 1);
+
+        // Reschedule invoice generation at the time of the PHASE event
+        testInvoicePluginApi.rescheduleDate = new DateTime(clock.getUTCNow()).plusDays(30);
+
+        try {
+            invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), null, callContext);
+            Assert.fail();
+        } catch (final InvoiceApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
+        }
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+        Assert.assertFalse(testInvoicePluginApi.wasRescheduled);
+
+        // Let the next invoice go through
+        testInvoicePluginApi.rescheduleDate = null;
+
+        // Move to Evergreen PHASE: two invoice runs will be triggers, one by SubscriptionNotificationKey (PHASE event) and one by NextBillingDateNotificationKey (reschedule)
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.NULL_INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 4);
+        // Cannot check wasRescheduled flag, as it would be true only for one of the runs
+
+        // Reschedule next invoice one month in the future
+        testInvoicePluginApi.rescheduleDate = clock.getUTCNow().plusMonths(1);
+        try {
+            invoiceUserApi.triggerInvoiceGeneration(account.getId(), clock.getUTCToday(), null, callContext);
+            Assert.fail();
+        } catch (final InvoiceApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
+        }
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 5);
+        Assert.assertFalse(testInvoicePluginApi.wasRescheduled);
+
+        // Let the next invoice go through
+        testInvoicePluginApi.rescheduleDate = null;
+
+        // Move one month ahead: no NULL_INVOICE this time: since there is already a notification for that date, the reschedule is a no-op (and we keep the isRescheduled flag to false)
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 6);
+        Assert.assertFalse(testInvoicePluginApi.wasRescheduled);
+    }
+
+    @Test(groups = "slow")
     public void testWithRetries() throws Exception {
         // We take april as it has 30 days (easier to play with BCD)
         // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
@@ -256,6 +532,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
         accountChecker.checkAccount(account.getId(), accountData, callContext);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 0);
+
         // Make invoice plugin fail
         testInvoicePluginApi.shouldThrowException = true;
 
@@ -265,6 +543,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         // Invoice failed to generate
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false,  callContext).size(), 0);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 1);
+
         // Verify bus event has moved to the retry service (can't easily check the timestamp unfortunately)
         // No future notification at this point (FIXED item, the PHASE event is the trigger for the next one)
         checkRetryBusEvents(1, 0);
@@ -273,6 +553,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         clock.addDeltaFromReality(5 * 60 * 1000);
         checkRetryBusEvents(2, 0);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 2);
+
         // Fix invoice plugin
         testInvoicePluginApi.shouldThrowException = false;
 
@@ -282,6 +564,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         // No notification in the main queue at this point (the PHASE event is the trigger for the next one)
         checkNotificationsNoRetry(0);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 3);
+
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false,  callContext).size(), 1);
         invoiceChecker.checkInvoice(account.getId(),
                                     1,
@@ -294,6 +578,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         assertListenerStatus();
         checkNotificationsNoRetry(1);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 4);
+
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false,  callContext).size(), 2);
         invoiceChecker.checkInvoice(account.getId(),
                                     2,
@@ -307,6 +593,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         clock.addMonths(1);
         assertListenerStatus();
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 5);
+
         // Invoice failed to generate
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false,  callContext).size(), 2);
 
@@ -318,6 +606,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         // Verify there are no notification duplicates
         checkRetryNotifications("2012-06-01T00:15:00", 1);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 6);
+
         // Fix invoice plugin
         testInvoicePluginApi.shouldThrowException = false;
 
@@ -326,6 +616,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         assertListenerStatus();
         checkNotificationsNoRetry(1);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 7);
+
         // Invoice was generated
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false,  callContext).size(), 3);
         invoiceChecker.checkInvoice(account.getId(),
@@ -340,6 +632,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         clock.setTime(new DateTime("2012-07-01T00:00:00"));
         assertListenerStatus();
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 8);
+
         // Invoice failed to generate
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false,  callContext).size(), 3);
 
@@ -353,6 +647,8 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         assertListenerStatus();
         checkNotificationsNoRetry(1);
 
+        Assert.assertEquals(testInvoicePluginApi.invocationCount, 9);
+
         assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 4);
     }
 
@@ -404,10 +700,29 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
 
         boolean shouldThrowException = false;
         InvoiceItem additionalInvoiceItem;
+        boolean shouldAddTaxItem = true;
+        boolean isAborted = false;
+        boolean shouldUpdateDescription = false;
+        DateTime rescheduleDate;
+        boolean wasRescheduled = false;
+        int invocationCount = 0;
 
         @Override
         public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
-            return null;
+            invocationCount++;
+            wasRescheduled = invoiceContext.isRescheduled();
+            return new PriorInvoiceResult() {
+
+                @Override
+                public boolean isAborted() {
+                    return isAborted;
+                }
+
+                @Override
+                public DateTime getRescheduleDate() {
+                    return rescheduleDate;
+                }
+            };
         }
 
         @Override
@@ -416,8 +731,18 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
                 throw new InvoicePluginApiRetryException();
             } else if (additionalInvoiceItem != null) {
                 return ImmutableList.<InvoiceItem>of(additionalInvoiceItem);
-            } else {
+            } else if (shouldAddTaxItem) {
                 return ImmutableList.<InvoiceItem>of(createTaxInvoiceItem(invoice));
+            } else if (shouldUpdateDescription) {
+                final List<InvoiceItem> updatedInvoiceItems = new LinkedList<InvoiceItem>();
+                for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+                    final InvoiceItem updatedInvoiceItem = Mockito.spy(invoiceItem);
+                    Mockito.when(updatedInvoiceItem.getDescription()).thenReturn(String.format("[plugin] %s", invoiceItem.getId()));
+                    updatedInvoiceItems.add(updatedInvoiceItem);
+                }
+                return updatedInvoiceItems;
+            } else {
+                return ImmutableList.<InvoiceItem>of();
             }
         }
 
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
index d0cfcee..d9b37e5 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
@@ -49,6 +49,7 @@ import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.callcontext.CallContext;
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -297,7 +298,7 @@ public class TestWithTaxItems extends TestIntegrationBase {
         }
 
         @Override
-        public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+        public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> pluginProperties) {
             return null;
         }
 
@@ -307,17 +308,34 @@ public class TestWithTaxItems extends TestIntegrationBase {
             for (final TaxInvoiceItem item : taxItems) {
                 result.add(new TaxInvoiceItem(item.getId(), invoice.getId(), invoice.getAccountId(), item.getBundleId(), "Tax Item", item.getStartDate(), item.getAmount(), invoice.getCurrency()));
             }
-            taxItems.clear();
             return result;
         }
 
         @Override
-        public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+        public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> pluginProperties) {
+            Assert.assertFalse(invoiceContext.isRescheduled());
+
+            final Invoice invoice = invoiceContext.getInvoice();
+            Assert.assertNotNull(invoice);
+            for (final TaxInvoiceItem taxInvoiceItem : taxItems) {
+                final InvoiceItem createdTaxInvoiceItem = Iterables.<InvoiceItem>find(invoice.getInvoiceItems(),
+                                                                                      new Predicate<InvoiceItem>() {
+                                                                                          @Override
+                                                                                          public boolean apply(final InvoiceItem invoiceItem) {
+                                                                                              return invoiceItem.getId().compareTo(taxInvoiceItem.getId()) == 0;
+                                                                                          }
+                                                                                      });
+                Assert.assertEquals(createdTaxInvoiceItem.getAccountId(), taxInvoiceItem.getAccountId());
+            }
+
+            reset();
+
             return null;
         }
 
         @Override
-        public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+        public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> pluginProperties) {
+            Assert.fail();
             return null;
         }
 
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
index 607a2b6..d3ffe9c 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/usage/TestConsumableInArrear.java
@@ -194,6 +194,59 @@ public class TestConsumableInArrear extends TestIntegrationBase {
         assertListenerStatus();
     }
 
+    @Test(groups = "slow")
+    public void testWithNoRecurringPlan() throws Exception {
+        // We take april as it has 30 days (easier to play with BCD)
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(new LocalDate(2012, 4, 1));
+
+        final AccountData accountData = getAccountData(1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+        accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+        // Create subscription
+        final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Trebuchet", ProductCategory.BASE, BillingPeriod.NO_BILLING_PERIOD, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+        // Check bundle after BP got created otherwise we get an error from auditApi.
+        subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+
+        Assert.assertNull(bpSubscription.getSubscriptionBase().getChargedThroughDate());
+
+        // Record usage for first month
+        setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 4, 5), 85L, callContext);
+        setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 4, 15), 150L, callContext);
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 1, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.USAGE, new BigDecimal("1000")));
+
+        final DateTime firstExpectedCTD = account.getReferenceTime().withMonthOfYear(5).withDayOfMonth(1);
+        Assert.assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(firstExpectedCTD), 0);
+
+        // No usage in second month
+        busHandler.pushExpectedEvents(NextEvent.NULL_INVOICE);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        Assert.assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(firstExpectedCTD), 0);
+
+        // Record usage for third month (verify invoicing resumes)
+        setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 6, 5), 25L, callContext);
+        setUsage(bpSubscription.getId(), "stones", new LocalDate(2012, 6, 15), 50L, callContext);
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 2, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.USAGE, new BigDecimal("100")));
+
+        final DateTime secondExpectedCTD = account.getReferenceTime().withMonthOfYear(7).withDayOfMonth(1);
+        Assert.assertEquals(subscriptionBaseInternalApiApi.getSubscriptionFromId(bpSubscription.getId(), internalCallContext).getChargedThroughDate().compareTo(secondExpectedCTD), 0);
+    }
+
     private void setUsage(final UUID subscriptionId, final String unitType, final LocalDate startDate, final Long amount, final CallContext context) throws UsageApiException {
         final List<UsageRecord> usageRecords = new ArrayList<UsageRecord>();
         usageRecords.add(new UsageRecord(startDate, amount));
diff --git a/bin/gen_updater.rb b/bin/gen_updater.rb
index 14f75c9..9ad2429 100644
--- a/bin/gen_updater.rb
+++ b/bin/gen_updater.rb
@@ -21,10 +21,10 @@ metadata.each do |entry|
   train = parsed.last.to_i
   if train % 2 == 1
     current_dev_train = train if current_dev_train.to_i < train
-    current_dev_version = version if current_dev_version.nil? || (current_dev_version < version)
+    current_dev_version = version if current_dev_version.nil? || (Gem::Version.new(current_dev_version) < Gem::Version.new(version))
   else
     current_stable_train = train if current_stable_train.to_i < train
-    current_stable_version = version if current_stable_version.nil? || (current_stable_version < version)
+    current_stable_version = version if current_stable_version.nil? || (Gem::Version.new(current_stable_version) < Gem::Version.new(version))
   end
 
   releases << {

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index 775cd7c..f6e5a11 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
index 73dfb28..32018fd 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
@@ -278,6 +278,7 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
         return versionForDate(requestedDate).getCurrentSupportedCurrencies();
     }
 
+    @Override
     public Unit[] getUnits(final DateTime requestedDate) throws CatalogApiException {
         return versionForDate(requestedDate).getCurrentUnits();
     }
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java
index 5c788a0..1744d0d 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java
@@ -41,6 +41,7 @@ import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.Unit;
 import org.killbill.billing.catalog.rules.DefaultCaseCancelPolicy;
 import org.killbill.billing.catalog.rules.DefaultCaseChangePlanAlignment;
 import org.killbill.billing.catalog.rules.DefaultCaseChangePlanPolicy;
@@ -110,6 +111,11 @@ public class MockCatalog extends StandaloneCatalog implements Catalog {
     }
 
     @Override
+    public Unit[] getUnits(final DateTime requestedDate) throws CatalogApiException {
+        return getCurrentUnits();
+    }
+
+    @Override
     public Collection<Product> getProducts(final DateTime requestedDate) throws CatalogApiException {
         return getCurrentProducts();
     }
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index e4969b1..77a6048 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -41,6 +41,7 @@
 
     <units>
         <unit name="bullets"/>
+        <unit name="stones"/>
     </units>
 
     <products>
@@ -81,6 +82,9 @@
                 <addonProduct>Bullets</addonProduct>
             </available>
         </product>
+        <product name="Trebuchet">
+            <category>BASE</category>
+        </product>
         <product name="Cleaning">
             <category>ADD_ON</category>
         </product>
@@ -1354,6 +1358,49 @@
             <plansAllowedInBundle>-1</plansAllowedInBundle>
             <!-- arbitrary number of these (multipack) -->
         </plan>
+        <plan name="trebuchet-usage-in-arrear" prettyName="Trebuchet Monthly Plan">
+            <product>Trebuchet</product>
+            <finalPhase type="EVERGREEN">
+                <duration>
+                    <unit>UNLIMITED</unit>
+                </duration>
+                <usages>
+                    <usage name="trebuchet-in-arrear-usage" billingMode="IN_ARREAR" usageType="CAPACITY">
+                        <billingPeriod>MONTHLY</billingPeriod>
+                        <tiers>
+                            <tier>
+                                <limits>
+                                    <limit>
+                                        <unit>stones</unit>
+                                        <max>100</max>
+                                    </limit>
+                                </limits>
+                                <recurringPrice>
+                                    <price>
+                                        <currency>USD</currency>
+                                        <value>100</value>
+                                    </price>
+                                </recurringPrice>
+                            </tier>
+                            <tier>
+                                <limits>
+                                    <limit>
+                                        <unit>stones</unit>
+                                        <max>-1</max>
+                                    </limit>
+                                </limits>
+                                <recurringPrice>
+                                    <price>
+                                        <currency>USD</currency>
+                                        <value>1000</value>
+                                    </price>
+                                </recurringPrice>
+                            </tier>
+                        </tiers>
+                    </usage>
+                </usages>
+            </finalPhase>
+        </plan>
     </plans>
     <priceLists>
         <defaultPriceList name="DEFAULT">
@@ -1368,6 +1415,7 @@
                 <plan>pistol-quarterly</plan>
                 <plan>shotgun-annual</plan>
                 <plan>assault-rifle-annual</plan>
+                <plan>trebuchet-usage-in-arrear</plan>
                 <plan>laser-scope-monthly</plan>
                 <plan>telescopic-scope-monthly</plan>
                 <plan>cleaning-monthly</plan>

currency/pom.xml 2(+1 -1)

diff --git a/currency/pom.xml b/currency/pom.xml
index bd13500..efb61bd 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-currency</artifactId>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 80cd41b..4549ace 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index d36367d..9e366b8 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -38,6 +38,7 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.AccountEventsStreams;
 import org.killbill.billing.entitlement.EntitlementService;
 import org.killbill.billing.entitlement.EventsStream;
@@ -314,21 +315,11 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                 final InternalCallContext context = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, callContext);
                 final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), now, context);
 
-                final EventsStream eventsStreamForBaseSubscription = eventsStreamBuilder.buildForBaseSubscription(bundleId, callContext);
+                preCheckAddEntitlement(bundleId, entitlementRequestedDate, baseEntitlementWithAddOnsSpecifier, callContext);
+                try {
 
-                if (eventsStreamForBaseSubscription.isEntitlementCancelled() ||
-                    (eventsStreamForBaseSubscription.isEntitlementPending() &&
-                     (baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate() == null ||
-                      baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate().compareTo(eventsStreamForBaseSubscription.getEntitlementEffectiveStartDate()) < 0))) {
-                    throw new EntitlementApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
-                }
+                    final SubscriptionBaseBundle baseBundle = subscriptionBaseInternalApi.getBundleFromId(bundleId, context);
 
-                // Check the base entitlement state is not blocked
-                if (eventsStreamForBaseSubscription.isBlockChange(entitlementRequestedDate)) {
-                    throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
-                }
-
-                try {
                     final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
                     final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(baseEntitlementWithAddOnsSpecifier);
 
@@ -338,8 +329,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                     final BlockingState newBlockingState = new DefaultBlockingState(subscription.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_START, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, entitlementRequestedDate);
                     entitlementUtils.setBlockingStatesAndPostBlockingTransitionEvent(ImmutableList.<BlockingState>of(newBlockingState), subscription.getBundleId(), context);
 
-
-                    return new DefaultEntitlement(eventsStreamForBaseSubscription.getAccountId(), subscription.getId(), eventsStreamBuilder, entitlementApi, pluginExecution,
+                    return new DefaultEntitlement(baseBundle.getAccountId(), subscription.getId(), eventsStreamBuilder, entitlementApi, pluginExecution,
                                                   blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
                                                   entitlementUtils, dateHelper, clock, securityApi, internalCallContextFactory, callContext);
                 } catch (final SubscriptionBaseApiException e) {
@@ -350,6 +340,37 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
         return pluginExecution.executeWithPlugin(addEntitlementWithPlugin, pluginContext);
     }
 
+    private void preCheckAddEntitlement(final UUID bundleId, final DateTime entitlementRequestedDate, final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier, final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, callContext);
+        final List<SubscriptionBase> subscriptionsByBundle;
+
+        try {
+            subscriptionsByBundle = subscriptionBaseInternalApi.getSubscriptionsForBundle(bundleId, null, context);
+
+            if (subscriptionsByBundle == null || subscriptionsByBundle.size() == 0) {
+                throw new EntitlementApiException(ErrorCode.SUB_NO_ACTIVE_SUBSCRIPTIONS, bundleId);
+            }
+        } catch (SubscriptionBaseApiException e) {
+            throw new EntitlementApiException(e);
+        }
+
+        if (!ProductCategory.STANDALONE.equals(subscriptionsByBundle.get(0).getCategory())) {
+            final EventsStream eventsStreamForBaseSubscription = eventsStreamBuilder.buildForBaseSubscription(bundleId, callContext);
+
+            if (eventsStreamForBaseSubscription.isEntitlementCancelled() ||
+                (eventsStreamForBaseSubscription.isEntitlementPending() &&
+                 (baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate() == null ||
+                  baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate().compareTo(eventsStreamForBaseSubscription.getEntitlementEffectiveStartDate()) < 0))) {
+                throw new EntitlementApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
+            }
+
+            // Check the base entitlement state is not blocked
+            if (eventsStreamForBaseSubscription.isBlockChange(entitlementRequestedDate)) {
+                throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
+            }
+        }
+    }
+
     @Override
     public List<EntitlementAOStatusDryRun> getDryRunStatusForChange(final UUID bundleId, final String targetProductName, @Nullable final LocalDate effectiveDate, final TenantContext context) throws EntitlementApiException {
         final InternalTenantContext internalContext = internalCallContextFactory.createInternalTenantContext(bundleId, ObjectType.BUNDLE, context);
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
index 58285fb..cfbd91f 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
@@ -18,7 +18,6 @@
 
 package org.killbill.billing.entitlement.api;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
@@ -45,9 +44,7 @@ import org.testng.annotations.Test;
 import com.google.common.collect.ImmutableList;
 
 import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
-import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.fail;
 
 public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedDB {
@@ -542,7 +539,6 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
 
     }
 
-
     @Test(groups = "slow")
     public void testCreateEntitlementInThePast() throws AccountApiException, EntitlementApiException, SubscriptionBaseApiException {
         final LocalDate initialDate = new LocalDate(2013, 8, 7);
@@ -645,7 +641,6 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
 
     }
 
-
     @Test(groups = "slow")
     public void testCreateBaseWithEntitlementInTheFuture() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
         final LocalDate initialDate = new LocalDate(2013, 8, 7);
@@ -757,7 +752,6 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
         assertEquals(entitlement.getEffectiveStartDate(), initialDate);
     }
 
-
     @Test(groups = "slow")
     public void testCreateBaseSubscriptionsWithAddOns() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
         final LocalDate initialDate = new LocalDate(2013, 8, 7);
@@ -805,7 +799,6 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
 
     }
 
-
     @Test(groups = "slow", expectedExceptions = EntitlementApiException.class)
     public void testCreateBaseSubscriptionsWithAddOnsMissingBase() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
         final LocalDate initialDate = new LocalDate(2013, 8, 7);
@@ -845,4 +838,46 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
         assertListenerStatus();
 
     }
+
+    @Test(groups = "slow")
+    public void testCreatedBundledStandaloneEntitlements() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = createAccount(getAccountData(7));
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Knife", BillingPeriod.MONTHLY, "notrial", null);
+
+        // Create STANDALONE entitlement
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
+        final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, null, null, false, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        assertEquals(baseEntitlement.getAccountId(), account.getId());
+        assertEquals(baseEntitlement.getExternalKey(), account.getExternalKey());
+        assertEquals(baseEntitlement.getLastActiveProduct().getName(), "Knife");
+        assertEquals(baseEntitlement.getLastActivePlan().getName(), "knife-monthly-notrial");
+        assertEquals(baseEntitlement.getLastActiveProductCategory(), ProductCategory.STANDALONE);
+
+        // Add another STANDALONE entitlement
+        final PlanPhaseSpecifier spec1 = new PlanPhaseSpecifier("Knife", BillingPeriod.MONTHLY, "notrial", null);
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
+        final Entitlement anotherStandaloneEntitlement = entitlementApi.addEntitlement(baseEntitlement.getBundleId(), spec1, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        assertEquals(anotherStandaloneEntitlement.getAccountId(), account.getId());
+        assertEquals(anotherStandaloneEntitlement.getExternalKey(), account.getExternalKey());
+        assertEquals(anotherStandaloneEntitlement.getBundleId(), baseEntitlement.getBundleId());
+
+        assertEquals(anotherStandaloneEntitlement.getLastActivePriceList().getName(), "notrial");
+        assertEquals(anotherStandaloneEntitlement.getLastActiveProduct().getName(), "Knife");
+        assertEquals(anotherStandaloneEntitlement.getLastActivePlan().getName(), "knife-monthly-notrial");
+        assertEquals(anotherStandaloneEntitlement.getLastActiveProductCategory(), ProductCategory.STANDALONE);
+
+        List<Entitlement> bundleEntitlements = entitlementApi.getAllEntitlementsForBundle(anotherStandaloneEntitlement.getBundleId(), callContext);
+        assertEquals(bundleEntitlements.size(), 2);
+
+        bundleEntitlements = entitlementApi.getAllEntitlementsForAccountIdAndExternalKey(account.getId(), account.getExternalKey(), callContext);
+        assertEquals(bundleEntitlements.size(), 2);
+    }
 }

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index 0b60752..c52b20c 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
index 0f6c011..7558dd5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
@@ -92,7 +92,7 @@ public class InvoiceApiHelper {
 
         boolean success = false;
         GlobalLock lock = null;
-        Iterable<Invoice> invoicesForPlugins = null;
+        Iterable<DefaultInvoice> invoicesForPlugins = null;
         try {
             lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
 
@@ -100,10 +100,9 @@ public class InvoiceApiHelper {
 
             final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
             final List<InvoiceModelDao> invoiceModelDaos = new LinkedList<InvoiceModelDao>();
-            for (final Invoice invoiceForPlugin : invoicesForPlugins) {
-                // Call plugin
-                final List<InvoiceItem> additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, isDryRun, context, internalCallContext);
-                invoiceForPlugin.addInvoiceItems(additionalInvoiceItems);
+            for (final DefaultInvoice invoiceForPlugin : invoicesForPlugins) {
+                // Call plugin(s)
+                invoicePluginDispatcher.updateOriginalInvoiceWithPluginInvoiceItems(invoiceForPlugin, isDryRun, context, internalCallContext);
 
                 // Transformation to InvoiceModelDao
                 final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoiceForPlugin);
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 ca4e3c1..fddc528 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
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -36,7 +36,6 @@ import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceApiHelper;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
-import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoicePayment;
 import org.killbill.billing.invoice.api.InvoicePaymentType;
 import org.killbill.billing.invoice.api.InvoiceStatus;
@@ -76,6 +75,10 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
 
     @Override
     public Invoice getInvoiceById(final UUID invoiceId, final InternalTenantContext context) throws InvoiceApiException {
+        return getInvoiceByIdInternal(invoiceId, context);
+    }
+
+    private DefaultInvoice getInvoiceByIdInternal(final UUID invoiceId, final InternalTenantContext context) {
         return new DefaultInvoice(dao.getById(invoiceId, context));
     }
 
@@ -132,12 +135,12 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
 
         // See https://github.com/killbill/killbill/issues/265
         final CallContext callContext = internalCallContextFactory.createCallContext(context);
-        final Invoice invoice = getInvoiceById(refund.getInvoiceId(), context);
+        final DefaultInvoice invoice = getInvoiceByIdInternal(refund.getInvoiceId(), context);
         final UUID accountId = invoice.getAccountId();
         final WithAccountLock withAccountLock = new WithAccountLock() {
             @Override
-            public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
-                return ImmutableList.<Invoice>of(invoice);
+            public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException {
+                return ImmutableList.<DefaultInvoice>of(invoice);
             }
         };
         invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, callContext);
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 52806af..da061de 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
@@ -204,6 +204,10 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
 
     @Override
     public Invoice getInvoice(final UUID invoiceId, final TenantContext context) throws InvoiceApiException {
+        return getInvoiceInternal(invoiceId, context);
+    }
+
+    private DefaultInvoice getInvoiceInternal(final UUID invoiceId, final TenantContext context) {
         final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(invoiceId, ObjectType.INVOICE, context);
         return new DefaultInvoice(dao.getById(invoiceId, internalTenantContext), getCatalogSafelyForPrettyNames(internalTenantContext));
     }
@@ -285,13 +289,13 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
         final WithAccountLock withAccountLock = new WithAccountLock() {
 
             @Override
-            public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
+            public Iterable<DefaultInvoice> 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>();
+                final Map<Currency, DefaultInvoice> newInvoicesForExternalCharges = new HashMap<Currency, DefaultInvoice>();
+                final Map<UUID, DefaultInvoice> existingInvoicesForExternalCharges = new HashMap<UUID, DefaultInvoice>();
 
                 for (final InvoiceItem charge : charges) {
                     final Invoice invoiceForExternalCharge;
@@ -301,13 +305,13 @@ 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, invoiceDate, effectiveDate, currency, status);
+                            final DefaultInvoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
                             newInvoicesForExternalCharges.put(currency, newInvoiceForExternalCharge);
                         }
                         invoiceForExternalCharge = newInvoicesForExternalCharges.get(currency);
                     } else {
                         if (existingInvoicesForExternalCharges.get(invoiceIdForExternalCharge) == null) {
-                            final Invoice existingInvoiceForExternalCharge = getInvoice(invoiceIdForExternalCharge, context);
+                            final DefaultInvoice existingInvoiceForExternalCharge = getInvoiceInternal(invoiceIdForExternalCharge, context);
                             if (InvoiceStatus.COMMITTED.equals(existingInvoiceForExternalCharge.getStatus())) {
                                 throw new InvoiceApiException(ErrorCode.INVOICE_ALREADY_COMMITTED, existingInvoiceForExternalCharge.getId());
                             }
@@ -341,7 +345,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
                     invoiceForExternalCharge.addInvoiceItem(externalCharge);
                 }
 
-                return Iterables.<Invoice>concat(newInvoicesForExternalCharges.values(), existingInvoicesForExternalCharges.values());
+                return Iterables.<DefaultInvoice>concat(newInvoicesForExternalCharges.values(), existingInvoicesForExternalCharges.values());
             }
         };
 
@@ -382,12 +386,12 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
             private InvoiceItem creditItem;
 
             @Override
-            public List<Invoice> prepareInvoices() throws InvoiceApiException {
+            public List<DefaultInvoice> 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;
+                final DefaultInvoice invoiceForCredit;
                 if (invoiceId == null) {
                     final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
                     invoiceForCredit = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
@@ -411,7 +415,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
                                                       itemDetails);
                 invoiceForCredit.addInvoiceItem(creditItem);
 
-                return ImmutableList.<Invoice>of(invoiceForCredit);
+                return ImmutableList.<DefaultInvoice>of(invoiceForCredit);
             }
         };
 
@@ -443,8 +447,8 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
 
         final WithAccountLock withAccountLock = new WithAccountLock() {
             @Override
-            public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
-                final Invoice invoice = getInvoiceAndCheckCurrency(invoiceId, currency, context);
+            public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException {
+                final DefaultInvoice invoice = getInvoiceAndCheckCurrency(invoiceId, currency, context);
                 final InvoiceItem adjustmentItem = invoiceApiHelper.createAdjustmentItem(invoice,
                                                                                          invoiceItemId,
                                                                                          amount,
@@ -457,7 +461,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
                     invoice.addInvoiceItem(adjustmentItem);
                 }
 
-                return ImmutableList.<Invoice>of(invoice);
+                return ImmutableList.<DefaultInvoice>of(invoice);
             }
         };
 
@@ -560,8 +564,8 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
                                                                     }));
     }
 
-    private Invoice getInvoiceAndCheckCurrency(final UUID invoiceId, @Nullable final Currency currency, final TenantContext context) throws InvoiceApiException {
-        final Invoice invoice = getInvoice(invoiceId, context);
+    private DefaultInvoice getInvoiceAndCheckCurrency(final UUID invoiceId, @Nullable final Currency currency, final TenantContext context) throws InvoiceApiException {
+        final DefaultInvoice invoice = getInvoiceInternal(invoiceId, context);
         // Check the specified currency matches the one of the existing invoice
         if (currency != null && invoice.getCurrency() != currency) {
             throw new InvoiceApiException(ErrorCode.CURRENCY_INVALID, currency, invoice.getCurrency());
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/WithAccountLock.java b/invoice/src/main/java/org/killbill/billing/invoice/api/WithAccountLock.java
index 165c100..2e13120 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/WithAccountLock.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/WithAccountLock.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2015 Groupon, Inc
- * Copyright 2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -17,7 +17,9 @@
 
 package org.killbill.billing.invoice.api;
 
+import org.killbill.billing.invoice.model.DefaultInvoice;
+
 public interface WithAccountLock {
 
-    public Iterable<Invoice> prepareInvoices() throws InvoiceApiException;
+    public Iterable<DefaultInvoice> prepareInvoices() throws InvoiceApiException;
 }
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 71accaa..00af476 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -75,12 +75,9 @@ import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
 import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
 import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates.UsageDef;
 import org.killbill.billing.invoice.model.DefaultInvoice;
-import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
-import org.killbill.billing.invoice.model.InvoiceItemCatalogBase;
 import org.killbill.billing.invoice.model.InvoiceItemFactory;
 import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
 import org.killbill.billing.invoice.model.ParentInvoiceItem;
-import org.killbill.billing.invoice.model.RecurringInvoiceItem;
 import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
 import org.killbill.billing.invoice.notification.NextBillingDateNotificationKey;
 import org.killbill.billing.junction.BillingEvent;
@@ -560,58 +557,20 @@ public class InvoiceDispatcher {
 
         boolean success = false;
         try {
-            // Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin
+            // Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin(s)
             final InvoiceItem cbaItemPreInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext);
-            DefaultInvoice tmpInvoiceForInvoicePlugins = invoice;
             if (cbaItemPreInvoicePlugins != null) {
-                tmpInvoiceForInvoicePlugins = (DefaultInvoice) tmpInvoiceForInvoicePlugins.clone();
-                tmpInvoiceForInvoicePlugins.addInvoiceItem(cbaItemPreInvoicePlugins);
+                invoice.addInvoiceItem(cbaItemPreInvoicePlugins);
             }
+
             //
             // Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
             //
-            final List<InvoiceItem> additionalInvoiceItemsFromPlugins = invoicePluginDispatcher.getAdditionalInvoiceItems(tmpInvoiceForInvoicePlugins, isDryRun, callContext, internalCallContext);
-            if (additionalInvoiceItemsFromPlugins.isEmpty()) {
-                // PERF: avoid re-computing the CBA if no change was made
+            final boolean invoiceUpdated = invoicePluginDispatcher.updateOriginalInvoiceWithPluginInvoiceItems(invoice, isDryRun, callContext, internalCallContext);
+            if (invoiceUpdated) {
+                // Remove the temporary CBA item as we need to re-compute CBA
                 if (cbaItemPreInvoicePlugins != null) {
-                    invoice.addInvoiceItem(cbaItemPreInvoicePlugins);
-                }
-            } else {
-                // Add or update items from generated invoice
-                for (final InvoiceItem cur : additionalInvoiceItemsFromPlugins) {
-                    final InvoiceItem exitingItem = Iterables.tryFind(tmpInvoiceForInvoicePlugins.getInvoiceItems(), new Predicate<InvoiceItem>() {
-                        @Override
-                        public boolean apply(final InvoiceItem input) {
-                            return input.getId().equals(cur.getId());
-                        }
-                    }).orNull();
-                    if (exitingItem != null) {
-                        invoice.removeInvoiceItem(exitingItem);
-                    }
-
-                    final InvoiceItem sanitizedInvoiceItemFromPlugin = new InvoiceItemCatalogBase(cur.getId(),
-                                                                                                  cur.getCreatedDate(),
-                                                                                                  MoreObjects.firstNonNull(cur.getInvoiceId(), invoice.getId()),
-                                                                                                  cur.getAccountId(),
-                                                                                                  cur.getBundleId(),
-                                                                                                  cur.getSubscriptionId(),
-                                                                                                  cur.getDescription(),
-                                                                                                  cur.getPlanName(),
-                                                                                                  cur.getPhaseName(),
-                                                                                                  cur.getUsageName(),
-                                                                                                  cur.getPrettyPlanName(),
-                                                                                                  cur.getPrettyPhaseName(),
-                                                                                                  cur.getPrettyUsageName(),
-                                                                                                  cur.getStartDate(),
-                                                                                                  cur.getEndDate(),
-                                                                                                  cur.getAmount(),
-                                                                                                  cur.getRate(),
-                                                                                                  cur.getCurrency(),
-                                                                                                  cur.getLinkedItemId(),
-                                                                                                  cur.getQuantity(),
-                                                                                                  cur.getItemDetails(),
-                                                                                                  cur.getInvoiceItemType());
-                    invoice.addInvoiceItem(sanitizedInvoiceItemFromPlugin);
+                    invoice.removeInvoiceItem(cbaItemPreInvoicePlugins);
                 }
 
                 // Use credit after we call the plugin (https://github.com/killbill/killbill/issues/637)
@@ -639,7 +598,7 @@ public class InvoiceDispatcher {
                 success = true;
 
                 try {
-                    setChargedThroughDates(invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), internalCallContext);
+                    setChargedThroughDates(invoice, internalCallContext);
                 } catch (final SubscriptionBaseApiException e) {
                     log.error("Failed handling SubscriptionBase change.", e);
                     return null;
@@ -651,8 +610,8 @@ public class InvoiceDispatcher {
                 commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, internalCallContext);
             }
 
-            if (success) {
-                final DefaultInvoice refreshedInvoice = new DefaultInvoice(invoiceDao.getById(invoice.getId(), internalCallContext));
+            if (isDryRun || success) {
+                final DefaultInvoice refreshedInvoice = isDryRun ? invoice : new DefaultInvoice(invoiceDao.getById(invoice.getId(), internalCallContext));
                 invoicePluginDispatcher.onSuccessCall(targetDate, refreshedInvoice, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
             } else {
                 invoicePluginDispatcher.onFailureCall(targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
@@ -854,12 +813,23 @@ public class InvoiceDispatcher {
         return internalCallContextFactory.createCallContext(context);
     }
 
-    private void setChargedThroughDates(final Collection<InvoiceItem> fixedPriceItems,
-                                        final Collection<InvoiceItem> recurringItems,
-                                        final InternalCallContext context) throws SubscriptionBaseApiException {
+    private void setChargedThroughDates(final Invoice invoice, final InternalCallContext context) throws SubscriptionBaseApiException {
+        // Don't use invoice.getInvoiceItems(final Class<T> clazz) as some items can come from plugins
+        final Collection<InvoiceItem> invoiceItemsToConsider = new LinkedList<InvoiceItem>();
+        for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+            switch (invoiceItem.getInvoiceItemType()) {
+                case FIXED:
+                case RECURRING:
+                case USAGE:
+                    invoiceItemsToConsider.add(invoiceItem);
+                    break;
+                default:
+                    break;
+            }
+        }
+
         final Map<UUID, DateTime> chargeThroughDates = new HashMap<UUID, DateTime>();
-        addInvoiceItemsToChargeThroughDates(chargeThroughDates, fixedPriceItems, context);
-        addInvoiceItemsToChargeThroughDates(chargeThroughDates, recurringItems, context);
+        addInvoiceItemsToChargeThroughDates(chargeThroughDates, invoiceItemsToConsider, context);
 
         for (final UUID subscriptionId : chargeThroughDates.keySet()) {
             if (subscriptionId != null) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
index d1abded..59f8db4 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
@@ -24,7 +24,9 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import org.joda.time.DateTime;
@@ -37,6 +39,7 @@ 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.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.InvoiceItemCatalogBase;
 import org.killbill.billing.invoice.plugin.api.InvoiceContext;
 import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
 import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
@@ -48,7 +51,10 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 
 public class InvoicePluginDispatcher {
 
@@ -102,7 +108,7 @@ public class InvoicePluginDispatcher {
     }
 
     public void onSuccessCall(final LocalDate targetDate,
-                              final DefaultInvoice invoice,
+                              @Nullable final DefaultInvoice invoice,
                               final List<Invoice> existingInvoices,
                               final boolean isDryRun,
                               final boolean isRescheduled,
@@ -113,7 +119,7 @@ public class InvoicePluginDispatcher {
     }
 
     public void onFailureCall(final LocalDate targetDate,
-                              final DefaultInvoice invoice,
+                              @Nullable final DefaultInvoice invoice,
                               final List<Invoice> existingInvoices,
                               final boolean isDryRun,
                               final boolean isRescheduled,
@@ -125,7 +131,7 @@ public class InvoicePluginDispatcher {
 
     private void onCompletionCall(final boolean isSuccess,
                                   final LocalDate targetDate,
-                                  final DefaultInvoice originalInvoice,
+                                  @Nullable final DefaultInvoice originalInvoice,
                                   final List<Invoice> existingInvoices,
                                   final boolean isDryRun,
                                   final boolean isRescheduled,
@@ -137,7 +143,7 @@ public class InvoicePluginDispatcher {
         }
 
         // We clone the original invoice so plugins don't remove/add items
-        final Invoice clonedInvoice = (Invoice) originalInvoice.clone();
+        final Invoice clonedInvoice = originalInvoice == null ? null : (Invoice) originalInvoice.clone();
         final InvoiceContext invoiceContext = new DefaultInvoiceContext(targetDate, clonedInvoice, existingInvoices, isDryRun, isRescheduled, callContext);
 
         for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
@@ -149,39 +155,116 @@ public class InvoicePluginDispatcher {
         }
     }
 
-    //
-    // If we have multiple plugins there is a question of plugin ordering and also a 'product' questions to decide whether
-    // subsequent plugins should have access to items added by previous plugins
-    //
-    public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice originalInvoice, final boolean isDryRun, final CallContext callContext, final InternalTenantContext tenantContext) throws InvoiceApiException {
+    public boolean updateOriginalInvoiceWithPluginInvoiceItems(final DefaultInvoice originalInvoice, final boolean isDryRun, final CallContext callContext, final InternalTenantContext tenantContext) throws InvoiceApiException {
         log.debug("Invoking invoice plugins getAdditionalInvoiceItems: isDryRun='{}', originalInvoice='{}'", isDryRun, originalInvoice);
 
-        final List<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();
-
         final Collection<InvoicePluginApi> invoicePlugins = getInvoicePlugins(tenantContext).values();
         if (invoicePlugins.isEmpty()) {
-            return additionalInvoiceItems;
+            return false;
         }
 
-        // We clone the original invoice so plugins don't remove/add items
-        final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
+        boolean invoiceUpdated = false;
         for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
-            final List<InvoiceItem> items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
-            if (items != null) {
-                for (final InvoiceItem item : items) {
-                    validateInvoiceItemFromPlugin(item, invoicePlugin);
-                    additionalInvoiceItems.add(item);
+            // We clone the original invoice so plugins don't remove/add items
+            final Invoice clonedInvoice = (Invoice) originalInvoice.clone();
+            final List<InvoiceItem> additionalInvoiceItemsForPlugin = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
+
+            if (additionalInvoiceItemsForPlugin != null && !additionalInvoiceItemsForPlugin.isEmpty()) {
+                final Collection<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();
+                for (final InvoiceItem additionalInvoiceItem : additionalInvoiceItemsForPlugin) {
+                    final InvoiceItem sanitizedInvoiceItem = validateAndSanitizeInvoiceItemFromPlugin(originalInvoice, additionalInvoiceItem, invoicePlugin);
+                    additionalInvoiceItems.add(sanitizedInvoiceItem);
                 }
+                invoiceUpdated = invoiceUpdated || updateOriginalInvoiceWithPluginInvoiceItems(originalInvoice, additionalInvoiceItems);
             }
         }
-        return additionalInvoiceItems;
+
+        return invoiceUpdated;
+    }
+
+    private boolean updateOriginalInvoiceWithPluginInvoiceItems(final DefaultInvoice originalInvoice, final Collection<InvoiceItem> additionalInvoiceItems) {
+        if (additionalInvoiceItems.isEmpty()) {
+            return false;
+        }
+
+        // Add or update items from generated invoice
+        for (final InvoiceItem additionalInvoiceItem : additionalInvoiceItems) {
+            final InvoiceItem existingItem = Iterables.tryFind(originalInvoice.getInvoiceItems(),
+                                                               new Predicate<InvoiceItem>() {
+                                                                   @Override
+                                                                   public boolean apply(final InvoiceItem originalInvoiceItem) {
+                                                                       return originalInvoiceItem.getId().equals(additionalInvoiceItem.getId());
+                                                                   }
+                                                               }).orNull();
+            if (existingItem != null) {
+                originalInvoice.removeInvoiceItem(existingItem);
+            }
+            originalInvoice.addInvoiceItem(additionalInvoiceItem);
+        }
+
+        return true;
+    }
+
+    private InvoiceItem validateAndSanitizeInvoiceItemFromPlugin(final Invoice originalInvoice, final InvoiceItem additionalInvoiceItem, final InvoicePluginApi invoicePlugin) throws InvoiceApiException {
+        final InvoiceItem existingItem = Iterables.<InvoiceItem>tryFind(originalInvoice.getInvoiceItems(),
+                                                                      new Predicate<InvoiceItem>() {
+                                                                          @Override
+                                                                          public boolean apply(final InvoiceItem originalInvoiceItem) {
+                                                                              return originalInvoiceItem.getId().equals(additionalInvoiceItem.getId());
+                                                                          }
+                                                                      }).orNull();
+
+        if (!ALLOWED_INVOICE_ITEM_TYPES.contains(additionalInvoiceItem.getInvoiceItemType()) && existingItem == null) {
+            log.warn("Ignoring invoice item of type {} from InvoicePlugin {}: {}", additionalInvoiceItem.getInvoiceItemType(), invoicePlugin, additionalInvoiceItem);
+            throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_TYPE_INVALID, additionalInvoiceItem.getInvoiceItemType());
+        }
+
+        final UUID invoiceId = MoreObjects.firstNonNull(mutableField("invoiceId", existingItem != null ? existingItem.getInvoiceId() : null, additionalInvoiceItem.getInvoiceId(), invoicePlugin),
+                                                        originalInvoice.getId());
+        return new InvoiceItemCatalogBase(additionalInvoiceItem.getId(),
+                                          mutableField("createdDate", existingItem != null ? existingItem.getCreatedDate() : null, additionalInvoiceItem.getCreatedDate(), invoicePlugin),
+                                          invoiceId,
+                                          immutableField("accountId", existingItem, existingItem != null ? existingItem.getAccountId() : null, additionalInvoiceItem.getAccountId(), invoicePlugin),
+                                          immutableField("bundleId", existingItem, existingItem != null ? existingItem.getBundleId() : null, additionalInvoiceItem.getBundleId(), invoicePlugin),
+                                          immutableField("subscriptionId", existingItem, existingItem != null ? existingItem.getSubscriptionId() : null, additionalInvoiceItem.getSubscriptionId(), invoicePlugin),
+                                          mutableField("description", existingItem != null ? existingItem.getDescription() : null, additionalInvoiceItem.getDescription(), invoicePlugin),
+                                          immutableField("planName", existingItem, existingItem != null ? existingItem.getPlanName() : null, additionalInvoiceItem.getPlanName(), invoicePlugin),
+                                          immutableField("phaseName", existingItem, existingItem != null ? existingItem.getPhaseName() : null, additionalInvoiceItem.getPhaseName(), invoicePlugin),
+                                          immutableField("usageName", existingItem, existingItem != null ? existingItem.getUsageName() : null, additionalInvoiceItem.getUsageName(), invoicePlugin),
+                                          mutableField("prettyPlanName", existingItem != null ? existingItem.getPrettyPlanName() : null, additionalInvoiceItem.getPrettyPlanName(), invoicePlugin),
+                                          mutableField("prettyPhaseName", existingItem != null ? existingItem.getPrettyPhaseName() : null, additionalInvoiceItem.getPrettyPhaseName(), invoicePlugin),
+                                          mutableField("prettyUsageName", existingItem != null ? existingItem.getPrettyUsageName() : null, additionalInvoiceItem.getPrettyUsageName(), invoicePlugin),
+                                          immutableField("startDate", existingItem, existingItem != null ? existingItem.getStartDate() : null, additionalInvoiceItem.getStartDate(), invoicePlugin),
+                                          immutableField("endDate", existingItem, existingItem != null ? existingItem.getEndDate() : null, additionalInvoiceItem.getEndDate(), invoicePlugin),
+                                          mutableField("amount", existingItem != null ? existingItem.getAmount() : null, additionalInvoiceItem.getAmount(), invoicePlugin),
+                                          immutableField("rate", existingItem, existingItem != null ? existingItem.getRate() : null, additionalInvoiceItem.getRate(), invoicePlugin),
+                                          immutableField("currency", existingItem, existingItem != null ? existingItem.getCurrency() : null, additionalInvoiceItem.getCurrency(), invoicePlugin),
+                                          immutableField("linkedItemId", existingItem, existingItem != null ? existingItem.getLinkedItemId() : null, additionalInvoiceItem.getLinkedItemId(), invoicePlugin),
+                                          immutableField("quantity", existingItem, existingItem != null ? existingItem.getQuantity() : null, additionalInvoiceItem.getQuantity(), invoicePlugin),
+                                          mutableField("itemDetails", existingItem != null ? existingItem.getItemDetails() : null, additionalInvoiceItem.getItemDetails(), invoicePlugin),
+                                          immutableField("invoiceItemType", existingItem, existingItem != null ? existingItem.getInvoiceItemType() : null, additionalInvoiceItem.getInvoiceItemType(), invoicePlugin));
     }
 
-    private void validateInvoiceItemFromPlugin(final InvoiceItem invoiceItem, final InvoicePluginApi invoicePlugin) throws InvoiceApiException {
-        if (!ALLOWED_INVOICE_ITEM_TYPES.contains(invoiceItem.getInvoiceItemType())) {
-            log.warn("Ignoring invoice item of type {} from InvoicePlugin {}: {}", invoiceItem.getInvoiceItemType(), invoicePlugin, invoiceItem);
-            throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_TYPE_INVALID, invoiceItem.getInvoiceItemType());
+    private <T> T mutableField(final String fieldName, @Nullable final T existingValue, @Nullable final T updatedValue, final InvoicePluginApi invoicePlugin) {
+        if (updatedValue != null) {
+            log.debug("Overriding mutable invoice item value from InvoicePlugin {} for fieldName='{}': existingValue='{}', updatedValue='{}'",
+                      invoicePlugin, fieldName, existingValue, updatedValue);
+            return updatedValue;
+        } else {
+            return existingValue;
+        }
+    }
+
+    private <T> T immutableField(final String fieldName, @Nullable final InvoiceItem existingItem, @Nullable final T existingValue, @Nullable final T updatedValue, final InvoicePluginApi invoicePlugin) {
+        if (existingItem == null) {
+            return updatedValue;
+        }
+
+        if (updatedValue != null && !updatedValue.equals(existingValue)) {
+            log.warn("Ignoring immutable invoice item value from InvoicePlugin {} for fieldName='{}': existingValue='{}', updatedValue='{}'",
+                     invoicePlugin, fieldName, existingValue, updatedValue);
         }
+        return existingValue;
     }
 
     private Map<String, InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
index 0e6e941..9086ccb 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotificationKey.java
@@ -45,9 +45,9 @@ public class TestNextBillingDateNotificationKey {
         Assert.assertEquals(result.getUuidKey(), uuidKey);
         Assert.assertEquals(result.getTargetDate().compareTo(targetDate), 0);
         Assert.assertEquals(result.isDryRunForInvoiceNotification(), isDryRunForInvoiceNotification);
+        Assert.assertFalse(key.isRescheduled());
     }
 
-
     @Test(groups = "fast")
     public void testBasicWithUUIDKeys() throws Exception {
 
@@ -56,13 +56,14 @@ public class TestNextBillingDateNotificationKey {
         final DateTime targetDate = new DateTime();
         final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
 
-        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(null, ImmutableList.of(uuidKey1, uuidKey2), targetDate, isDryRunForInvoiceNotification, false);
+        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(null, ImmutableList.of(uuidKey1, uuidKey2), targetDate, isDryRunForInvoiceNotification, true);
         final String json = mapper.writeValueAsString(key);
 
         final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
         Assert.assertNull(result.getUuidKey());
         Assert.assertEquals(result.getTargetDate().compareTo(targetDate), 0);
         Assert.assertEquals(result.isDryRunForInvoiceNotification(), isDryRunForInvoiceNotification);
+        Assert.assertTrue(key.isRescheduled());
         Assert.assertNotNull(result.getUuidKeys());
 
         Assert.assertTrue(Iterables.contains(result.getUuidKeys(), uuidKey1));
@@ -80,6 +81,5 @@ public class TestNextBillingDateNotificationKey {
         // Compatibility mode : Although the  uuidKeys is not in the json, we verify the getter return the right result
         Assert.assertNotNull(result.getUuidKeys());
         Assert.assertEquals(result.getUuidKeys().iterator().next().toString(), "a38c363f-b25b-4287-8ebc-55964e116d2f");
-
     }
 }

jaxrs/pom.xml 2(+1 -1)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index ce69493..764f7ee 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-jaxrs</artifactId>
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index 7172209..0f8aefb 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -192,12 +192,15 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
         logDeprecationParameterWarningIfNeeded(QUERY_REQUESTED_DT, QUERY_ENTITLEMENT_REQUESTED_DT, QUERY_BILLING_REQUESTED_DT);
 
-        // For ADD_ON we can provide externalKey or the bundleId
-        final boolean createAddOnEntitlement = ProductCategory.ADD_ON.toString().equals(subscription.getProductCategory());
-        if (createAddOnEntitlement) {
+        // For ADD_ON we need to provide externalKey or the bundleId
+        if (ProductCategory.ADD_ON.toString().equals(subscription.getProductCategory())) {
             Preconditions.checkArgument(subscription.getExternalKey() != null || subscription.getBundleId() != null, "SubscriptionJson bundleId or externalKey should be specified for ADD_ON");
         }
 
+        final boolean isInitialBundle = ProductCategory.BASE == subscription.getProductCategory() ||
+                                        (ProductCategory.STANDALONE == subscription.getProductCategory() && subscription.getBundleId() == null);
+
+
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createCallContextNoAccountId(createdBy, reason, comment, request);
 
@@ -215,9 +218,9 @@ public class SubscriptionResource extends JaxRsResourceBase {
                 final LocalDate resolvedEntitlementDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(entitlementDate);
                 final LocalDate resolvedBillingDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(billingDate);
                 final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(subscription.getPriceOverrides(), spec, account.getCurrency());
-                final Entitlement result = createAddOnEntitlement ?
-                                           entitlementApi.addEntitlement(getBundleIdForAddOnCreation(subscription), spec, overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext) :
-                                           entitlementApi.createBaseEntitlement(account.getId(), spec, subscription.getExternalKey(), overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, renameKeyIfExistsAndUnused, pluginProperties, callContext);
+                final Entitlement result = isInitialBundle
+                                           ? entitlementApi.createBaseEntitlement(account.getId(), spec, subscription.getExternalKey(), overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, renameKeyIfExistsAndUnused, pluginProperties, callContext)
+                                           : entitlementApi.addEntitlement(getBundleIdForAddOnCreation(subscription), spec, overrides, resolvedEntitlementDate, resolvedBillingDate, isMigrated, pluginProperties, callContext);
                 if (newBCD != null) {
                     result.updateBCD(newBCD, null, callContext);
                 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
index 2f99d6d..dabce06 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
@@ -124,7 +124,7 @@ public class TenantResource extends JaxRsResourceBase {
     @Produces(APPLICATION_JSON)
     @ApiOperation(value = "Create a tenant", response = TenantJson.class)
     @ApiResponses(value = {@ApiResponse(code = 201, message = "Tenant created successfully"),
-                           @ApiResponse(code = 500, message = "Tenant already exists")})
+                           @ApiResponse(code = 409, message = "Tenant already exists")})
     public Response createTenant(final TenantJson json,
                                  @QueryParam(QUERY_TENANT_USE_GLOBAL_DEFAULT) @DefaultValue("false") final Boolean useGlobalDefault,
                                  @HeaderParam(HDR_CREATED_BY) final String createdBy,

junction/pom.xml 2(+1 -1)

diff --git a/junction/pom.xml b/junction/pom.xml
index 96d04c2..3d51db9 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-junction</artifactId>

NEWS 9(+5 -4)

diff --git a/NEWS b/NEWS
index b69e7fa..b7c3775 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,7 @@
+0.19.9
+    Introduce thread-local dirty DB flag
+    Fix NPE in bus and notifications threads
+
 0.19.5
     
     Database perf improvements
@@ -24,10 +28,7 @@
     See https://github.com/killbill/killbill/releases/tag/killbill-0.18.18
 
 0.18.17
-    Relax sanity checks for STANDALONE subscriptions #840
-    Fix JDBC connection leak in pagination API #853
-    Fix limitation where catalog plan name cannot end with an number #842
-    Reduce log level of InvoiceItemGeneratorLogger #851
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.18.17
 
 0.18.16
     See https://github.com/killbill/killbill/releases/tag/killbill-0.18.16

overdue/pom.xml 2(+1 -1)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index b262c52..0670da2 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-overdue</artifactId>

payment/pom.xml 2(+1 -1)

diff --git a/payment/pom.xml b/payment/pom.xml
index ee27bb7..8c33f57 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>

pom.xml 4(+2 -2)

diff --git a/pom.xml b/pom.xml
index 3521af4..d340aae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,10 +21,10 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.141.47</version>
+        <version>0.141.55</version>
     </parent>
     <artifactId>killbill</artifactId>
-    <version>0.19.7-SNAPSHOT</version>
+    <version>0.19.10-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index 020b7d2..bf7e2e2 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
index 6e522d6..a45d67e 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
@@ -17,14 +17,16 @@
 
 package org.killbill.billing.server.log.obfuscators;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 
 import ch.qos.logback.classic.pattern.ClassicConverter;
 import ch.qos.logback.classic.spi.ILoggingEvent;
-import com.google.common.collect.ImmutableList;
 
 /**
- * ObfuscatorConverter attempts to mask sensitive data in the log files.
+ * ObfuscatorConverter attempts to mask sensitive data in the log files. Extra parameters can be passed to the
+ * converter and the underlying obsfucators by adding arguments behind maskedMsg like the example shown below.
  * <p/>
  * To use, define a new conversion word in your Logback configuration, e.g.:
  * <pre>
@@ -33,7 +35,7 @@ import com.google.common.collect.ImmutableList;
  *             converterClass="org.killbill.billing.server.log.obfuscators.ObfuscatorConverter" />
  *         <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  *             <encoder>
- *                 <pattern>%date [%thread] - %maskedMsg%n</pattern>
+ *                 <pattern>%date [%thread] - %maskedMsg{param1, param2, ...}%n</pattern>
  *             </encoder>
  *         </appender>
  *         <root level="DEBUG">
@@ -44,10 +46,16 @@ import com.google.common.collect.ImmutableList;
  */
 public class ObfuscatorConverter extends ClassicConverter {
 
-    private final Collection<Obfuscator> obfuscators = ImmutableList.<Obfuscator>of(new ConfigMagicObfuscator(),
-                                                                                    new LoggingFilterObfuscator(),
-                                                                                    new PatternObfuscator(),
-                                                                                    new LuhnMaskingObfuscator());
+    private final Collection<Obfuscator> obfuscators = new ArrayList<Obfuscator>();
+
+    @Override
+    public void start() {
+        obfuscators.addAll(Arrays.asList(new ConfigMagicObfuscator(),
+                                         new LoggingFilterObfuscator(),
+                                         new PatternObfuscator(getOptionList()),
+                                         new LuhnMaskingObfuscator()));
+        super.start();
+    }
 
     @Override
     public String convert(final ILoggingEvent event) {
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
index a850c5b..df4887a 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
@@ -17,6 +17,7 @@
 
 package org.killbill.billing.server.log.obfuscators;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedList;
 import java.util.regex.Pattern;
@@ -27,7 +28,7 @@ import com.google.common.collect.ImmutableList;
 public class PatternObfuscator extends Obfuscator {
 
     // Hide by default sensitive bank, PCI and PII data. For PANs, see LuhnMaskingObfuscator
-    private static final String[] DEFAULT_SENSITIVE_KEYS = {
+    private static final Collection<String> DEFAULT_SENSITIVE_KEYS = ImmutableList.of(
             "accountnumber",
             "authenticationdata",
             "bankaccountnumber",
@@ -49,19 +50,27 @@ public class PatternObfuscator extends Obfuscator {
             "name",
             "number",
             "password",
-            "xid"
-    };
+            "xid");
 
     private final Collection<Pattern> patterns = new LinkedList<Pattern>();
 
     public PatternObfuscator() {
-        this(ImmutableList.<Pattern>of());
+        this(ImmutableList.<Pattern>of(), ImmutableList.<String>of());
     }
 
-    public PatternObfuscator(final Collection<Pattern> extraPatterns) {
+    public PatternObfuscator(final Collection<String> extraKeywords) {
+        this(ImmutableList.<Pattern>of(), extraKeywords);
+    }
+
+    public PatternObfuscator(final Collection<Pattern> extraPatterns, final Collection<String> extraKeywords) {
         super();
+        Collection<String> keywords = new ArrayList<String>();
+        keywords.addAll(DEFAULT_SENSITIVE_KEYS);
+        if (extraKeywords != null) {
+            keywords.addAll(extraKeywords);
+        }
 
-        for (final String sensitiveKey : DEFAULT_SENSITIVE_KEYS) {
+        for (final String sensitiveKey : keywords) {
             this.patterns.add(buildJSONPattern(sensitiveKey));
             this.patterns.add(buildXMLPattern(sensitiveKey));
             this.patterns.add(buildMultiValuesXMLPattern(sensitiveKey));
@@ -81,7 +90,7 @@ public class PatternObfuscator extends Obfuscator {
     }
 
     private Pattern buildXMLPattern(final String key) {
-        return Pattern.compile(key + ">([^<\\n]+)</[^<>]*" + key + ">", DEFAULT_PATTERN_FLAGS);
+        return Pattern.compile(key + "(?:\\s+.*?)?>([^<\\n]+)</[^<>]*" + key + ">", DEFAULT_PATTERN_FLAGS);
     }
 
     private Pattern buildMultiValuesXMLPattern(final String key) {
diff --git a/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties b/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
index c8dde49..e03035f 100644
--- a/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,18 +1,155 @@
 ## Top level keys
 # general.notice = This notice should rarely, if ever, be used as everyone will see it
 
+### 0.19.x series ###
+
+# 0.19.7
+0.19.7.updates           =
+0.19.7.notices           = This is the latest dev release.
+0.19.7.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.7
+
+# 0.19.6
+0.19.6.updates           = 0.19.7
+0.19.6.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.6.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.6
+
+# 0.19.5
+0.19.5.updates           = 0.19.7
+0.19.5.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.5.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.5
+
+# 0.19.4
+0.19.4.updates           = 0.19.7
+0.19.4.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.4.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.4
+
+# 0.19.3
+0.19.3.updates           = 0.19.7
+0.19.3.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.3.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.3
+
+# 0.19.2
+0.19.2.updates           = 0.19.7
+0.19.2.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.2.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.2
+
+# 0.19.1
+0.19.1.updates           = 0.19.7
+0.19.1.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.1.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.1
+
+# 0.19.0
+0.19.0.updates           = 0.19.7
+0.19.0.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.0.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.0
+
 ### 0.18.x series ###
 
+# 0.18.19
+0.18.19.updates           =
+0.18.19.notices           = This is the latest GA release.
+0.18.19.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.19
+
+# 0.18.18
+0.18.18.updates           = 0.18.19
+0.18.18.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.18.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.18
+
+# 0.18.17
+0.18.17.updates           = 0.18.19
+0.18.17.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.17.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.17
+
+# 0.18.16
+0.18.16.updates           = 0.18.19
+0.18.16.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.16.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.16
+
+# 0.18.15
+0.18.15.updates           = 0.18.19
+0.18.15.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.15.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.15
+
+# 0.18.14
+0.18.14.updates           = 0.18.19
+0.18.14.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.14.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.14
+
+# 0.18.13
+0.18.13.updates           = 0.18.19
+0.18.13.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.13.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.13
+
+# 0.18.12
+0.18.12.updates           = 0.18.19
+0.18.12.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.12.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.12
+
+# 0.18.11
+0.18.11.updates           = 0.18.19
+0.18.11.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.11.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.11
+
+# 0.18.10
+0.18.10.updates           = 0.18.19
+0.18.10.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.10.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.10
+
+# 0.18.9
+0.18.9.updates           = 0.18.19
+0.18.9.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.9.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.9
+
+# 0.18.8
+0.18.8.updates           = 0.18.19
+0.18.8.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.8.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.8
+
+# 0.18.7
+0.18.7.updates           = 0.18.19
+0.18.7.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.7.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.7
+
+# 0.18.6
+0.18.6.updates           = 0.18.19
+0.18.6.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.6.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.6
+
+# 0.18.5
+0.18.5.updates           = 0.18.19
+0.18.5.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.5.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.5
+
+# 0.18.4
+0.18.4.updates           = 0.18.19
+0.18.4.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.4.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.4
+
+# 0.18.3
+0.18.3.updates           = 0.18.19
+0.18.3.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.3.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.3
+
+# 0.18.2
+0.18.2.updates           = 0.18.19
+0.18.2.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.2.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.2
+
+# 0.18.1
+0.18.1.updates           = 0.18.19
+0.18.1.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.1.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.1
+
 # 0.18.0
-0.18.0.updates           =
-0.18.0.notices           = This is the latest GA release.
+0.18.0.updates           = 0.18.19
+0.18.0.notices           = We recommend upgrading to 0.18.19, our latest GA release.
 0.18.0.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.0
 
 ### 0.17.x series ###
 
 # 0.17.8
 0.17.8.updates           =
-0.17.8.notices           = This is the latest dev release.
+0.17.8.notices           = We recommend upgrading to 0.18.19, our latest GA release.
 0.17.8.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.17.8
 
 # 0.17.7
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
index 9f08735..fe9f6e6 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
@@ -103,7 +103,7 @@ public class TestCatalog extends TestJaxrsBase {
         Assert.assertEquals(catalogsJson.get(0).getName(), "Firearms");
         Assert.assertEquals(catalogsJson.get(0).getEffectiveDate().toLocalDate(), new LocalDate("2011-01-01"));
         Assert.assertEquals(catalogsJson.get(0).getCurrencies().size(), 3);
-        Assert.assertEquals(catalogsJson.get(0).getProducts().size(), 12);
+        Assert.assertEquals(catalogsJson.get(0).getProducts().size(), 13);
         Assert.assertEquals(catalogsJson.get(0).getPriceLists().size(), 7);
 
         for (final Product productJson : catalogsJson.get(0).getProducts()) {
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java
index 4d54512..ba4aa74 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java
@@ -17,12 +17,15 @@
 
 package org.killbill.billing.server.log.obfuscators;
 
+import java.util.List;
+
 import org.killbill.billing.server.log.ServerTestSuiteNoDB;
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.google.common.collect.ImmutableList;
 
 public class TestObfuscatorConverter extends ServerTestSuiteNoDB {
 
@@ -60,11 +63,52 @@ public class TestObfuscatorConverter extends ServerTestSuiteNoDB {
                "</gateway>");
     }
 
+    @Test(groups = "fast")
+    public void testLogSensitiveDataWithExtraKeywords() throws Exception {
+        verifyWithExtendedPatternObfuscator("Starting purchase call: \n" +
+                                            "<gateway>\n" +
+                                            "<card>4111111111111111</card>\n" +
+                                            "<address1>790 test blvd</address1>\n" +
+                                            "<bankAccountNumber>482391823</bankAccountNumber>\n" +
+                                            "<password>supersecret</password>\n" +
+                                            "</gateway>",
+                                            "Starting purchase call: \n" +
+                                            "<gateway>\n" +
+                                            "<card>411111******1111</card>\n" +
+                                            "<address1>*************</address1>\n" +
+                                            "<bankAccountNumber>*********</bankAccountNumber>\n" +
+                                            "<password>***********</password>\n" +
+                                            "</gateway>");
+    }
+
     private void verify(final String input, final String output) {
         final ILoggingEvent event = Mockito.mock(ILoggingEvent.class);
         Mockito.when(event.getFormattedMessage()).thenReturn(input);
 
+        converter.start();
         final String obfuscated = converter.convert(event);
         Assert.assertEquals(obfuscated, output, obfuscated);
     }
+
+    private void verifyWithExtendedPatternObfuscator(final String input, final String output) {
+        final ExtendedObfuscatorConverter extendedConverter = new ExtendedObfuscatorConverter();
+        final ILoggingEvent event = Mockito.mock(ILoggingEvent.class);
+        Mockito.when(event.getFormattedMessage()).thenReturn(input);
+
+        extendedConverter.start();
+        final String obfuscated = extendedConverter.convert(event);
+        Assert.assertEquals(obfuscated, output, obfuscated);
+    }
+
+    class ExtendedObfuscatorConverter extends ObfuscatorConverter {
+        @Override
+        public void start() {
+            super.start();
+        }
+
+        @Override
+        public List<String> getOptionList() {
+            return ImmutableList.of("address1");
+        }
+    }
 }
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
index 934b81e..ecc3cf3 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
@@ -59,6 +59,29 @@ public class TestPatternObfuscator extends ServerTestSuiteNoDB {
     }
 
     @Test(groups = "fast")
+    public void testXmlWithAttributesOnTheKey() throws Exception {
+        verify("<PayerInfo xsi:type=\"ebl:PayerInfoType\">\n" +
+               "<accountnumber xsi:type=\"ebl:EmailAddressType\">michaelhk@gmail.com</accountnumber>\n" +
+               "<PayerID xsi:type=\"ebl:UserIDType\">ZZS5TS7FD7MRA</PayerID>\n" +
+               "<PayerStatus xsi:type=\"ebl:PayPalUserStatusCodeType\">verified</PayerStatus>\n" +
+               "<PayerName xsi:type=\"ebl:PersonNameType\">\n" +
+               "<Salutation xmlns=\"urn:ebay:apis:eBLBaseComponents\"></Salutation>\n" +
+               "<ccFirstName xmlns=\"urn:ebay:apis:eBLBaseComponents\">Michael</ccFirstName>\n" +
+               "<MiddleName xmlns=\"urn:ebay:apis:eBLBaseComponents\"></MiddleName>\n" +
+               "<ccLastName xmlns=\"urn:ebay:apis:eBLBaseComponents\">Henrick</ccLastName>",
+
+               "<PayerInfo xsi:type=\"ebl:PayerInfoType\">\n" +
+               "<accountnumber xsi:type=\"ebl:EmailAddressType\">*******************</accountnumber>\n" +
+               "<PayerID xsi:type=\"ebl:UserIDType\">ZZS5TS7FD7MRA</PayerID>\n" +
+               "<PayerStatus xsi:type=\"ebl:PayPalUserStatusCodeType\">verified</PayerStatus>\n" +
+               "<PayerName xsi:type=\"ebl:PersonNameType\">\n" +
+               "<Salutation xmlns=\"urn:ebay:apis:eBLBaseComponents\"></Salutation>\n" +
+               "<ccFirstName xmlns=\"urn:ebay:apis:eBLBaseComponents\">*******</ccFirstName>\n" +
+               "<MiddleName xmlns=\"urn:ebay:apis:eBLBaseComponents\"></MiddleName>\n" +
+               "<ccLastName xmlns=\"urn:ebay:apis:eBLBaseComponents\">*******</ccLastName>");
+    }
+
+    @Test(groups = "fast")
     public void testCyberSource() throws Exception {
         verify("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index 4104d2d..6d90d6f 100644
--- a/profiles/killpay/pom.xml
+++ b/profiles/killpay/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killpay</artifactId>
diff --git a/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties b/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
index c8dde49..e03035f 100644
--- a/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,18 +1,155 @@
 ## Top level keys
 # general.notice = This notice should rarely, if ever, be used as everyone will see it
 
+### 0.19.x series ###
+
+# 0.19.7
+0.19.7.updates           =
+0.19.7.notices           = This is the latest dev release.
+0.19.7.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.7
+
+# 0.19.6
+0.19.6.updates           = 0.19.7
+0.19.6.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.6.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.6
+
+# 0.19.5
+0.19.5.updates           = 0.19.7
+0.19.5.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.5.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.5
+
+# 0.19.4
+0.19.4.updates           = 0.19.7
+0.19.4.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.4.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.4
+
+# 0.19.3
+0.19.3.updates           = 0.19.7
+0.19.3.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.3.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.3
+
+# 0.19.2
+0.19.2.updates           = 0.19.7
+0.19.2.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.2.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.2
+
+# 0.19.1
+0.19.1.updates           = 0.19.7
+0.19.1.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.1.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.1
+
+# 0.19.0
+0.19.0.updates           = 0.19.7
+0.19.0.notices           = We recommend upgrading to 0.19.7, our latest dev release.
+0.19.0.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.19.0
+
 ### 0.18.x series ###
 
+# 0.18.19
+0.18.19.updates           =
+0.18.19.notices           = This is the latest GA release.
+0.18.19.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.19
+
+# 0.18.18
+0.18.18.updates           = 0.18.19
+0.18.18.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.18.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.18
+
+# 0.18.17
+0.18.17.updates           = 0.18.19
+0.18.17.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.17.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.17
+
+# 0.18.16
+0.18.16.updates           = 0.18.19
+0.18.16.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.16.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.16
+
+# 0.18.15
+0.18.15.updates           = 0.18.19
+0.18.15.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.15.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.15
+
+# 0.18.14
+0.18.14.updates           = 0.18.19
+0.18.14.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.14.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.14
+
+# 0.18.13
+0.18.13.updates           = 0.18.19
+0.18.13.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.13.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.13
+
+# 0.18.12
+0.18.12.updates           = 0.18.19
+0.18.12.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.12.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.12
+
+# 0.18.11
+0.18.11.updates           = 0.18.19
+0.18.11.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.11.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.11
+
+# 0.18.10
+0.18.10.updates           = 0.18.19
+0.18.10.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.10.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.10
+
+# 0.18.9
+0.18.9.updates           = 0.18.19
+0.18.9.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.9.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.9
+
+# 0.18.8
+0.18.8.updates           = 0.18.19
+0.18.8.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.8.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.8
+
+# 0.18.7
+0.18.7.updates           = 0.18.19
+0.18.7.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.7.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.7
+
+# 0.18.6
+0.18.6.updates           = 0.18.19
+0.18.6.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.6.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.6
+
+# 0.18.5
+0.18.5.updates           = 0.18.19
+0.18.5.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.5.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.5
+
+# 0.18.4
+0.18.4.updates           = 0.18.19
+0.18.4.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.4.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.4
+
+# 0.18.3
+0.18.3.updates           = 0.18.19
+0.18.3.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.3.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.3
+
+# 0.18.2
+0.18.2.updates           = 0.18.19
+0.18.2.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.2.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.2
+
+# 0.18.1
+0.18.1.updates           = 0.18.19
+0.18.1.notices           = We recommend upgrading to 0.18.19, our latest GA release.
+0.18.1.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.1
+
 # 0.18.0
-0.18.0.updates           =
-0.18.0.notices           = This is the latest GA release.
+0.18.0.updates           = 0.18.19
+0.18.0.notices           = We recommend upgrading to 0.18.19, our latest GA release.
 0.18.0.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.18.0
 
 ### 0.17.x series ###
 
 # 0.17.8
 0.17.8.updates           =
-0.17.8.notices           = This is the latest dev release.
+0.17.8.notices           = We recommend upgrading to 0.18.19, our latest GA release.
 0.17.8.release-notes     = https://github.com/killbill/killbill/releases/tag/killbill-0.17.8
 
 # 0.17.7

profiles/pom.xml 2(+1 -1)

diff --git a/profiles/pom.xml b/profiles/pom.xml
index 9891298..355a94f 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles</artifactId>
diff --git a/subscription/pom.xml b/subscription/pom.xml
index e169cdf..53ae54d 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-subscription</artifactId>
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/TestSubscriptionDao.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/TestSubscriptionDao.java
index 99b6544..2fc6531 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/TestSubscriptionDao.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/TestSubscriptionDao.java
@@ -37,6 +37,9 @@ import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
 import org.killbill.billing.subscription.events.user.ApiEventBuilder;
 import org.killbill.billing.subscription.events.user.ApiEventCreate;
 import org.killbill.billing.util.UUIDs;
+import org.killbill.billing.util.glue.KillbillApiAopModule;
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.IDBI;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
@@ -166,4 +169,54 @@ public class TestSubscriptionDao extends SubscriptionTestSuiteWithEmbeddedDB {
 
 
     }
+
+    @Test(groups = "slow")
+    public void testDirtyFlag() throws SubscriptionBaseApiException {
+        // @BeforeMethod created the account
+        KillbillApiAopModule.resetDirtyDBFlag();
+
+        final IDBI dbiSpy = Mockito.spy(dbi);
+        final IDBI roDbiSpy = Mockito.spy(roDbi);
+        final SubscriptionDao subscriptionDao = new DefaultSubscriptionDao(dbiSpy,
+                                                                           roDbiSpy,
+                                                                           clock,
+                                                                           addonUtils,
+                                                                           notificationQueueService,
+                                                                           bus,
+                                                                           controlCacheDispatcher,
+                                                                           nonEntityDao,
+                                                                           internalCallContextFactory);
+        Mockito.verify(dbiSpy, Mockito.times(0)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(0)).open();
+
+        Assert.assertEquals(subscriptionDao.getSubscriptionBundleForAccount(accountId, internalCallContext).size(), 0);
+        Mockito.verify(dbiSpy, Mockito.times(0)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(1)).open();
+
+        Assert.assertEquals(subscriptionDao.getSubscriptionBundleForAccount(accountId, internalCallContext).size(), 0);
+        Mockito.verify(dbiSpy, Mockito.times(0)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(2)).open();
+
+        final String externalKey = UUID.randomUUID().toString();
+        final DateTime startDate = clock.getUTCNow();
+        final DateTime createdDate = startDate.plusSeconds(10);
+        final DefaultSubscriptionBaseBundle bundleDef = new DefaultSubscriptionBaseBundle(externalKey, accountId, startDate, startDate, createdDate, createdDate);
+        final SubscriptionBaseBundle bundle = subscriptionDao.createSubscriptionBundle(bundleDef, catalog, false, internalCallContext);
+        Mockito.verify(dbiSpy, Mockito.times(1)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(2)).open();
+
+        Assert.assertEquals(subscriptionDao.getSubscriptionBundleForAccount(accountId, internalCallContext).size(), 1);
+        Mockito.verify(dbiSpy, Mockito.times(2)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(2)).open();
+
+        Assert.assertEquals(subscriptionDao.getSubscriptionBundleForAccount(accountId, internalCallContext).size(), 1);
+        Mockito.verify(dbiSpy, Mockito.times(3)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(2)).open();
+
+        KillbillApiAopModule.resetDirtyDBFlag();
+
+        Assert.assertEquals(subscriptionDao.getSubscriptionBundleForAccount(accountId, internalCallContext).size(), 1);
+        Mockito.verify(dbiSpy, Mockito.times(3)).open();
+        Mockito.verify(roDbiSpy, Mockito.times(3)).open();
+    }
 }
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java
index fa1c027..2b5da99 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java
@@ -36,11 +36,14 @@ import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineAp
 import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
 import org.killbill.billing.subscription.api.user.TestSubscriptionHelper;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
 import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
 import org.killbill.billing.subscription.glue.TestDefaultSubscriptionModuleWithEmbeddedDB;
 import org.killbill.billing.util.config.definition.SubscriptionConfig;
 import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.bus.api.PersistentBus;
 import org.killbill.clock.ClockMock;
+import org.killbill.notificationq.api.NotificationQueueService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.annotations.AfterMethod;
@@ -65,10 +68,12 @@ public class SubscriptionTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteW
     protected SubscriptionBaseInternalApi subscriptionInternalApi;
     @Inject
     protected SubscriptionBaseTransferApi transferApi;
-
+    @Inject
+    protected PersistentBus bus;
     @Inject
     protected SubscriptionBaseTimelineApi repairApi;
-
+    @Inject
+    protected NotificationQueueService notificationQueueService;
     @Inject
     protected CatalogService catalogService;
     @Inject
@@ -79,7 +84,8 @@ public class SubscriptionTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteW
     protected ClockMock clock;
     @Inject
     protected BusService busService;
-
+    @Inject
+    protected AddonUtils addonUtils;
     @Inject
     protected TestSubscriptionHelper testUtil;
     @Inject

tenant/pom.xml 2(+1 -1)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index 4c3727a..c83a4c3 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-tenant</artifactId>
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
index eb3f5ba..e477023 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
@@ -92,6 +92,19 @@ public class DefaultTenantUserApi implements TenantUserApi {
         }
 
         try {
+            // Not transactional, but there is a db constraint on that column
+            if (data.getApiKey() != null && getTenantByApiKey(data.getApiKey()) != null) {
+                throw new TenantApiException(ErrorCode.TENANT_ALREADY_EXISTS, data.getExternalKey());
+            }
+        } catch (final RuntimeException e) {
+            if (e.getCause() instanceof IllegalStateException) {
+                // could happen exemption, stating that the key is not found
+            } else {
+                throw e;
+            }
+        }
+
+        try {
             tenantDao.create(new TenantModelDao(tenant), internalCallContextFactory.createInternalCallContextWithoutAccountRecordId(context));
         } catch (final TenantApiException e) {
             throw new TenantApiException(e, ErrorCode.TENANT_CREATION_FAILED);

usage/pom.xml 2(+1 -1)

diff --git a/usage/pom.xml b/usage/pom.xml
index b1cc6fe..c648b85 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-usage</artifactId>

util/pom.xml 2(+1 -1)

diff --git a/util/pom.xml b/util/pom.xml
index f4270c0..fe72a83 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.19.7-SNAPSHOT</version>
+        <version>0.19.10-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
index 7a01782..a6f6287 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
@@ -24,6 +24,7 @@ import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.glue.KillbillApiAopModule;
 import org.killbill.clock.Clock;
 import org.skife.jdbi.v2.Handle;
 import org.skife.jdbi.v2.IDBI;
@@ -82,12 +83,13 @@ public class EntitySqlDaoTransactionalJdbiWrapper {
 
     /**
      * @param <ReturnType>                   object type to return from the transaction
-     * @param ro                             whether to use the read-only connection
+     * @param requestedRO                    hint as whether to use the read-only connection
      * @param entitySqlDaoTransactionWrapper transaction to execute
      * @return result from the transaction fo type ReturnType
      */
-    public <ReturnType> ReturnType execute(final boolean ro, final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper) {
+    public <ReturnType> ReturnType execute(final boolean requestedRO, final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper) {
         final String debugInfo = logger.isDebugEnabled() ? getDebugInfo() : null;
+        final boolean ro = shouldUseRODBI(requestedRO, debugInfo);
         final String debugPrefix = ro ? "RO" : "RW";
 
         final Handle handle = ro ? roDbi.open() : dbi.open();
@@ -107,12 +109,33 @@ public class EntitySqlDaoTransactionalJdbiWrapper {
         }
     }
 
+    private boolean shouldUseRODBI(final boolean requestedRO, final String debugInfo) {
+        if (!requestedRO) {
+            KillbillApiAopModule.setDirtyDBFlag();
+            logger.debug("[RW] Dirty flag set, transaction: {}", debugInfo);
+            return false;
+        } else {
+            if (KillbillApiAopModule.getDirtyDBFlag()) {
+                // Redirect to the rw instance, to work-around any replication delay
+                logger.debug("[RW] RO DBI handle requested, but dirty flag set, transaction: {}", debugInfo);
+                return false;
+            } else {
+                return true;
+            }
+        }
+    }
+
     //
     // This is only used in the pagination APIs when streaming results. We want to keep the connection open, and also there is no need
     // to send bus events, record notifications where we need to keep the Connection through the jDBI Handle.
     //
     public <M extends EntityModelDao<E>, E extends Entity, T extends EntitySqlDao<M, E>> T onDemandForStreamingResults(final Class<T> sqlObjectType) {
-        return roDbi.onDemand(sqlObjectType);
+        final String debugInfo = logger.isDebugEnabled() ? getDebugInfo() : null;
+        if (shouldUseRODBI(true, debugInfo)) {
+            return roDbi.onDemand(sqlObjectType);
+        } else {
+            return dbi.onDemand(sqlObjectType);
+        }
     }
 
     /**
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java
index 9176282..c751f83 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java
@@ -35,6 +35,12 @@ import com.google.inject.matcher.Matchers;
 public class KillbillApiAopModule extends AbstractModule {
 
     private static final Logger logger = LoggerFactory.getLogger(KillbillApiAopModule.class);
+    private static final ThreadLocal<Boolean> perThreadDirtyDBFlag = new ThreadLocal<Boolean>();
+
+    static {
+        // Set an initial value
+        resetDirtyDBFlag();
+    }
 
     @Override
     protected void configure() {
@@ -53,15 +59,31 @@ public class KillbillApiAopModule extends AbstractModule {
             return prof.executeWithProfiling(ProfilingFeatureType.API, invocation.getMethod().getName(), new WithProfilingCallback() {
                 @Override
                 public Object execute() throws Throwable {
-                    logger.debug("Entering API call {}, arguments: {}", invocation.getMethod(), invocation.getArguments());
-                    final Object proceed = invocation.proceed();
-                    logger.debug("Exiting  API call {}, returning: {}", invocation.getMethod(), proceed);
-                    return proceed;
+                    try {
+                        logger.debug("Entering API call {}, arguments: {}", invocation.getMethod(), invocation.getArguments());
+                        final Object proceed = invocation.proceed();
+                        logger.debug("Exiting  API call {}, returning: {}", invocation.getMethod(), proceed);
+                        return proceed;
+                    } finally {
+                        resetDirtyDBFlag();
+                    }
                 }
             });
         }
     }
 
+    public static void setDirtyDBFlag() {
+        perThreadDirtyDBFlag.set(true);
+    }
+
+    public static void resetDirtyDBFlag() {
+        perThreadDirtyDBFlag.set(false);
+    }
+
+    public static Boolean getDirtyDBFlag() {
+        return perThreadDirtyDBFlag.get() == Boolean.TRUE;
+    }
+
     private static final Matcher<Method> SYNTHETIC_METHOD_MATCHER = new Matcher<Method>() {
         @Override
         public boolean matches(final Method method) {
diff --git a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg
index 3b16041..1328fc6 100644
--- a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg
@@ -67,6 +67,7 @@ from <tableName()> t
 where t.is_active
 and <idField("t.")> in (<ids>)
 <AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
 ;
 >>
 
diff --git a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
index cf6ac03..7af9782 100644
--- a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
@@ -44,6 +44,7 @@ where t.is_active
 and t.object_id = :objectId
 and t.object_type = :objectType
 <AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
 ;
 >>
 
@@ -55,6 +56,7 @@ where 1 = 1
 and t.object_id = :objectId
 and t.object_type = :objectType
 <AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
 ;
 >>
 
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
index 719cff7..37e3f80 100644
--- a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
@@ -23,6 +23,7 @@ import javax.inject.Named;
 import javax.sql.DataSource;
 
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.glue.KillbillApiAopModule;
 import org.killbill.commons.embeddeddb.EmbeddedDB;
 import org.skife.jdbi.v2.IDBI;
 import org.slf4j.Logger;
@@ -63,6 +64,7 @@ public class GuicyKillbillTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite
     public void beforeMethod() throws Exception {
         cleanupAllTables();
         controlCacheDispatcher.clearAll();
+        KillbillApiAopModule.resetDirtyDBFlag();
     }
 
     protected void cleanupAllTables() {