killbill-memoizeit

Changes

NEWS 3(+3 -0)

pom.xml 2(+1 -1)

Details

diff --git a/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java b/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java
index 865f58f..2d43bcd 100644
--- a/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java
+++ b/api/src/main/java/org/killbill/billing/callcontext/DefaultCallContext.java
@@ -20,9 +20,9 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 
-import org.killbill.clock.Clock;
 import org.killbill.billing.util.callcontext.CallOrigin;
 import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
 
 public class DefaultCallContext extends CallContextBase {
 
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 33c4db0..a896c01 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -93,11 +93,6 @@ public interface SubscriptionBaseInternalApi {
 
     public void updateExternalKey(UUID bundleId, String newExternalKey, InternalCallContext context);
 
-    public Iterable<DateTime> getFutureNotificationsForAccount(InternalCallContext context);
-
-    public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext);
-
-
     public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException;
 
     public int getDefaultBillCycleDayLocal(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription, final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier, final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java
new file mode 100644
index 0000000..559a626
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationDryRunInvoice.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+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.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.invoice.api.DryRunType;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import static com.tc.util.Assert.fail;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+public class TestIntegrationDryRunInvoice extends TestIntegrationBase {
+
+    private static final DryRunArguments DRY_RUN_UPCOMING_INVOICE_ARG = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE);
+    private static final DryRunArguments DRY_RUN_TARGET_DATE_ARG = new TestDryRunArguments(DryRunType.TARGET_DATE);
+
+    //
+    // Basic test with one subscription that verifies the behavior of using invoice dryRun api with no date
+    //
+    @Test(groups = "slow")
+    public void testDryRunWithNoTargetDate() throws Exception {
+        final int billingDay = 14;
+        final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);
+        // set clock to the initial start date
+        clock.setTime(initialCreationDate);
+
+        log.info("Beginning test with BCD of " + billingDay);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+        int invoiceNumber = 1;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE, NextEvent.BLOCK NextEvent.INVOICE
+        //
+        DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        DefaultSubscriptionBase subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+        invoiceChecker.checkChargedThroughDate(subscription.getId(), clock.getUTCToday(), callContext);
+
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 6, 14), new LocalDate(2015, 7, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+
+        // This will verify that the upcoming Phase is found and the invoice is generated at the right date, with correct items
+        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        // Move through time and verify we get the same invoice
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        // This will verify that the upcoming invoice notification is found and the invoice is generated at the right date, with correct items
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 7, 14), new LocalDate(2015, 8, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        // Move through time and verify we get the same invoice
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        // One more time, this will verify that the upcoming invoice notification is found and the invoice is generated at the right date, with correct items
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 8, 14), new LocalDate(2015, 9, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+    }
+
+    //
+    // More sophisticated test with two non aligned subscriptions that verifies the behavior of using invoice dryRun api with no date
+    // - The first subscription is an annual (SUBSCRIPTION aligned) whose billingDate is the first (we start on Jan 2nd to take into account the 30 days trial)
+    // - The second and third subscriptions are monthly (ACCOUNT aligned) whose billingDate are the 14 (we start on Dec 15 to also take into account the 30 days trial)
+    //
+    // The test verifies that the dryRun invoice with supplied date will always take into account the 'closest' invoice that should be generated
+    //
+    @Test(groups = "slow")
+    public void testDryRunWithNoTargetDateAndMultipleNonAlignedSubscriptions() throws Exception {
+        // Set in such a way that annual billing date will be the 1st
+        final DateTime initialCreationDate = new DateTime(2014, 1, 2, 0, 0, 0, 0, testTimeZone);
+        clock.setTime(initialCreationDate);
+
+        // billing date for the monthly
+        final int billingDay = 14;
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+        int invoiceNumber = 1;
+
+        // Create ANNUAL BP
+        DefaultEntitlement baseEntitlementAnnual = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        DefaultSubscriptionBase subscriptionAnnual = subscriptionDataFromSubscription(baseEntitlementAnnual.getSubscriptionBase());
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+        invoiceChecker.checkChargedThroughDate(subscriptionAnnual.getId(), clock.getUTCToday(), callContext);
+
+        // Verify next dryRun invoice and then move the clock to that date to also verify real invoice is the same
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2014, 2, 1), new LocalDate(2015, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+
+        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // 2014-2-1
+        clock.addDays(30);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        // Since we only have one subscription next dryRun will show the annual
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        // 2014-12-15
+        final DateTime secondSubscriptionCreationDate = new DateTime(2014, 12, 15, 0, 0, 0, 0, testTimeZone);
+        clock.setTime(secondSubscriptionCreationDate);
+
+        // Create the first monthly
+        DefaultEntitlement baseEntitlementMonthly1 = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey1", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        DefaultSubscriptionBase subscriptionMonthly1 = subscriptionDataFromSubscription(baseEntitlementMonthly1.getSubscriptionBase());
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, new ExpectedInvoiceItemCheck(secondSubscriptionCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+        invoiceChecker.checkChargedThroughDate(subscriptionMonthly1.getId(), clock.getUTCToday(), callContext);
+
+        // Create the second monthly
+        DefaultEntitlement baseEntitlementMonthly2 = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey2", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        DefaultSubscriptionBase subscriptionMonthly2 = subscriptionDataFromSubscription(baseEntitlementMonthly2.getSubscriptionBase());
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, new ExpectedInvoiceItemCheck(secondSubscriptionCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
+        invoiceChecker.checkChargedThroughDate(subscriptionMonthly2.getId(), clock.getUTCToday(), callContext);
+
+        // Verify next dryRun invoice and then move the clock to that date to also verify real invoice is the same
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 1, 14), new LocalDate(2015, 2, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 1, 14), new LocalDate(2015, 2, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // 2015-1-14
+        clock.addDays(30);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        // We test first the next expected invoice for a specific subscription: We can see the targetDate is 2015-2-14 and not 2015-2-1
+        final DryRunArguments dryRunUpcomingInvoiceWithFilterArg1 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly1.getId(), null, null, null);
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg1, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        final DryRunArguments dryRunUpcomingInvoiceWithFilterArg2 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly2.getId(), null, null, null);
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg2, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        // Then we test first the next expected invoice at the account level
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // 2015-2-1
+        clock.addDays(18);
+        assertListenerStatus();
+        invoiceChecker.checkInvoice(account.getId(), invoiceNumber++, callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("29.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+    }
+
+    @Test(groups = "slow")
+    public void testDryRunWithPendingSubscription() throws Exception {
+
+        final LocalDate initialDate = new LocalDate(2017, 4, 1);
+        clock.setDay(initialDate);
+
+        // Create account with non BCD to force junction BCD logic to activate
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        final LocalDate futureDate = new LocalDate(2017, 5, 1);
+
+        // No CREATE event as this is set in the future
+        final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureDate, futureDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
+        assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
+        assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureDate), 0);
+        assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+        assertListenerStatus();
+
+        // Generate a dryRun invoice on the billing startDate
+        final Invoice dryRunInvoice1 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, DRY_RUN_TARGET_DATE_ARG, callContext);
+        assertEquals(dryRunInvoice1.getInvoiceItems().size(), 1);
+        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getStartDate(), futureDate);
+        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getPlanName(), "shotgun-annual");
+
+        // Generate a dryRun invoice with a plan change
+        final DryRunArguments dryRunSubscriptionActionArg = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null,
+                                                                                    SubscriptionEventType.CHANGE, createdEntitlement.getId(), createdEntitlement.getBundleId(), futureDate, BillingActionPolicy.IMMEDIATE);
+
+        // First one day prior subscription starts
+        try {
+            invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate.minusDays(1), dryRunSubscriptionActionArg, callContext);
+            fail("Should fail to trigger dryRun invoice prior subscription starts");
+        } catch (final InvoiceApiException e) {
+            assertEquals(e.getCode(), ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
+        }
+
+        // Second, on the startDate
+        final Invoice dryRunInvoice2 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, dryRunSubscriptionActionArg, callContext);
+        assertEquals(dryRunInvoice2.getInvoiceItems().size(), 1);
+        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getStartDate(), futureDate);
+        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getPlanName(), "pistol-monthly");
+
+        // Check BCD is not yet set
+        final Account refreshedAccount1 = accountUserApi.getAccountById(account.getId(), callContext);
+        assertEquals(refreshedAccount1.getBillCycleDayLocal(), new Integer(0));
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE);
+        final Invoice realInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, null, callContext);
+        assertListenerStatus();
+
+        assertEquals(realInvoice.getInvoiceItems().size(), 1);
+        assertEquals(realInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+        assertEquals(realInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+        assertEquals(realInvoice.getInvoiceItems().get(0).getStartDate(), futureDate);
+        assertEquals(realInvoice.getInvoiceItems().get(0).getPlanName(), "shotgun-annual");
+
+        // Check BCD is now set
+        final Account refreshedAccount2 = accountUserApi.getAccountById(account.getId(), callContext);
+        assertEquals(refreshedAccount2.getBillCycleDayLocal(), new Integer(31));
+
+        // Move clock past startDate to check nothing happens
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
+        clock.addDays(31);
+        assertListenerStatus();
+
+        // Move clock after PHASE event
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addMonths(12);
+        assertListenerStatus();
+    }
+
+    @Test(groups = "slow")
+    public void testDryRunWithPendingCancelledSubscription() throws Exception {
+
+        final LocalDate initialDate = new LocalDate(2017, 4, 1);
+        clock.setDay(initialDate);
+
+        // Create account with non BCD to force junction BCD logic to activate
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial", null);
+
+        final LocalDate futureStartDate = new LocalDate(2017, 5, 1);
+
+        // No CREATE event as this is set in the future
+        final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureStartDate, futureStartDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
+        assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
+        assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureStartDate), 0);
+        assertEquals(createdEntitlement.getEffectiveEndDate(), null);
+        assertListenerStatus();
+
+        // Generate an invoice using a future targetDate
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        final Invoice firstInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureStartDate, null, callContext);
+        assertListenerStatus();
+
+        assertEquals(firstInvoice.getInvoiceItems().size(), 1);
+        assertEquals(firstInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+        assertEquals(firstInvoice.getInvoiceItems().get(0).getAmount().compareTo(new BigDecimal("19.95")), 0);
+        assertEquals(firstInvoice.getInvoiceItems().get(0).getStartDate(), futureStartDate);
+        assertEquals(firstInvoice.getInvoiceItems().get(0).getPlanName(), "pistol-monthly-notrial");
+
+        // Cancel subscription on its pending startDate
+        createdEntitlement.cancelEntitlementWithDate(futureStartDate, true, ImmutableList.<PluginProperty>of(), callContext);
+
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 4, 1), new LocalDate(2017, 4, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("19.95")));
+
+        final Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2017, 5, 1));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        // Move to startDate/cancel Date
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.NULL_INVOICE, NextEvent.INVOICE);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        assertEquals(invoices.size(), 2);
+
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("19.95")));
+        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
+    }
+
+
+    @Test(groups = "slow", description = "See https://github.com/killbill/killbill/issues/774")
+    public void testDryRunTargetDateWithIntermediateInvoice() throws Exception {
+        final DateTime initialCreationDate = new DateTime(2014, 1, 2, 0, 0, 0, 0, testTimeZone);
+        clock.setTime(initialCreationDate);
+
+        // billing date for the monthly
+        final int billingDay = 14;
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+
+        // Create first ANNUAL BP -> BCD = 1
+        createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual1", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+        // 2014-1-4
+        clock.addDays(2);
+        // Create second ANNUAL BP -> BCD = 3
+        createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual2", "Pistol", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+        // 2014-2-1
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(28);
+        assertListenerStatus();
+
+        // 2014-2-3
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(2);
+        assertListenerStatus();
+
+        // 2014-12-15
+        final DateTime monthlySubscriptionCreationDate = new DateTime(2014, 12, 15, 0, 0, 0, 0, testTimeZone);
+        clock.setTime(monthlySubscriptionCreationDate);
+
+        // Create the monthly
+        createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+        // 2015-1-14
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+
+        // At this point (2015-1-14), we have 3 pending invoice notifications:
+        //
+        // - The 1st ANNUAL on 2015-2-1
+        // - The 2nd ANNUAL on 2015-2-3
+        // - The MONTHLY on 2015-2-14
+        //
+        // 1. We verify that a DryRunType.TARGET_DATE for 2015-2-14 leads to an invoice that **only** contains the MONTHLY item (fix for #774)
+        //
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(2015, 2, 14), DRY_RUN_TARGET_DATE_ARG, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        // 2. We verify that a DryRunType.TARGET_DATE for 2015-2-3 leads to an invoice that **only** contains the 2nd ANNUAL item (fix for #774)
+        //
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 3), new LocalDate(2016, 2, 3), InvoiceItemType.RECURRING, new BigDecimal("199.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), new LocalDate(2015, 2, 3), DRY_RUN_TARGET_DATE_ARG, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 3));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        // 3. We verify that UPCOMING_INVOICE leads to next invoice fo the ANNUAL
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 1));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+    }
+
+    @Test(groups = "slow")
+    public void testDryRunWithAOs() throws Exception {
+        final LocalDate initialDate = new LocalDate(2017, 12, 1);
+        clock.setDay(initialDate);
+
+        // Create account with non BCD to force junction BCD logic to activate
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
+
+        // No CREATE event as this is set in the future
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial", null);
+        final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, null, null, false, true, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        final DefaultEntitlement aoEntitlement = addAOEntitlementAndCheckForCompletion(baseEntitlement.getBundleId(), "Refurbish-Maintenance", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2018, 2, 1), new LocalDate(2018, 3, 1), InvoiceItemType.RECURRING, new BigDecimal("19.95")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2018, 2, 1), new LocalDate(2018, 3, 1), InvoiceItemType.RECURRING, new BigDecimal("199.95")));
+
+        // Specify AO subscriptionId filter
+        final DryRunArguments dryRunUpcomingInvoiceWithFilterArg1 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, aoEntitlement.getId(), null, null, null);
+        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg1, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2018, 2, 1));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        // Specify BP subscriptionId filter
+        final DryRunArguments dryRunUpcomingInvoiceWithFilterArg2 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, baseEntitlement.getId(), null, null, null);
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg2, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2018, 2, 1));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+        // Specify bundleId filter
+        final DryRunArguments dryRunUpcomingInvoiceWithFilterArg3 = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, null, baseEntitlement.getBundleId(), null, null);
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunUpcomingInvoiceWithFilterArg3, callContext);
+        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2018, 2, 1));
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+
+    }
+
+    @Test(groups = "slow")
+    public void testDryRunWithUpcomingSubscriptionEvents() throws Exception {
+
+        final DateTime initialDate = new DateTime(2017, 11, 1, 0, 3, 42, 0, testTimeZone);
+
+        // set clock to the initial start date
+        clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+
+        final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        assertNotNull(baseEntitlement);
+
+        // Verify the next invoice based on the PHASE event is correctly seen in the dryRun scenario
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 1), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 1), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+
+        // Future pause the subscription
+        LocalDate effectivePauseDate = new LocalDate(2017, 12, 15);
+        entitlementApi.pause(baseEntitlement.getBundleId(), effectivePauseDate, ImmutableList.<PluginProperty>of(), callContext);
+
+        // Verify the next invoice based on the PAUSE event is correctly seen in the dryRun scenario
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2018, 1, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-137.07")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 1), new LocalDate(2017, 12, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("137.07")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        // Hit the pause effective date 2017-12-15)
+        busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+        clock.addDays(14);
+        assertListenerStatus();
+
+        // Unfortunately we can't reuse *exactly the items from the dryRun invoice because the effective date for the CBA is set with current date.
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2018, 1, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-137.07")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2017, 12, 15), InvoiceItemType.CBA_ADJ, new BigDecimal("137.07")));
+        invoiceChecker.checkInvoice(account.getId(), 3, callContext, expectedInvoices);
+        expectedInvoices.clear();
+
+        // Future resume the subscription
+        LocalDate effectiveResumeDate = new LocalDate(2017, 12, 25);
+        entitlementApi.resume(baseEntitlement.getBundleId(), effectiveResumeDate, ImmutableList.<PluginProperty>of(), callContext);
+
+        // Verify the next invoice based on the RESUME event is correctly seen in the dryRun scenario
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 25), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("56.44")));
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 15), new LocalDate(2017, 12, 15), InvoiceItemType.CBA_ADJ, new BigDecimal("-56.44")));
+        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, DRY_RUN_UPCOMING_INVOICE_ARG, callContext);
+        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
+        expectedInvoices.clear();
+
+        busHandler.pushExpectedEvents(NextEvent.BLOCK, NextEvent.INVOICE);
+        clock.addDays(10);
+        assertListenerStatus();
+
+        invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 25), new LocalDate(2018, 1, 1), InvoiceItemType.RECURRING, new BigDecimal("56.44")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2017, 12, 25), new LocalDate(2017, 12, 25), InvoiceItemType.CBA_ADJ, new BigDecimal("-56.44")));
+
+    }
+
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
index 8155e6d..c9c9157 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoice.java
@@ -25,26 +25,19 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
-import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
 import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
-import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.catalog.api.UsagePriceOverride;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.entitlement.api.Entitlement;
-import org.killbill.billing.entitlement.api.SubscriptionEventType;
-import org.killbill.billing.invoice.api.DryRunArguments;
-import org.killbill.billing.invoice.api.DryRunType;
 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;
@@ -55,170 +48,11 @@ import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
 
-import static com.tc.util.Assert.fail;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
 public class TestIntegrationInvoice extends TestIntegrationBase {
 
-    //
-    // Basic test with one subscription that verifies the behavior of using invoice dryRun api with no date
-    //
-    @Test(groups = "slow")
-    public void testDryRunWithNoTargetDate() throws Exception {
-        final int billingDay = 14;
-        final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);
-        // set clock to the initial start date
-        clock.setTime(initialCreationDate);
-
-        log.info("Beginning test with BCD of " + billingDay);
-        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
-
-        int invoiceItemCount = 1;
-
-        //
-        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE, NextEvent.BLOCK NextEvent.INVOICE
-        //
-        DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
-        DefaultSubscriptionBase subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
-        invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
-        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
-        invoiceChecker.checkChargedThroughDate(subscription.getId(), clock.getUTCToday(), callContext);
-
-        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 6, 14), new LocalDate(2015, 7, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-
-        // This will verify that the upcoming Phase is found and the invoice is generated at the right date, with correct items
-        DryRunArguments dryRun = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE);
-        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
-        // Move through time and verify we get the same invoice
-        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        clock.addDays(30);
-        assertListenerStatus();
-        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, expectedInvoices);
-        expectedInvoices.clear();
-
-        // This will verify that the upcoming invoice notification is found and the invoice is generated at the right date, with correct items
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 7, 14), new LocalDate(2015, 8, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
-
-        // Move through time and verify we get the same invoice
-        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        clock.addMonths(1);
-        assertListenerStatus();
-        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
-        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, expectedInvoices);
-        expectedInvoices.clear();
-
-        // One more time, this will verify that the upcoming invoice notification is found and the invoice is generated at the right date, with correct items
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 8, 14), new LocalDate(2015, 9, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-    }
-
-    //
-    // More sophisticated test with two non aligned subscriptions that verifies the behavior of using invoice dryRun api with no date
-    // - The first subscription is an annual (SUBSCRIPTION aligned) whose billingDate is the first (we start on Jan 2nd to take into account the 30 days trial)
-    // - The second subscription is a monthly (ACCOUNT aligned) whose billingDate is the 14 (we start on Dec 15 to also take into account the 30 days trial)
-    //
-    // The test verifies that the dryRun invoice with supplied date will always take into account the 'closest' invoice that should be generated
-    //
-    @Test(groups = "slow")
-    public void testDryRunWithNoTargetDateAndMultipleNonAlignedSubscriptions() throws Exception {
-        // Set in such a way that annual billing date will be the 1st
-        final DateTime initialCreationDate = new DateTime(2014, 1, 2, 0, 0, 0, 0, testTimeZone);
-        clock.setTime(initialCreationDate);
-
-        // billing date for the monthly
-        final int billingDay = 14;
-
-        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
-
-        int invoiceItemCount = 1;
-
-        // Create ANNUAL BP
-        DefaultEntitlement baseEntitlementAnnual = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKeyAnnual", "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
-        DefaultSubscriptionBase subscriptionAnnual = subscriptionDataFromSubscription(baseEntitlementAnnual.getSubscriptionBase());
-        invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(initialCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
-        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
-        invoiceChecker.checkChargedThroughDate(subscriptionAnnual.getId(), clock.getUTCToday(), callContext);
-
-
-        // Verify next dryRun invoice and then move the clock to that date to also verify real invoice is the same
-        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2014, 2, 1), new LocalDate(2015, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
-
-        DryRunArguments dryRun = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE);
-        Invoice dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
-        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        // 2014-2-1
-        clock.addDays(30);
-        assertListenerStatus();
-        invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
-        expectedInvoices.clear();
-
-        // Since we only have one subscription next dryRun will show the annual
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-        expectedInvoices.clear();
-
-        // 2014-12-15
-        final DateTime secondSubscriptionCreationDate = new DateTime(2014, 12, 15, 0, 0, 0, 0, testTimeZone);
-        clock.setTime(secondSubscriptionCreationDate);
-
-        // Create the monthly
-        DefaultEntitlement baseEntitlementMonthly = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
-        DefaultSubscriptionBase subscriptionMonthly = subscriptionDataFromSubscription(baseEntitlementMonthly.getSubscriptionBase());
-        invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, new ExpectedInvoiceItemCheck(secondSubscriptionCreationDate.toLocalDate(), null, InvoiceItemType.FIXED, new BigDecimal("0")));
-        // No end date for the trial item (fixed price of zero), and CTD should be today (i.e. when the trial started)
-        invoiceChecker.checkChargedThroughDate(subscriptionMonthly.getId(), clock.getUTCToday(), callContext);
-
-        // Verify next dryRun invoice and then move the clock to that date to also verify real invoice is the same
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 1, 14), new LocalDate(2015, 2, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
-        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        // 2015-1-14
-        clock.addDays(30);
-        assertListenerStatus();
-        invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
-        expectedInvoices.clear();
-
-
-        // We test first the next expected invoice for a specific subscription: We can see the targetDate is 2015-2-14 and not 2015-2-1
-        final DryRunArguments dryRunWIthSubscription = new TestDryRunArguments(DryRunType.UPCOMING_INVOICE, null, null, null, null, null, null, subscriptionMonthly.getId(), null, null, null);
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRunWIthSubscription, callContext);
-        assertEquals(dryRunInvoice.getTargetDate(), new LocalDate(2015, 2, 14));
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-        expectedInvoices.clear();
-
-        // Then we test first the next expected invoice at the account level
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 1), new LocalDate(2016, 2, 1), InvoiceItemType.RECURRING, new BigDecimal("2399.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-
-        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        // 2015-2-1
-        clock.addDays(18);
-        assertListenerStatus();
-        invoiceChecker.checkInvoice(account.getId(), invoiceItemCount++, callContext, expectedInvoices);
-        expectedInvoices.clear();
-
-        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2015, 2, 14), new LocalDate(2015, 3, 14), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-        dryRunInvoice = invoiceUserApi.triggerInvoiceGeneration(account.getId(), null, dryRun, callContext);
-        invoiceChecker.checkInvoiceNoAudits(dryRunInvoice, expectedInvoices);
-    }
-
     @Test(groups = "slow")
     public void testApplyCreditOnExistingBalance() throws Exception {
         final DateTime initialCreationDate = new DateTime(2015, 5, 15, 0, 0, 0, 0, testTimeZone);
@@ -268,7 +102,6 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
         final BigDecimal accountBalance3 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
         assertTrue(accountBalance3.compareTo(BigDecimal.ZERO) == 0);
 
-
         final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, false, ImmutableList.<PluginProperty>of(), callContext);
         assertEquals(payments.size(), 1);
 
@@ -289,7 +122,7 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
 
         int invoiceItemCount = 1;
 
-        DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY,  NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
         DefaultSubscriptionBase subscription = subscriptionDataFromSubscription(baseEntitlement.getSubscriptionBase());
 
         final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
@@ -336,8 +169,7 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
         assertEquals(accountPayments.get(2).getPurchasedAmount(), new BigDecimal("10.00"));
     }
 
-
-    @Test(groups = "slow", description= "See https://github.com/killbill/killbill/issues/127#issuecomment-292445089")
+    @Test(groups = "slow", description = "See https://github.com/killbill/killbill/issues/127#issuecomment-292445089")
     public void testIntegrationWithBCDLargerThanEndMonth() throws Exception {
 
         final int billingDay = 31;
@@ -369,141 +201,6 @@ public class TestIntegrationInvoice extends TestIntegrationBase {
         assertListenerStatus();
     }
 
-    @Test(groups = "slow")
-    public void testDryRunWithPendingSubscription() throws Exception {
-
-        final LocalDate initialDate = new LocalDate(2017, 4, 1);
-        clock.setDay(initialDate);
-
-        // Create account with non BCD to force junction BCD logic to activate
-        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
-
-        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null);
-
-        final LocalDate futureDate = new LocalDate(2017, 5, 1);
-
-        // No CREATE event as this is set in the future
-        final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureDate, futureDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
-        assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
-        assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureDate), 0);
-        assertEquals(createdEntitlement.getEffectiveEndDate(), null);
-        assertListenerStatus();
-
-        // Generate a dryRun invoice on the billing startDate
-        final DryRunArguments dryRunArguments1 = new TestDryRunArguments(DryRunType.TARGET_DATE);
-        final Invoice dryRunInvoice1 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, dryRunArguments1, callContext);
-        assertEquals(dryRunInvoice1.getInvoiceItems().size(), 1);
-        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getInvoiceItemType(),  InvoiceItemType.FIXED);
-        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
-        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getStartDate(),  futureDate);
-        assertEquals(dryRunInvoice1.getInvoiceItems().get(0).getPlanName(),  "shotgun-annual");
-
-
-        // Generate a dryRun invoice with a plan change
-        final DryRunArguments dryRunArguments = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null,
-                SubscriptionEventType.CHANGE,  createdEntitlement.getId(), createdEntitlement.getBundleId(), futureDate, BillingActionPolicy.IMMEDIATE);
-
-        // First one day prior subscription starts
-        try {
-            invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate.minusDays(1), dryRunArguments, callContext);
-            fail("Should fail to trigger dryRun invoice prior subscription starts");
-        } catch (final InvoiceApiException e) {
-            assertEquals(e.getCode(),ErrorCode.INVOICE_NOTHING_TO_DO.getCode());
-        }
-
-        // Second, on the startDate
-        final Invoice dryRunInvoice2 = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, dryRunArguments, callContext);
-        assertEquals(dryRunInvoice2.getInvoiceItems().size(), 1);
-        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getInvoiceItemType(),  InvoiceItemType.FIXED);
-        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
-        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getStartDate(),  futureDate);
-        assertEquals(dryRunInvoice2.getInvoiceItems().get(0).getPlanName(),  "pistol-monthly");
-
-
-        // Check BCD is not yet set
-        final Account refreshedAccount1 = accountUserApi.getAccountById(account.getId(), callContext);
-        assertEquals(refreshedAccount1.getBillCycleDayLocal(), new Integer(0));
-
-
-        busHandler.pushExpectedEvents(NextEvent.INVOICE);
-        final Invoice realInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, null, callContext);
-        assertListenerStatus();
-
-        assertEquals(realInvoice.getInvoiceItems().size(), 1);
-        assertEquals(realInvoice.getInvoiceItems().get(0).getInvoiceItemType(),  InvoiceItemType.FIXED);
-        assertEquals(realInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
-        assertEquals(realInvoice.getInvoiceItems().get(0).getStartDate(),  futureDate);
-        assertEquals(realInvoice.getInvoiceItems().get(0).getPlanName(),  "shotgun-annual");
-
-        // Check BCD is now set
-        final Account refreshedAccount2 = accountUserApi.getAccountById(account.getId(), callContext);
-        assertEquals(refreshedAccount2.getBillCycleDayLocal(), new Integer(31));
-
-
-        // Move clock past startDate to check nothing happens
-        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.NULL_INVOICE);
-        clock.addDays(31);
-        assertListenerStatus();
-
-        // Move clock after PHASE event
-        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
-        clock.addMonths(12);
-        assertListenerStatus();
-    }
-
-
-    @Test(groups = "slow")
-    public void testDryRunWithPendingCancelledSubscription() throws Exception {
-
-        final LocalDate initialDate = new LocalDate(2017, 4, 1);
-        clock.setDay(initialDate);
-
-        // Create account with non BCD to force junction BCD logic to activate
-        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
-
-        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial", null);
-
-        final LocalDate futureDate = new LocalDate(2017, 5, 1);
-
-        // No CREATE event as this is set in the future
-        final Entitlement createdEntitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, futureDate, futureDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
-        assertEquals(createdEntitlement.getState(), Entitlement.EntitlementState.PENDING);
-        assertEquals(createdEntitlement.getEffectiveStartDate().compareTo(futureDate), 0);
-        assertEquals(createdEntitlement.getEffectiveEndDate(), null);
-        assertListenerStatus();
-
-        // Generate an invoice using a future targetDate
-        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
-        final Invoice firstInvoice = invoiceUserApi.triggerInvoiceGeneration(createdEntitlement.getAccountId(), futureDate, null, callContext);
-        assertListenerStatus();
-
-        assertEquals(firstInvoice.getInvoiceItems().size(), 1);
-        assertEquals(firstInvoice.getInvoiceItems().get(0).getInvoiceItemType(),  InvoiceItemType.RECURRING);
-        assertEquals(firstInvoice.getInvoiceItems().get(0).getAmount().compareTo(new BigDecimal("19.95")), 0);
-        assertEquals(firstInvoice.getInvoiceItems().get(0).getStartDate(),  futureDate);
-        assertEquals(firstInvoice.getInvoiceItems().get(0).getPlanName(),  "pistol-monthly-notrial");
-
-
-        // Cancel subscription on its pending startDate
-        createdEntitlement.cancelEntitlementWithDate(futureDate, true, ImmutableList.<PluginProperty>of(), callContext);
-
-        // Move to startDate/cancel Date
-        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.NULL_INVOICE, NextEvent.INVOICE);
-        clock.addMonths(1);
-        assertListenerStatus();
-
-        final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
-        assertEquals(invoices.size(), 2);
-
-        final List<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
-                new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 6, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.95")),
-                new ExpectedInvoiceItemCheck(new LocalDate(2017, 5, 1), new LocalDate(2017, 5, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("19.95")));
-        invoiceChecker.checkInvoice(invoices.get(1).getId(), callContext, toBeChecked);
-
-
-
-
-    }
 
     @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/783")
     public void testIntegrationWithRecurringFreePlan() throws Exception {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index a755f9c..2ed5d58 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -41,7 +41,6 @@ import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.api.InvoiceStatus;
@@ -51,7 +50,6 @@ import org.killbill.billing.invoice.dao.InvoiceModelDao;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
-import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
@@ -722,7 +720,7 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         newItems.add(recurring3);
         newItems.add(repair3);
         shellInvoice.addInvoiceItems(newItems);
-        invoiceDao.createInvoice(shellInvoice, new FutureAccountNotifications(new HashMap<UUID, List<SubscriptionNotification>>()), internalCallContext);
+        invoiceDao.createInvoice(shellInvoice, new FutureAccountNotifications(), internalCallContext);
 
 
         // Move ahead one month, verify nothing from previous data was generated
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 33d55ad..f060623 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -26,6 +26,7 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
@@ -40,7 +41,6 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.entity.EntityPersistenceException;
 import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.InvoicePluginDispatcher;
 import org.killbill.billing.invoice.api.DefaultInvoicePaymentErrorEvent;
 import org.killbill.billing.invoice.api.DefaultInvoicePaymentInfoEvent;
@@ -286,7 +286,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
     @Override
     public List<InvoiceItemModelDao> createInvoices(final List<InvoiceModelDao> invoices,
                                                     final InternalCallContext context) {
-        return createInvoices(invoices, new FutureAccountNotifications(ImmutableMap.<UUID, List<SubscriptionNotification>>of()), context);
+        return createInvoices(invoices, new FutureAccountNotifications(), context);
     }
 
     private List<InvoiceItemModelDao> createInvoices(final Iterable<InvoiceModelDao> invoices,
@@ -999,19 +999,18 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
     private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
                                              final FutureAccountNotifications callbackDateTimePerSubscriptions, final InternalCallContext internalCallContext) {
 
+        for (final LocalDate notificationDate : callbackDateTimePerSubscriptions.getNotificationsForTrigger().keySet()) {
+            final DateTime notificationDateTime = internalCallContext.toUTCDateTime(notificationDate);
+            final Set<UUID> subscriptionIds = callbackDateTimePerSubscriptions.getNotificationsForTrigger().get(notificationDate);
+            nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionIds, notificationDateTime, internalCallContext);
+        }
+
         final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(internalCallContext).getMillis();
-        final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
-        for (final UUID subscriptionId : callbackDateTimePerSubscriptions.getNotifications().keySet()) {
-            final List<SubscriptionNotification> callbackDateTimeUTC = callbackDateTimePerSubscriptions.getNotifications().get(subscriptionId);
-            for (final SubscriptionNotification cur : callbackDateTimeUTC) {
-                if (isInvoiceNotificationEnabled) {
-                    final DateTime curDryRunNotificationTime = cur.getEffectiveDate().minus(dryRunNotificationTime);
-                    final DateTime effectiveCurDryRunNotificationTime = (curDryRunNotificationTime.isAfter(clock.getUTCNow())) ? curDryRunNotificationTime : clock.getUTCNow();
-                    nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, effectiveCurDryRunNotificationTime, cur.getEffectiveDate(), internalCallContext);
-                }
-                if (cur.isForInvoiceNotificationTrigger()) {
-                    nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionId, cur.getEffectiveDate(), internalCallContext);
-                }
+        if (dryRunNotificationTime > 0) {
+            for (final LocalDate notificationDate : callbackDateTimePerSubscriptions.getNotificationsForDryRun().keySet()) {
+                final DateTime notificationDateTime = internalCallContext.toUTCDateTime(notificationDate);
+                final Set<UUID> subscriptionIds = callbackDateTimePerSubscriptions.getNotificationsForDryRun().get(notificationDate);
+                nextBillingDatePoster.insertNextBillingDryRunNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionIds, notificationDateTime, notificationDateTime.plusMillis((int) dryRunNotificationTime), internalCallContext);
             }
         }
     }
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 46d742e..fabf9c7 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -24,7 +24,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.Iterator;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -46,14 +46,12 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.CatalogApiException;
-import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.entitlement.api.SubscriptionEventType;
 import org.killbill.billing.events.BusInternalEvent;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.events.InvoiceNotificationInternalEvent;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.api.DefaultInvoiceService;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.invoice.api.DryRunType;
@@ -86,7 +84,6 @@ import org.killbill.billing.junction.BillingEvent;
 import org.killbill.billing.junction.BillingEventSet;
 import org.killbill.billing.junction.BillingInternalApi;
 import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.api.TagApiException;
@@ -114,6 +111,7 @@ import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
@@ -264,7 +262,7 @@ public class InvoiceDispatcher {
         final boolean upcomingInvoiceDryRun = isDryRun && DryRunType.UPCOMING_INVOICE.equals(dryRunArguments.getDryRunType());
 
         LocalDate inputTargetDate = inputTargetDateMaybeNull;
-        // A null inputTargetDate is only allowed in dryRun mode to have the system compute it
+        // A null inputTargetDate is only allowed in UPCOMING_INVOICE dryRun mode to have the system compute it
         if (inputTargetDate == null && !upcomingInvoiceDryRun) {
             inputTargetDate = clock.getUTCToday();
         }
@@ -276,28 +274,59 @@ public class InvoiceDispatcher {
             if (billingEvents.isEmpty()) {
                 return null;
             }
-            final Iterable<UUID> filteredSubscriptionIdsForDryRun = getFilteredSubscriptionIdsForDryRun(dryRunArguments, billingEvents);
-            final List<LocalDate> candidateTargetDates = (inputTargetDate != null) ?
-                                                         ImmutableList.<LocalDate>of(inputTargetDate) :
-                                                         getUpcomingInvoiceCandidateDates(filteredSubscriptionIdsForDryRun, context);
-            for (final LocalDate curTargetDate : candidateTargetDates) {
-                final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDate, billingEvents, isDryRun, context);
-                if (invoice != null) {
-                    filterInvoiceItemsForDryRun(filteredSubscriptionIdsForDryRun, invoice);
-
-                    if (!isDryRun && parkedAccount) {
-                        try {
-                            log.info("Illegal invoicing state fixed for accountId='{}', unparking account", accountId);
-                            parkedAccountsManager.unparkAccount(accountId, context);
-                        } catch (final TagApiException ignored) {
-                            log.warn("Unable to unpark account", ignored);
-                        }
+
+            // Avoid pulling all invoices when AUTO_INVOICING_OFF is set since we will disable invoicing later
+            // (Note that we can't return right away as we send a NullInvoice event)
+            final List<Invoice> existingInvoices = billingEvents.isAccountAutoInvoiceOff() ?
+                                                   ImmutableList.<Invoice>of() :
+                                                   ImmutableList.<Invoice>copyOf(Collections2.transform(invoiceDao.getInvoicesByAccount(context),
+                                                                                                        new Function<InvoiceModelDao, Invoice>() {
+                                                                                                            @Override
+                                                                                                            public Invoice apply(final InvoiceModelDao input) {
+                                                                                                                return new DefaultInvoice(input);
+                                                                                                            }
+                                                                                                        }));
+            Invoice invoice;
+            if (!isDryRun) {
+                invoice = processAccountWithLockAndInputTargetDate(accountId, inputTargetDate, billingEvents, existingInvoices, false, context);
+                if (parkedAccount) {
+                    try {
+                        log.info("Illegal invoicing state fixed for accountId='{}', unparking account", accountId);
+                        parkedAccountsManager.unparkAccount(accountId, context);
+                    } catch (final TagApiException ignored) {
+                        log.warn("Unable to unpark account", ignored);
                     }
+                }
+            } else /* Dry run use cases */ {
+
+                final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+                                                                                                          DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+                final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+
+                final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap = getNextTransitionsForSubscriptions(billingEvents);
+
+                // List of all existing invoice notifications
+                final List<LocalDate> allCandidateTargetDates = getUpcomingInvoiceCandidateDates(futureNotifications, nextScheduledSubscriptionsEventMap, ImmutableList.<UUID>of(), context);
+
+                if (dryRunArguments.getDryRunType() == DryRunType.UPCOMING_INVOICE) {
+
+                    final Iterable<UUID> filteredSubscriptionIdsForDryRun = getFilteredSubscriptionIdsFor_UPCOMING_INVOICE_DryRun(dryRunArguments, billingEvents);
 
-                    return invoice;
+                    // List of existing invoice notifications associated to the filter set of subscriptionIds
+                    final List<LocalDate> filteredCandidateTargetDates = Iterables.isEmpty(filteredSubscriptionIdsForDryRun) ?
+                                                                         allCandidateTargetDates :
+                                                                         getUpcomingInvoiceCandidateDates(futureNotifications, nextScheduledSubscriptionsEventMap, filteredSubscriptionIdsForDryRun, context);
+
+                    if (Iterables.isEmpty(filteredSubscriptionIdsForDryRun)) {
+                        invoice = processDryRun_UPCOMING_INVOICE_Invoice(accountId, allCandidateTargetDates, billingEvents, existingInvoices, context);
+                    } else {
+                        invoice = processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(accountId, filteredCandidateTargetDates, allCandidateTargetDates, billingEvents, existingInvoices, context);
+                    }
+                } else /* DryRunType.TARGET_DATE, SUBSCRIPTION_ACTION */ {
+                    invoice = processDryRun_TARGET_DATE_Invoice(accountId, inputTargetDate, allCandidateTargetDates, billingEvents, existingInvoices, context);
                 }
             }
-            return null;
+            return invoice;
         } catch (final CatalogApiException e) {
             log.warn("Failed to retrieve BillingEvents for accountId='{}', dryRunArguments='{}'", accountId, dryRunArguments, e);
             return null;
@@ -313,32 +342,89 @@ public class InvoiceDispatcher {
                 parkAccount(accountId, context);
             }
             throw e;
+        } catch (final NoSuchNotificationQueue e) {
+            // Should not happen, notificationQ is only used for dry run mode
+            if (!isDryRun) {
+                log.warn("Missing notification queue, accountId='{}', dryRunArguments='{}', parking account", accountId, dryRunArguments, e);
+                parkAccount(accountId, context);
+            }
+            throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, "Failed to retrieve future notifications from notificationQ");
         }
     }
 
-    private void parkAccount(final UUID accountId, final InternalCallContext context) {
-        try {
-            parkedAccountsManager.parkAccount(accountId, context);
-        } catch (final TagApiException ignored) {
-            log.warn("Unable to park account", ignored);
+    // Return a map of subscriptionId / localDate identifying what is the next upcoming billing transition (PHASE, PAUSE, ..)
+    private Map<UUID, DateTime> getNextTransitionsForSubscriptions(final BillingEventSet billingEvents) {
+
+        final DateTime now = clock.getUTCNow();
+        final Map<UUID, DateTime> result = new HashMap<UUID, DateTime>();
+        for (final BillingEvent evt : billingEvents) {
+            final UUID subscriptionId = evt.getSubscription().getId();
+            final DateTime evtEffectiveDate = evt.getEffectiveDate();
+            if (evtEffectiveDate.compareTo(now) <= 0) {
+                continue;
+            }
+            final DateTime nextUpcomingPerSubscriptionDate = result.get(subscriptionId);
+            if (nextUpcomingPerSubscriptionDate == null || nextUpcomingPerSubscriptionDate.compareTo(evtEffectiveDate) > 0) {
+                result.put(subscriptionId, evtEffectiveDate);
+            }
         }
+        return result;
     }
 
-    private void filterInvoiceItemsForDryRun(final Iterable<UUID> filteredSubscriptionIdsForDryRun, final Invoice invoice) {
-        if (!filteredSubscriptionIdsForDryRun.iterator().hasNext()) {
-            return;
+    private Invoice processDryRun_UPCOMING_INVOICE_Invoice(final UUID accountId, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+        for (final LocalDate curTargetDate : allCandidateTargetDates) {
+            final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDate, billingEvents, existingInvoices, true, context);
+            if (invoice != null) {
+                return invoice;
+            }
         }
+        return null;
+    }
 
-        final Iterator<InvoiceItem> it = invoice.getInvoiceItems().iterator();
-        while (it.hasNext()) {
-            final InvoiceItem cur = it.next();
-            if (!Iterables.contains(filteredSubscriptionIdsForDryRun, cur.getSubscriptionId())) {
-                it.remove();
+    private Invoice processDryRun_UPCOMING_INVOICE_FILTERING_Invoice(final UUID accountId, final List<LocalDate> filteringCandidateTargetDates, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+        for (final LocalDate curTargetDate : filteringCandidateTargetDates) {
+            final Invoice invoice = processDryRun_TARGET_DATE_Invoice(accountId, curTargetDate, allCandidateTargetDates, billingEvents, existingInvoices, context);
+            if (invoice != null) {
+                return invoice;
             }
         }
+        return null;
     }
 
-    private Iterable<UUID> getFilteredSubscriptionIdsForDryRun(@Nullable final DryRunArguments dryRunArguments, final BillingEventSet billingEvents) {
+    private Invoice processDryRun_TARGET_DATE_Invoice(final UUID accountId, final LocalDate targetDate, final List<LocalDate> allCandidateTargetDates, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
+
+        LocalDate prevLocalDate = null;
+        for (final LocalDate cur : allCandidateTargetDates) {
+            if (cur.compareTo(targetDate) < 0) {
+                prevLocalDate = cur;
+            } else {
+                break;
+            }
+        }
+
+        // Generate a dryRun invoice for such date if required in such a way that dryRun invoice on our targetDate only contains items that we expect to see
+        final Invoice additionalInvoice = prevLocalDate != null ?
+                                          processAccountWithLockAndInputTargetDate(accountId, prevLocalDate, billingEvents, existingInvoices, true, context) :
+                                          null;
+
+        final List<Invoice> augmentedExistingInvoices = additionalInvoice != null ?
+                                                        new ImmutableList.Builder().addAll(existingInvoices).add(additionalInvoice).build() :
+                                                        existingInvoices;
+
+        final Invoice targetInvoice = processAccountWithLockAndInputTargetDate(accountId, targetDate, billingEvents, augmentedExistingInvoices, true, context);
+        // If our targetDate -- user specified -- did not align with any boundary, we return previous 'additionalInvoice' invoice
+        return targetInvoice != null ? targetInvoice : additionalInvoice;
+    }
+
+    private void parkAccount(final UUID accountId, final InternalCallContext context) {
+        try {
+            parkedAccountsManager.parkAccount(accountId, context);
+        } catch (final TagApiException ignored) {
+            log.warn("Unable to park account", ignored);
+        }
+    }
+
+    private Iterable<UUID> getFilteredSubscriptionIdsFor_UPCOMING_INVOICE_DryRun(@Nullable final DryRunArguments dryRunArguments, final BillingEventSet billingEvents) {
         if (dryRunArguments == null ||
             !dryRunArguments.getDryRunType().equals(DryRunType.UPCOMING_INVOICE) ||
             (dryRunArguments.getSubscriptionId() == null && dryRunArguments.getBundleId() == null)) {
@@ -362,8 +448,12 @@ public class InvoiceDispatcher {
         });
     }
 
-    private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId, final LocalDate targetDate,
-                                                             final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext internalCallContext) throws InvoiceApiException {
+    private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId,
+                                                             final LocalDate targetDate,
+                                                             final BillingEventSet billingEvents,
+                                                             final List<Invoice> existingInvoices,
+                                                             final boolean isDryRun,
+                                                             final InternalCallContext internalCallContext) throws InvoiceApiException {
         final ImmutableAccountData account;
         try {
             account = accountApi.getImmutableAccountDataById(accountId, internalCallContext);
@@ -372,11 +462,11 @@ public class InvoiceDispatcher {
             return null;
         }
 
-        final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, existingInvoices, internalCallContext);
         final DefaultInvoice invoice = invoiceWithMetadata.getInvoice();
 
         // Compute future notifications
-        final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, internalCallContext);
+        final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, billingEvents, internalCallContext);
 
         // If invoice comes back null, there is nothing new to generate, we can bail early
         if (invoice == null) {
@@ -471,20 +561,11 @@ public class InvoiceDispatcher {
         return invoice;
     }
 
-    private InvoiceWithMetadata generateKillBillInvoice(final ImmutableAccountData account, final LocalDate targetDate, final BillingEventSet billingEvents, final InternalCallContext context) throws InvoiceApiException {
-        final List<Invoice> invoices = billingEvents.isAccountAutoInvoiceOff() ?
-                                       ImmutableList.<Invoice>of() :
-                                       ImmutableList.<Invoice>copyOf(Collections2.transform(invoiceDao.getInvoicesByAccount(context),
-                                                                                            new Function<InvoiceModelDao, Invoice>() {
-                                                                                                @Override
-                                                                                                public Invoice apply(final InvoiceModelDao input) {
-                                                                                                    return new DefaultInvoice(input);
-                                                                                                }
-                                                                                            }));
+    private InvoiceWithMetadata generateKillBillInvoice(final ImmutableAccountData account, final LocalDate targetDate, final BillingEventSet billingEvents, final List<Invoice> existingInvoices, final InternalCallContext context) throws InvoiceApiException {
         final UUID targetInvoiceId;
         // Filter out DRAFT invoices for computation  of existing items unless Account is in AUTO_INVOICING_REUSE_DRAFT
         if (billingEvents.isAccountAutoInvoiceReuseDraft()) {
-            final Invoice existingDraft = Iterables.tryFind(invoices, new Predicate<Invoice>() {
+            final Invoice existingDraft = Iterables.tryFind(existingInvoices, new Predicate<Invoice>() {
                 @Override
                 public boolean apply(final Invoice input) {
                     return input.getStatus() == InvoiceStatus.DRAFT;
@@ -495,49 +576,71 @@ public class InvoiceDispatcher {
             targetInvoiceId = null;
         }
 
-        return generator.generateInvoice(account, billingEvents, invoices, targetInvoiceId, targetDate, account.getCurrency(), context);
+        return generator.generateInvoice(account, billingEvents, existingInvoices, targetInvoiceId, targetDate, account.getCurrency(), context);
     }
 
-    private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final InternalCallContext context) {
-        final Map<UUID, List<SubscriptionNotification>> result = new HashMap<UUID, List<SubscriptionNotification>>();
+    private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final InternalCallContext context) {
 
-        for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
+        final Map<LocalDate, Set<UUID>> notificationListForTrigger = new HashMap<LocalDate, Set<UUID>>();
 
-            final List<SubscriptionNotification> perSubscriptionNotifications = new ArrayList<SubscriptionNotification>();
+        for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
 
             final SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().get(subscriptionId);
-            // Add next recurring date if any
+
             if (subscriptionFutureNotificationDates.getNextRecurringDate() != null) {
-                perSubscriptionNotifications.add(new SubscriptionNotification(context.toUTCDateTime(subscriptionFutureNotificationDates.getNextRecurringDate()), true));
+                Set<UUID> subscriptionsForDates = notificationListForTrigger.get(subscriptionFutureNotificationDates.getNextRecurringDate());
+                if (subscriptionsForDates == null) {
+                    subscriptionsForDates = new HashSet<UUID>();
+                    notificationListForTrigger.put(subscriptionFutureNotificationDates.getNextRecurringDate(), subscriptionsForDates);
+                }
+                subscriptionsForDates.add(subscriptionId);
             }
-            // Add next usage dates if any
+
             if (subscriptionFutureNotificationDates.getNextUsageDates() != null) {
                 for (final UsageDef usageDef : subscriptionFutureNotificationDates.getNextUsageDates().keySet()) {
+
                     final LocalDate nextNotificationDateForUsage = subscriptionFutureNotificationDates.getNextUsageDates().get(usageDef);
-                    final DateTime subscriptionUsageCallbackDate = nextNotificationDateForUsage != null ? context.toUTCDateTime(nextNotificationDateForUsage) : null;
-                    perSubscriptionNotifications.add(new SubscriptionNotification(subscriptionUsageCallbackDate, true));
+                    Set<UUID> subscriptionsForDates = notificationListForTrigger.get(nextNotificationDateForUsage);
+                    if (subscriptionsForDates == null) {
+                        subscriptionsForDates = new HashSet<UUID>();
+                        notificationListForTrigger.put(nextNotificationDateForUsage, subscriptionsForDates);
+                    }
+                    subscriptionsForDates.add(subscriptionId);
                 }
             }
-            if (!perSubscriptionNotifications.isEmpty()) {
-                result.put(subscriptionId, perSubscriptionNotifications);
-            }
         }
 
-        // If dryRunNotification is enabled we also need to fetch the upcoming PHASE dates (we add SubscriptionNotification with isForInvoiceNotificationTrigger = false)
-        final boolean isInvoiceNotificationEnabled = invoiceConfig.getDryRunNotificationSchedule(context).getMillis() > 0;
+        final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
+        final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
+
+        final Map<LocalDate, Set<UUID>> notificationListForDryRun = isInvoiceNotificationEnabled ? new HashMap<LocalDate, Set<UUID>>() : ImmutableMap.<LocalDate, Set<UUID>>of();
         if (isInvoiceNotificationEnabled) {
-            final Map<UUID, DateTime> upcomingPhasesForSubscriptions = subscriptionApi.getNextFutureEventForSubscriptions(SubscriptionBaseTransitionType.PHASE, context);
-            for (final UUID cur : upcomingPhasesForSubscriptions.keySet()) {
-                final DateTime curDate = upcomingPhasesForSubscriptions.get(cur);
-                List<SubscriptionNotification> resultValue = result.get(cur);
-                if (resultValue == null) {
-                    resultValue = new ArrayList<SubscriptionNotification>();
+            for (final LocalDate curDate : notificationListForTrigger.keySet()) {
+                final LocalDate curDryRunDate = context.toLocalDate(context.toUTCDateTime(curDate).minus(dryRunNotificationTime));
+                Set<UUID> subscriptionsForDryRunDates = notificationListForDryRun.get(curDryRunDate);
+                if (subscriptionsForDryRunDates == null) {
+                    subscriptionsForDryRunDates = new HashSet<UUID>();
+                    notificationListForDryRun.put(curDryRunDate, subscriptionsForDryRunDates);
                 }
-                resultValue.add(new SubscriptionNotification(curDate, false));
-                result.put(cur, resultValue);
+                subscriptionsForDryRunDates.addAll(notificationListForTrigger.get(curDate));
+            }
+
+            final Map<UUID, DateTime> upcomingPhasesForSubscriptions = isInvoiceNotificationEnabled ?
+                                                                       getNextTransitionsForSubscriptions(billingEvents) :
+                                                                       ImmutableMap.<UUID, DateTime>of();
+
+            for (UUID curId : upcomingPhasesForSubscriptions.keySet()) {
+                final LocalDate curDryRunDate = context.toLocalDate(upcomingPhasesForSubscriptions.get(curId).minus(dryRunNotificationTime));
+                Set<UUID> subscriptionsForDryRunDates = notificationListForDryRun.get(curDryRunDate);
+                if (subscriptionsForDryRunDates == null) {
+                    subscriptionsForDryRunDates = new HashSet<UUID>();
+                    notificationListForDryRun.put(curDryRunDate, subscriptionsForDryRunDates);
+                }
+                subscriptionsForDryRunDates.add(curId);
             }
         }
-        return new FutureAccountNotifications(result);
+
+        return new FutureAccountNotifications(notificationListForTrigger, notificationListForDryRun);
     }
 
     private List<InvoiceItemModelDao> transformToInvoiceModelDao(final List<InvoiceItem> invoiceItems) {
@@ -656,40 +759,48 @@ public class InvoiceDispatcher {
 
     public static class FutureAccountNotifications {
 
-        private final Map<UUID, List<SubscriptionNotification>> notifications;
+        private final Map<LocalDate, Set<UUID>> notificationListForTrigger;
+        private final Map<LocalDate, Set<UUID>> notificationListForDryRun;
 
-        public FutureAccountNotifications(final Map<UUID, List<SubscriptionNotification>> notifications) {
-            this.notifications = notifications;
+        public FutureAccountNotifications() {
+            this(ImmutableMap.<LocalDate, Set<UUID>>of(), ImmutableMap.<LocalDate, Set<UUID>>of());
         }
 
-        public Map<UUID, List<SubscriptionNotification>> getNotifications() {
-            return notifications;
+        public FutureAccountNotifications(final Map<LocalDate, Set<UUID>> notificationListForTrigger, final Map<LocalDate, Set<UUID>> notificationListForDryRun) {
+            this.notificationListForTrigger = notificationListForTrigger;
+            this.notificationListForDryRun = notificationListForDryRun;
         }
 
-        public static class SubscriptionNotification {
+        public Map<LocalDate, Set<UUID>> getNotificationsForTrigger() {
+            return notificationListForTrigger;
+        }
 
-            private final DateTime effectiveDate;
-            private final boolean isForNotificationTrigger;
+        public Map<LocalDate, Set<UUID>> getNotificationsForDryRun() {
+            return notificationListForDryRun;
+        }
+    }
 
-            public SubscriptionNotification(final DateTime effectiveDate, final boolean isForNotificationTrigger) {
-                this.effectiveDate = effectiveDate;
-                this.isForNotificationTrigger = isForNotificationTrigger;
-            }
+    private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
+                                                             final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap,
+                                                             final Iterable<UUID> filteredSubscriptionIds,
+                                                             final InternalCallContext internalCallContext) {
 
-            public DateTime getEffectiveDate() {
-                return effectiveDate;
-            }
+        final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(futureNotifications, filteredSubscriptionIds);
 
-            public boolean isForInvoiceNotificationTrigger() {
-                return isForNotificationTrigger;
+        final Iterable<DateTime> nextScheduledSubscriptionsEvents;
+        if (!Iterables.isEmpty(filteredSubscriptionIds)) {
+            List<DateTime> tmp = new ArrayList<DateTime>();
+            for (final UUID curSubscriptionId : nextScheduledSubscriptionsEventMap.keySet()) {
+                if (Iterables.contains(filteredSubscriptionIds, curSubscriptionId)) {
+                    tmp.add(nextScheduledSubscriptionsEventMap.get(curSubscriptionId));
+                }
             }
+            nextScheduledSubscriptionsEvents = tmp;
+        } else {
+            nextScheduledSubscriptionsEvents = nextScheduledSubscriptionsEventMap.values();
         }
-    }
 
-    private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<UUID> filteredSubscriptionIds, final InternalCallContext internalCallContext) {
-        final Iterable<DateTime> nextScheduledInvoiceDates = getNextScheduledInvoiceEffectiveDate(filteredSubscriptionIds, internalCallContext);
-        final Iterable<DateTime> nextScheduledSubscriptionsEventDates = subscriptionApi.getFutureNotificationsForAccount(internalCallContext);
-        return Lists.<DateTime, LocalDate>transform(UPCOMING_NOTIFICATION_DATE_ORDERING.sortedCopy(Iterables.<DateTime>concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEventDates)),
+        return Lists.<DateTime, LocalDate>transform(UPCOMING_NOTIFICATION_DATE_ORDERING.sortedCopy(Iterables.<DateTime>concat(nextScheduledInvoiceDates, nextScheduledSubscriptionsEvents)),
                                                     new Function<DateTime, LocalDate>() {
                                                         @Override
                                                         public LocalDate apply(final DateTime input) {
@@ -698,28 +809,30 @@ public class InvoiceDispatcher {
                                                     });
     }
 
-    private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final Iterable<UUID> filteredSubscriptionIds, final InternalCallContext internalCallContext) {
-        try {
-            final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
-                                                                                                      DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
-            final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
-
-            final Collection<DateTime> effectiveDates = new LinkedList<DateTime>();
-            for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
-                final boolean isEventForSubscription = !filteredSubscriptionIds.iterator().hasNext() || Iterables.contains(filteredSubscriptionIds, input.getEvent().getUuidKey());
-
-                final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
-                                                              input.getEvent().isDryRunForInvoiceNotification() : false;
-                if (isEventForSubscription && !isEventDryRunForNotifications) {
-                    effectiveDates.add(input.getEffectiveDate());
+    private Iterable<DateTime> getNextScheduledInvoiceEffectiveDate(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
+                                                                    final Iterable<UUID> filteredSubscriptionIds) {
+        final Collection<DateTime> effectiveDates = new LinkedList<DateTime>();
+        for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
+
+            // If we don't specify a filter list of subscriptionIds, we look at all events.
+            boolean isEventForSubscription = Iterables.isEmpty(filteredSubscriptionIds);
+            // If we specify a filter, we keep the date if at least one of the subscriptions from the event list matches one of the subscription from our filter list
+            if (!Iterables.isEmpty(filteredSubscriptionIds)) {
+                for (final UUID curSubscriptionId : filteredSubscriptionIds) {
+                    if (Iterables.contains(input.getEvent().getUuidKeys(), curSubscriptionId)) {
+                        isEventForSubscription = true;
+                        break;
+                    }
                 }
-
             }
 
-            return effectiveDates;
-        } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
-            throw new IllegalStateException(noSuchNotificationQueue);
+            final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
+                                                          input.getEvent().isDryRunForInvoiceNotification() : false;
+            if (isEventForSubscription && !isEventDryRunForNotifications) {
+                effectiveDates.add(input.getEffectiveDate());
+            }
         }
+        return effectiveDates;
     }
 
     private static final class TargetDateDryRunArguments implements DryRunArguments {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
index 85391a5..47dc673 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
@@ -83,20 +83,21 @@ public class DefaultNextBillingDateNotifier extends RetryableService implements 
 
                 // Just to ensure compatibility with json that might not have that targetDate field (old versions < 0.13.6)
                 final DateTime targetDate = key.getTargetDate() != null ? key.getTargetDate() : eventDate;
+                final UUID firstSubscriptionId = key.getUuidKeys().iterator().next();
                 try {
-                    final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(key.getUuidKey(), internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
+                    final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(firstSubscriptionId, internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
                     if (subscription == null) {
-                        log.warn("Unable to retrieve subscriptionId='{}' for event {}", key.getUuidKey(), key);
+                        log.warn("Unable to retrieve subscriptionId='{}' for event {}", firstSubscriptionId, key);
                         return;
                     }
                     if (key.isDryRunForInvoiceNotification() != null && // Just to ensure compatibility with json that might not have that field (old versions < 0.13.6)
                         key.isDryRunForInvoiceNotification()) {
-                        processEventForInvoiceNotification(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
+                        processEventForInvoiceNotification(firstSubscriptionId, targetDate, userToken, accountRecordId, tenantRecordId);
                     } else {
-                        processEventForInvoiceGeneration(key.getUuidKey(), targetDate, userToken, accountRecordId, tenantRecordId);
+                        processEventForInvoiceGeneration(firstSubscriptionId, targetDate, userToken, accountRecordId, tenantRecordId);
                     }
-                } catch (final SubscriptionBaseApiException e) {
-                    log.warn("Error retrieving subscriptionId='{}'", key.getUuidKey(), e);
+                } catch (SubscriptionBaseApiException e) {
+                    log.warn("Error retrieving subscriptionId='{}'", firstSubscriptionId, e);
                 }
             }
         };
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
index 6dac86e..fb269c5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
@@ -33,12 +33,16 @@ import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificatio
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
 import com.google.inject.Inject;
 
 public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
 
     private static final Logger log = LoggerFactory.getLogger(DefaultNextBillingDatePoster.class);
 
+    private static Joiner JOINER = Joiner.on(",");
+
     private final NotificationQueueService notificationQueueService;
 
     @Inject
@@ -47,20 +51,29 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
     }
 
     @Override
-    public void insertNextBillingNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
-                                                             final UUID subscriptionId, final DateTime futureNotificationTime, final InternalCallContext internalCallContext) {
-        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionId, Boolean.FALSE, futureNotificationTime, futureNotificationTime, internalCallContext);
+    public void insertNextBillingNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                                             final UUID accountId,
+                                                             final Iterable<UUID> subscriptionIds,
+                                                             final DateTime futureNotificationTime,
+                                                             final InternalCallContext internalCallContext) {
+        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.FALSE, futureNotificationTime, futureNotificationTime, internalCallContext);
     }
 
     @Override
-    public void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
-                                                                   final UUID subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext) {
-        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionId, Boolean.TRUE, futureNotificationTime, targetDate, internalCallContext);
+    public void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                                                   final UUID accountId,
+                                                                   final Iterable<UUID> subscriptionIds,
+                                                                   final DateTime futureNotificationTime,
+                                                                   final DateTime targetDate,
+                                                                   final InternalCallContext internalCallContext) {
+        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.TRUE, futureNotificationTime, targetDate, internalCallContext);
     }
 
     private void insertNextBillingFromTransactionInternal(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
-                                                          final UUID subscriptionId, final Boolean isDryRunForInvoiceNotification,
-                                                          final DateTime futureNotificationTime, final DateTime targetDate,
+                                                          final Iterable<UUID> subscriptionIds,
+                                                          final Boolean isDryRunForInvoiceNotification,
+                                                          final DateTime futureNotificationTime,
+                                                          final DateTime targetDate,
                                                           final InternalCallContext internalCallContext) {
         final NotificationQueue nextBillingQueue;
         try {
@@ -70,7 +83,7 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
             // If we see existing notification for the same date (and isDryRunForInvoiceNotification mode), we don't insert a new notification
             final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = nextBillingQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
 
-            boolean existingFutureNotificationWithSameDate = false;
+            NotificationEventWithMetadata<NextBillingDateNotificationKey> existingNotificationForEffectiveDate = null;
             for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
                 final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
                                                               input.getEvent().isDryRunForInvoiceNotification() : false;
@@ -81,25 +94,28 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
                 if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 &&
                     ((isDryRunForInvoiceNotification && isEventDryRunForNotifications) ||
                      (!isDryRunForInvoiceNotification && !isEventDryRunForNotifications))) {
-                    existingFutureNotificationWithSameDate = true;
+                    existingNotificationForEffectiveDate = input;
                 }
                 // Go through all results to close the connection
             }
 
-            if (!existingFutureNotificationWithSameDate) {
-                log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), subscriptionId.toString());
+            if (existingNotificationForEffectiveDate == null) {
+                log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), JOINER.join(subscriptionIds));
 
+                final NextBillingDateNotificationKey newNotificationEvent = new NextBillingDateNotificationKey(null, subscriptionIds, targetDate, isDryRunForInvoiceNotification);
                 nextBillingQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), futureNotificationTime,
-                                                                         new NextBillingDateNotificationKey(subscriptionId, targetDate, isDryRunForInvoiceNotification), internalCallContext.getUserToken(),
+                                                                         newNotificationEvent, internalCallContext.getUserToken(),
                                                                          internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
-            } else if (log.isDebugEnabled()) {
-                log.debug("*********************   SKIPPING Queuing next billing date notification at {} for subscriptionId {} *******************", futureNotificationTime.toString(), subscriptionId.toString());
+            } else {
+                log.info("Updating next billing date notification event at {} for subscriptionId {}", futureNotificationTime.toString(), JOINER.join(subscriptionIds));
+                final NextBillingDateNotificationKey updateNotificationEvent = new NextBillingDateNotificationKey(existingNotificationForEffectiveDate.getEvent(), subscriptionIds);
+                nextBillingQueue.updateFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), existingNotificationForEffectiveDate.getRecordId(), updateNotificationEvent, internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
             }
 
         } catch (final NoSuchNotificationQueue e) {
             log.error("Attempting to put items on a non-existent queue (NextBillingDateNotifier).", e);
         } catch (final IOException e) {
-            log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionId);
+            log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionIds);
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
index 664d415..1b64c60 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
@@ -23,21 +23,35 @@ import org.killbill.notificationq.DefaultUUIDNotificationKey;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 
 public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
 
     private Boolean isDryRunForInvoiceNotification;
     private DateTime targetDate;
+    private final Iterable<UUID> uuidKeys;
 
     @JsonCreator
-    public NextBillingDateNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey,
+    public NextBillingDateNotificationKey(@Deprecated @JsonProperty("uuidKey") final UUID uuidKey,
+                                          @JsonProperty("uuidKeys") final Iterable<UUID> uuidKeys,
                                           @JsonProperty("targetDate") final DateTime targetDate,
                                           @JsonProperty("isDryRunForInvoiceNotification") final Boolean isDryRunForInvoiceNotification) {
         super(uuidKey);
+        this.uuidKeys = uuidKeys;
         this.targetDate = targetDate;
         this.isDryRunForInvoiceNotification = isDryRunForInvoiceNotification;
     }
 
+    public NextBillingDateNotificationKey(NextBillingDateNotificationKey existing,
+                                          final Iterable<UUID> newUUIDKeys) {
+        super(null);
+        this.uuidKeys = ImmutableSet.copyOf(Iterables.concat(existing.getUuidKeys(), newUUIDKeys));
+        this.targetDate = existing.getTargetDate();
+        this.isDryRunForInvoiceNotification = existing.isDryRunForInvoiceNotification();
+    }
+
     @JsonProperty("isDryRunForInvoiceNotification")
     public Boolean isDryRunForInvoiceNotification() {
         return isDryRunForInvoiceNotification;
@@ -46,4 +60,13 @@ public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
     public DateTime getTargetDate() {
         return targetDate;
     }
+
+    public final Iterable<UUID> getUuidKeys() {
+        // Deprecated mode
+        if (uuidKeys == null || !uuidKeys.iterator().hasNext()) {
+            return ImmutableList.of(getUuidKey());
+        } else {
+            return uuidKeys;
+        }
+    }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
index 96836bd..d2dce63 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
@@ -27,8 +27,8 @@ import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
 public interface NextBillingDatePoster {
 
     void insertNextBillingNotificationFromTransaction(EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, UUID accountId,
-                                                      UUID subscriptionId, DateTime futureNotificationTime, InternalCallContext internalCallContext);
+                                                      Iterable<UUID> subscriptionId, DateTime futureNotificationTime, InternalCallContext internalCallContext);
 
     void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
-                                                            final UUID subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext);
+                                                            final Iterable<UUID> subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext);
 }
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 a7c6830..b837bdf 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
@@ -24,18 +24,21 @@ import org.killbill.billing.util.jackson.ObjectMapper;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
 public class TestNextBillingDateNotificationKey {
 
     private static final ObjectMapper mapper = new ObjectMapper();
 
     @Test(groups = "fast")
-    public void testBasic() throws Exception {
+    public void testBasicWithUUIDKey() throws Exception {
 
         final UUID uuidKey = UUID.randomUUID();
         final DateTime targetDate = new DateTime();
         final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
 
-        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(uuidKey, targetDate, isDryRunForInvoiceNotification);
+        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(uuidKey, null, targetDate, isDryRunForInvoiceNotification);
         final String json = mapper.writeValueAsString(key);
 
         final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
@@ -44,6 +47,28 @@ public class TestNextBillingDateNotificationKey {
         Assert.assertEquals(result.isDryRunForInvoiceNotification(), isDryRunForInvoiceNotification);
     }
 
+
+    @Test(groups = "fast")
+    public void testBasicWithUUIDKeys() throws Exception {
+
+        final UUID uuidKey1 = UUID.randomUUID();
+        final UUID uuidKey2 = UUID.randomUUID();
+        final DateTime targetDate = new DateTime();
+        final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
+
+        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(null, ImmutableList.of(uuidKey1, uuidKey2), targetDate, isDryRunForInvoiceNotification);
+        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.assertNotNull(result.getUuidKeys());
+
+        Assert.assertTrue(Iterables.contains(result.getUuidKeys(), uuidKey1));
+        Assert.assertTrue(Iterables.contains(result.getUuidKeys(), uuidKey2));
+    }
+
     @Test(groups = "fast")
     public void testWithMissingFields() throws Exception {
         final String json = "{\"uuidKey\":\"a38c363f-b25b-4287-8ebc-55964e116d2f\"}";
@@ -52,5 +77,9 @@ public class TestNextBillingDateNotificationKey {
         Assert.assertNull(result.getTargetDate());
         Assert.assertNull(result.isDryRunForInvoiceNotification());
 
+        // 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");
+
     }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
index 763bd45..b620fab 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
@@ -31,6 +31,8 @@ import org.killbill.notificationq.api.NotificationQueue;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
+
 import static org.awaitility.Awaitility.await;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
@@ -47,7 +49,7 @@ public class TestNextBillingDateNotifier extends InvoiceTestSuiteWithEmbeddedDB 
 
         final NotificationQueue nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME, DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
 
-        nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(subscriptionId, now, Boolean.FALSE), internalCallContext.getUserToken(), accountRecordId, internalCallContext.getTenantRecordId());
+        nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(null, ImmutableList.<UUID>of(subscriptionId), now, Boolean.FALSE), internalCallContext.getUserToken(), accountRecordId, internalCallContext.getTenantRecordId());
 
         // Move time in the future after the notification effectiveDate
         clock.setDeltaFromReality(3000);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
index 693d0b7..42fccdb 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/proRations/InvoiceTestUtils.java
@@ -28,8 +28,6 @@ import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.entity.EntityPersistenceException;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.TestInvoiceHelper;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
@@ -47,7 +45,6 @@ import org.mockito.Mockito;
 import org.testng.Assert;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 
 public class InvoiceTestUtils {
 
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index af1ceb2..a07977a 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -66,6 +66,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
     private Account account;
     private SubscriptionBase subscription;
     private InternalCallContext context;
+    private InvoiceDispatcher dispatcher;
 
     @Override
     @BeforeMethod(groups = "slow")
@@ -74,6 +75,11 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         account = invoiceUtil.createAccount(callContext);
         subscription = invoiceUtil.createSubscription();
         context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+
+        dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
+                                           internalCallContextFactory,  invoicePluginDispatcher, locker, busService.getBus(),
+                                           notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
+
     }
 
     @Test(groups = "slow")
@@ -96,7 +102,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
 
         final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
                                                                    internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
-                                                                   null, invoiceConfig, clock, parkedAccountsManager);
+                                                                   notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
 
         Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
         Assert.assertNotNull(invoice);
@@ -139,7 +145,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
 
         final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
                                                                    internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
-                                                                   null, invoiceConfig, clock, parkedAccountsManager);
+                                                                   notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
 
         // Verify initial tags state for account
         Assert.assertTrue(tagUserApi.getTagsForAccount(accountId, true, callContext).isEmpty());
@@ -286,8 +292,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
         final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
                                                                    internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
-                                                                   null, invoiceConfig, clock, parkedAccountsManager);
-
+                                                                   notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
         final Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), new LocalDate("2012-07-30"), null, context);
         Assert.assertNotNull(invoice);
 
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index 6cf3f1a..81c6db8 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -84,6 +84,7 @@ import org.killbill.billing.util.currency.KillBillMoney;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.clock.Clock;
 import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.notificationq.api.NotificationQueueService;
 import org.mockito.Mockito;
 import org.skife.jdbi.v2.IDBI;
 import org.testng.Assert;
@@ -161,6 +162,7 @@ public class TestInvoiceHelper {
     private final MutableInternalCallContext internalCallContext;
     private final InternalCallContextFactory internalCallContextFactory;
     private final InvoiceConfig invoiceConfig;
+    private final NotificationQueueService notificationQueueService;
     // Low level SqlDao used by the tests to directly insert rows
     private final InvoicePaymentSqlDao invoicePaymentSqlDao;
     private final InvoiceItemSqlDao invoiceItemSqlDao;
@@ -170,7 +172,7 @@ public class TestInvoiceHelper {
     @Inject
     public TestInvoiceHelper(final InvoiceGenerator generator, final IDBI dbi,
                              final BillingInternalApi billingApi, final AccountInternalApi accountApi, final ImmutableAccountInternalApi immutableAccountApi, final InvoicePluginDispatcher invoicePluginDispatcher, final AccountUserApi accountUserApi, final SubscriptionBaseInternalApi subscriptionApi, final BusService busService,
-                             final InvoiceDao invoiceDao, final GlobalLocker locker, final Clock clock, final NonEntityDao nonEntityDao, final CacheControllerDispatcher cacheControllerDispatcher, final MutableInternalCallContext internalCallContext, final InvoiceConfig invoiceConfig,
+                             final InvoiceDao invoiceDao, final GlobalLocker locker, final Clock clock, final NonEntityDao nonEntityDao, final NotificationQueueService notificationQueueService, final MutableInternalCallContext internalCallContext, final InvoiceConfig invoiceConfig,
                              final ParkedAccountsManager parkedAccountsManager, final InternalCallContextFactory internalCallContextFactory) {
         this.generator = generator;
         this.billingApi = billingApi;
@@ -184,6 +186,7 @@ public class TestInvoiceHelper {
         this.locker = locker;
         this.clock = clock;
         this.nonEntityDao = nonEntityDao;
+        this.notificationQueueService = notificationQueueService;
         this.parkedAccountsManager = parkedAccountsManager;
         this.internalCallContext = internalCallContext;
         this.internalCallContextFactory = internalCallContextFactory;

NEWS 3(+3 -0)

diff --git a/NEWS b/NEWS
index c1e7da5..9b7028a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+0.18.14
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.18.14
+
 0.18.13
     See https://github.com/killbill/killbill/releases/tag/killbill-0.18.13
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index 88fd895..6887cc5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -175,7 +175,8 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
         final Boolean result = doJanitorOperationWithAccountLock(new JanitorIterationCallback() {
             @Override
             public Boolean doIteration() {
-                return updatePaymentAndTransactionInternal(payment, null, null, paymentTransaction, paymentTransactionInfoPlugin, internalTenantContext);
+                final PaymentTransactionModelDao refreshedPaymentTransaction = paymentDao.getPaymentTransaction(paymentTransaction.getId(), internalTenantContext);
+                return updatePaymentAndTransactionInternal(payment, null, null, refreshedPaymentTransaction, paymentTransactionInfoPlugin, internalTenantContext);
             }
         }, internalTenantContext);
         return result != null && result;
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
index e46ad72..168e06e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
@@ -18,7 +18,6 @@
 package org.killbill.billing.payment.core.janitor;
 
 import java.math.BigDecimal;
-import java.util.List;
 import java.util.UUID;
 
 import org.killbill.billing.account.api.Account;
@@ -27,7 +26,13 @@ import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
+import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
 import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentInfoPlugin;
 import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
 import org.killbill.billing.util.globallocker.LockerType;
 import org.killbill.commons.locker.GlobalLock;
@@ -106,4 +111,71 @@ public class TestIncompletePaymentTransactionTaskWithDB extends PaymentTestSuite
             }
         }
     }
+
+    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/809")
+    public void testUpdateWithinLock() throws PaymentApiException {
+        final Payment payment = paymentApi.createAuthorization(account,
+                                                          account.getPaymentMethodId(),
+                                                          null,
+                                                          BigDecimal.TEN,
+                                                          Currency.EUR,
+                                                          null,
+                                                          UUID.randomUUID().toString(),
+                                                          UUID.randomUUID().toString(),
+                                                          ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.UNDEFINED.toString(), false)),
+                                                          callContext);
+        final PaymentModelDao paymentModel = paymentDao.getPayment(payment.getId(), internalCallContext);
+        final UUID transactionId = payment.getTransactions().get(0).getId();
+        final PaymentTransactionModelDao transactionModel = paymentDao.getPaymentTransaction(transactionId, internalCallContext);
+
+        Assert.assertEquals(paymentModel.getStateName(), "AUTH_ERRORED");
+        Assert.assertEquals(transactionModel.getTransactionStatus().toString(), "UNKNOWN");
+
+        paymentDao.updatePaymentAndTransactionOnCompletion(
+                account.getId(),
+                null,
+                payment.getId(),
+                TransactionType.AUTHORIZE,
+                "AUTH_SUCCESS",
+                "AUTH_SUCCESS",
+                transactionId,
+                TransactionStatus.SUCCESS,
+                BigDecimal.TEN,
+                Currency.EUR,
+                "200",
+                "Ok",
+                internalCallContext);
+
+        paymentApi.createCapture(account,
+                                 payment.getId(),
+                                 BigDecimal.TEN,
+                                 Currency.EUR,
+                                 null,
+                                 UUID.randomUUID().toString(),
+                                 ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PROCESSED.toString(), false)),
+                                 callContext);
+
+        final PaymentModelDao paymentAfterCapture = paymentDao.getPayment(payment.getId(), internalCallContext);
+        Assert.assertEquals(paymentAfterCapture.getStateName(), "CAPTURE_SUCCESS");
+
+        PaymentTransactionInfoPlugin paymentTransactionInfoPlugin = new DefaultNoOpPaymentInfoPlugin(
+                payment.getId(),
+                transactionId,
+                TransactionType.AUTHORIZE,
+                BigDecimal.TEN,
+                Currency.EUR,
+                transactionModel.getEffectiveDate(),
+                transactionModel.getCreatedDate(),
+                PaymentPluginStatus.PROCESSED,
+                "200",
+                "OK");
+        incompletePaymentTransactionTask.updatePaymentAndTransactionIfNeededWithAccountLock(
+                paymentModel,
+                transactionModel,
+                paymentTransactionInfoPlugin,
+                internalCallContext);
+
+        final PaymentModelDao paymentAfterJanitor = paymentDao.getPayment(payment.getId(), internalCallContext);
+        Assert.assertEquals(paymentAfterJanitor.getStateName(), "CAPTURE_SUCCESS");
+    }
 }

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 2537129..58b57c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.141.12</version>
+        <version>0.141.13-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.19.0-SNAPSHOT</version>
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index 6768cec..fab57eb 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -58,7 +58,6 @@ import org.killbill.billing.subscription.api.SubscriptionApiBase;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
 import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
 import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
@@ -72,7 +71,6 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
 import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
 import org.killbill.billing.subscription.engine.addon.AddonUtils;
-import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
 import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
 import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
@@ -89,20 +87,14 @@ import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 import org.killbill.clock.Clock;
 import org.killbill.clock.DefaultClock;
-import org.killbill.notificationq.api.NotificationEvent;
-import org.killbill.notificationq.api.NotificationEventWithMetadata;
-import org.killbill.notificationq.api.NotificationQueue;
 import org.killbill.notificationq.api.NotificationQueueService;
-import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
@@ -780,51 +772,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
-    public Iterable<DateTime> getFutureNotificationsForAccount(final InternalCallContext internalCallContext) {
-        try {
-            final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultSubscriptionBaseService.SUBSCRIPTION_SERVICE_NAME,
-                                                                                                      DefaultSubscriptionBaseService.NOTIFICATION_QUEUE_NAME);
-            final Iterable<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
-            return Iterables.transform(futureNotifications, new Function<NotificationEventWithMetadata<NotificationEvent>, DateTime>() {
-                @Nullable
-                @Override
-                public DateTime apply(final NotificationEventWithMetadata<NotificationEvent> input) {
-                    return input.getEffectiveDate();
-                }
-            });
-        } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
-            throw new IllegalStateException(noSuchNotificationQueue);
-        }
-    }
-
-    @Override
-    public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType, final InternalCallContext internalCallContext) {
-        final Iterable<SubscriptionBaseEvent> events = dao.getFutureEventsForAccount(internalCallContext);
-        final Iterable<SubscriptionBaseEvent> filteredEvents = Iterables.filter(events, new Predicate<SubscriptionBaseEvent>() {
-            @Override
-            public boolean apply(final SubscriptionBaseEvent input) {
-                switch (input.getType()) {
-                    case PHASE:
-                        return eventType == SubscriptionBaseTransitionType.PHASE;
-                    case BCD_UPDATE:
-                        return eventType == SubscriptionBaseTransitionType.BCD_CHANGE;
-                    case API_USER:
-                    default:
-                        return true;
-                }
-            }
-        });
-        final Map<UUID, DateTime> result = filteredEvents.iterator().hasNext() ? new HashMap<UUID, DateTime>() : ImmutableMap.<UUID, DateTime>of();
-        for (final SubscriptionBaseEvent cur : filteredEvents) {
-            final DateTime targetDate = result.get(cur.getSubscriptionId());
-            if (targetDate == null || targetDate.compareTo(cur.getEffectiveDate()) > 0) {
-                result.put(cur.getSubscriptionId(), cur.getEffectiveDate());
-            }
-        }
-        return result;
-    }
-
-    @Override
     public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
 
         final Catalog catalog;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index 322f804..3af29b0 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -535,24 +535,6 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
     }
 
     @Override
-    public Iterable<SubscriptionBaseEvent> getFutureEventsForAccount(final InternalTenantContext context) {
-        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Iterable<SubscriptionBaseEvent>>() {
-            @Override
-            public Iterable<SubscriptionBaseEvent> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
-                final List<SubscriptionEventModelDao> activeEvents = transactional.getFutureActiveEventsForAccount(clock.getUTCNow().toDate(), context);
-                return Iterables.transform(activeEvents, new Function<SubscriptionEventModelDao, SubscriptionBaseEvent>() {
-
-                    @Override
-                    public SubscriptionBaseEvent apply(final SubscriptionEventModelDao input) {
-                        return SubscriptionEventModelDao.toSubscriptionEvent(input);
-                    }
-                });
-            }
-        });
-    }
-
-    @Override
     public List<SubscriptionBaseEvent> getPendingEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
         final Date now = clock.getUTCNow().toDate();
 
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
index 9063bba..c5d14bc 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
@@ -19,6 +19,7 @@ package org.killbill.billing.subscription.engine.dao.model;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.events.EventBaseBuilder;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
@@ -35,7 +36,6 @@ import org.killbill.billing.util.entity.dao.EntityModelDao;
 import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
 
 public class SubscriptionEventModelDao extends EntityModelDaoBase implements EntityModelDao<SubscriptionBaseEvent> {
-
     private long totalOrdering;
     private EventType eventType;
     private ApiEventType userType;
@@ -216,6 +216,29 @@ public class SubscriptionEventModelDao extends EntityModelDaoBase implements Ent
         return result;
     }
 
+    public boolean isOfSubscriptionBaseTransitionType(final SubscriptionBaseTransitionType type) {
+        switch(type) {
+            case CREATE:
+                return eventType == EventType.API_USER && userType == ApiEventType.CREATE;
+            case TRANSFER:
+                return eventType == EventType.API_USER && userType == ApiEventType.TRANSFER;
+            case CHANGE:
+                return eventType == EventType.API_USER && userType == ApiEventType.CHANGE;
+            case CANCEL:
+                return eventType == EventType.API_USER && userType == ApiEventType.CANCEL;
+            case UNCANCEL:
+                return eventType == EventType.API_USER && userType == ApiEventType.UNCANCEL;
+            case PHASE:
+                return eventType == EventType.PHASE;
+            case BCD_CHANGE:
+                return eventType == EventType.BCD_UPDATE;
+            case START_BILLING_DISABLED:
+            case END_BILLING_DISABLED:
+            default:
+                return false;
+        }
+    }
+
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder();
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 4393e40..0939c7f 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -26,6 +26,7 @@ import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.entitlement.api.SubscriptionApiException;
 import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
 import org.killbill.billing.subscription.api.transfer.BundleTransferData;
 import org.killbill.billing.subscription.api.transfer.TransferCancelData;
@@ -75,8 +76,6 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
 
     public SubscriptionBaseEvent getEventById(UUID eventId, InternalTenantContext context);
 
-    public Iterable<SubscriptionBaseEvent> getFutureEventsForAccount(InternalTenantContext context);
-
     public List<SubscriptionBaseEvent> getEventsForSubscription(UUID subscriptionId, InternalTenantContext context);
 
     public List<SubscriptionBaseEvent> getPendingEventsForSubscription(UUID subscriptionId, InternalTenantContext context);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 8bf6081..5d7c296 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -508,10 +508,6 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
         }
     }
 
-    @Override
-    public Iterable<SubscriptionBaseEvent> getFutureEventsForAccount(final InternalTenantContext context) {
-        return null;
-    }
 
     @Override
     public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleTransferData data,