killbill-memoizeit

invoice: Refactor the computation of next invoice future notitications Prior

8/30/2015 9:18:10 PM

Details

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index af96f5d..070348d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -44,10 +44,11 @@ import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
 import org.killbill.billing.invoice.model.BillingModeGenerator;
+import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
 import org.killbill.billing.invoice.model.DefaultInvoice;
 import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
-import org.killbill.billing.invoice.model.DefaultBillingModeGenerator;
 import org.killbill.billing.invoice.model.InvalidDateSequenceException;
 import org.killbill.billing.invoice.model.RecurringInvoiceItem;
 import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
@@ -92,12 +93,12 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
      * adjusts target date to the maximum invoice target date, if future invoices exist
      */
     @Override
-    public Invoice generateInvoice(final Account account, @Nullable final BillingEventSet events,
-                                   @Nullable final List<Invoice> existingInvoices,
-                                   final LocalDate targetDate,
-                                   final Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException {
+    public InvoiceWithMetadata generateInvoice(final Account account, @Nullable final BillingEventSet events,
+                                               @Nullable final List<Invoice> existingInvoices,
+                                               final LocalDate targetDate,
+                                               final Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException {
         if ((events == null) || (events.size() == 0) || events.isAccountAutoInvoiceOff()) {
-            return null;
+            return new InvoiceWithMetadata(null, ImmutableMap.<UUID, SubscriptionFutureNotificationDates>of());
         }
 
         validateTargetDate(targetDate);
@@ -105,21 +106,25 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
         final Invoice invoice = new DefaultInvoice(account.getId(), new LocalDate(clock.getUTCNow(), account.getTimeZone()), adjustedTargetDate, targetCurrency);
         final UUID invoiceId = invoice.getId();
+        final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
 
-        final List<InvoiceItem> inAdvanceItems = generateFixedAndRecurringInvoiceItems(account.getId(), invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency);
-        invoice.addInvoiceItems(inAdvanceItems);
+        final List<InvoiceItem> fixedAndRecurringItems = generateFixedAndRecurringInvoiceItems(account.getId(), invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency, perSubscriptionFutureNotificationDates);
+        invoice.addInvoiceItems(fixedAndRecurringItems);
 
-        final List<InvoiceItem> usageItems = generateUsageConsumableInArrearItems(account, invoiceId, events, existingInvoices, targetDate, context);
+        final List<InvoiceItem> usageItems = generateUsageConsumableInArrearItems(account, invoiceId, events, existingInvoices, targetDate, perSubscriptionFutureNotificationDates, context);
         invoice.addInvoiceItems(usageItems);
 
-        return invoice.getInvoiceItems().size() != 0 ? invoice : null;
+        return new InvoiceWithMetadata(invoice.getInvoiceItems().isEmpty() ? null : invoice, perSubscriptionFutureNotificationDates);
     }
 
     private List<InvoiceItem> generateUsageConsumableInArrearItems(final Account account,
                                                                    final UUID invoiceId, final BillingEventSet eventSet,
                                                                    @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
+                                                                   final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates,
                                                                    final InternalCallContext internalCallContext) throws InvoiceApiException {
 
+        final Map<String, LocalDate> perSubscriptionUsage = new HashMap<String, LocalDate>();
+
         final Map<UUID, List<InvoiceItem>> perSubscriptionConsumableInArrearUsageItems = extractPerSubscriptionExistingConsumableInArrearUsageItems(eventSet.getUsages(), existingInvoices);
         try {
             final List<InvoiceItem> items = Lists.newArrayList();
@@ -157,7 +162,11 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
                 if (curSubscriptionId != null && !curSubscriptionId.equals(subscriptionId)) {
                     final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
                     final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
-                    items.addAll(subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of()));
+
+                    final List<InvoiceItem> newProposedItems = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
+                    items.addAll(newProposedItems);
+                    updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, newProposedItems, perSubscriptionFutureNotificationDates);
+
                     curEvents = Lists.newArrayList();
                 }
                 curSubscriptionId = subscriptionId;
@@ -166,7 +175,10 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
             if (curSubscriptionId != null) {
                 final SubscriptionConsumableInArrear subscriptionConsumableInArrear = new SubscriptionConsumableInArrear(invoiceId, curEvents, rawUsageOptimizerResult.getRawUsage(), targetDate, rawUsageOptimizerResult.getRawUsageStartDate());
                 final List<InvoiceItem> consumableInUsageArrearItems = perSubscriptionConsumableInArrearUsageItems.get(curSubscriptionId);
-                items.addAll(subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of()));
+
+                final List<InvoiceItem> newProposedItems = subscriptionConsumableInArrear.computeMissingUsageInvoiceItems(consumableInUsageArrearItems != null ? consumableInUsageArrearItems : ImmutableList.<InvoiceItem>of());
+                items.addAll(newProposedItems);
+                updatePerSubscriptionNextNotificationUsageDate(curSubscriptionId, newProposedItems, perSubscriptionFutureNotificationDates);
             }
             return items;
 
@@ -175,6 +187,19 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         }
     }
 
+
+    private void updatePerSubscriptionNextNotificationUsageDate(final UUID subscriptionId, final List<InvoiceItem> newProposedItems, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+        for (final InvoiceItem item : newProposedItems) {
+            SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
+            if (subscriptionFutureNotificationDates == null) {
+                subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates();
+                perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
+            }
+            subscriptionFutureNotificationDates.updateNextUsageDateIfRequired(item.getUsageName(), item.getEndDate());
+        }
+    }
+
+
     private Map<UUID, List<InvoiceItem>> extractPerSubscriptionExistingConsumableInArrearUsageItems(final Map<String, Usage> knownUsage, @Nullable final List<Invoice> existingInvoices) {
 
         if (existingInvoices == null || existingInvoices.isEmpty()) {
@@ -212,7 +237,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
     private List<InvoiceItem> generateFixedAndRecurringInvoiceItems(final UUID accountId, final UUID invoiceId, final BillingEventSet eventSet,
                                                                     @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
-                                                                    final Currency targetCurrency) throws InvoiceApiException {
+                                                                    final Currency targetCurrency, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
         final AccountItemTree accountItemTree = new AccountItemTree(accountId, invoiceId);
         if (existingInvoices != null) {
             for (final Invoice invoice : existingInvoices) {
@@ -228,8 +253,8 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
         // Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
         final List<InvoiceItem> proposedItems = new ArrayList<InvoiceItem>();
-        generateRecurringInvoiceItems(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems);
-        processFixedPriceEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems);
+        processRecurringBillingEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate);
+        processFixedBillingEvents(invoiceId, accountId, eventSet, targetDate, targetCurrency, proposedItems);
 
         accountItemTree.mergeWithProposedItems(proposedItems);
         return accountItemTree.getResultingItemList();
@@ -258,8 +283,10 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         return maxDate;
     }
 
-    private List<InvoiceItem> generateRecurringInvoiceItems(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
-                                                            final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) throws InvoiceApiException {
+    private List<InvoiceItem> processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
+                                                            final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems,
+                                                            Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate) throws InvoiceApiException {
+
         if (events.size() == 0) {
             return proposedItems;
         }
@@ -278,17 +305,39 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
             if (!events.getSubscriptionIdsWithAutoInvoiceOff().
                     contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
                 final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent.getSubscription().getId()) ? nextEvent : null;
-                proposedItems.addAll(processRecurringEvents(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode()));
+                final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder, events.getRecurringBillingMode());
+                proposedItems.addAll(newProposedItems);
+                updatePerSubscriptionNextNotificationDate(thisEvent.getSubscription().getId(), newProposedItems, perSubscriptionFutureNotificationDate);
             }
         }
-        proposedItems.addAll(processRecurringEvents(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode()));
+        final List<InvoiceItem> newProposedItems = processRecurringEvent(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder, events.getRecurringBillingMode());
+        proposedItems.addAll(newProposedItems);
+        updatePerSubscriptionNextNotificationDate(nextEvent.getSubscription().getId(), newProposedItems, perSubscriptionFutureNotificationDate);
 
         log.info(logStringBuilder.toString());
 
         return proposedItems;
     }
 
-    private List<InvoiceItem> processFixedPriceEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) {
+    private void updatePerSubscriptionNextNotificationDate(final UUID subscriptionId, final List<InvoiceItem> newProposedItems, Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+        for (final InvoiceItem item : newProposedItems) {
+            if ((item.getEndDate() != null) &&
+                (item.getAmount() == null ||
+                 item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+                SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates.get(subscriptionId);
+                if (subscriptionFutureNotificationDates == null) {
+                    subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates();
+                    perSubscriptionFutureNotificationDates.put(subscriptionId, subscriptionFutureNotificationDates);
+                }
+                subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(item.getEndDate());
+            }
+        }
+    }
+
+
+
+
+    private List<InvoiceItem> processFixedBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events, final LocalDate targetDate, final Currency currency, final List<InvoiceItem> proposedItems) {
         final Iterator<BillingEvent> eventIt = events.iterator();
         while (eventIt.hasNext()) {
             final BillingEvent thisEvent = eventIt.next();
@@ -301,11 +350,10 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         return proposedItems;
     }
 
-
     // Turn a set of events into a list of invoice items. Note that the dates on the invoice items will be rounded (granularity of a day)
-    private List<InvoiceItem> processRecurringEvents(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
-                                                     final LocalDate targetDate, final Currency currency,
-                                                     final StringBuilder logStringBuilder, final BillingMode billingMode) throws InvoiceApiException {
+    private List<InvoiceItem> processRecurringEvent(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
+                                                    final LocalDate targetDate, final Currency currency,
+                                                    final StringBuilder logStringBuilder, final BillingMode billingMode) throws InvoiceApiException {
         final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
 
         // Handle recurring items
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
index 8023ee9..3ff29a6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
@@ -32,6 +32,6 @@ import org.killbill.billing.junction.BillingEventSet;
 
 public interface InvoiceGenerator {
 
-    public Invoice generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
+    public InvoiceWithMetadata generateInvoice(Account account, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
                                    LocalDate targetDate, Currency targetCurrency, final InternalCallContext context) throws InvoiceApiException;
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
new file mode 100644
index 0000000..a319245
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.invoice.generator;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.invoice.api.Invoice;
+
+public class InvoiceWithMetadata {
+
+    private final Invoice invoice;
+    private final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates;
+
+    public static class SubscriptionFutureNotificationDates {
+
+        private LocalDate nextRecurringDate;
+        private Map<String, LocalDate> nextUsageDates;
+
+        public SubscriptionFutureNotificationDates() {
+            this.nextRecurringDate = null;
+            this.nextUsageDates = null;
+        }
+
+        public void updateNextRecurringDateIfRequired(final LocalDate nextRecurringDateCandidate) {
+            nextRecurringDate = getMaxDate(nextRecurringDate, nextRecurringDateCandidate);
+        }
+
+        public void updateNextUsageDateIfRequired(final String usageName, final LocalDate nextUsageDateCandidate) {
+            if (nextUsageDates == null) {
+                nextUsageDates = new HashMap<String, LocalDate>();
+            }
+            final LocalDate nextUsageDate = getMaxDate(nextUsageDates.get(usageName), nextUsageDateCandidate);
+            nextUsageDates.put(usageName, nextUsageDate);
+        }
+
+        public LocalDate getNextRecurringDate() {
+            return nextRecurringDate;
+        }
+
+        public Map<String, LocalDate> getNextUsageDates() {
+            return nextUsageDates;
+        }
+
+        private static LocalDate getMaxDate(@Nullable final LocalDate existingDate, final LocalDate nextDateCandidate) {
+            if (existingDate == null) {
+                return nextDateCandidate;
+            } else {
+                return nextDateCandidate.compareTo(existingDate) > 0 ? nextDateCandidate : existingDate;
+            }
+        }
+
+    }
+
+    public InvoiceWithMetadata(final Invoice invoice, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates) {
+        this.invoice = invoice;
+        this.perSubscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates;
+    }
+
+    public Invoice getInvoice() {
+        return invoice;
+    }
+
+    public Map<UUID, SubscriptionFutureNotificationDates> getPerSubscriptionFutureNotificationDates() {
+        return perSubscriptionFutureNotificationDates;
+    }
+
+}
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 d229093..6f4b247 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -68,6 +68,8 @@ import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
 import org.killbill.billing.invoice.dao.InvoiceModelDao;
 import org.killbill.billing.invoice.generator.BillingIntervalDetail;
 import org.killbill.billing.invoice.generator.InvoiceGenerator;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
 import org.killbill.billing.invoice.model.DefaultInvoice;
 import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
 import org.killbill.billing.invoice.model.InvoiceItemFactory;
@@ -203,7 +205,7 @@ public class InvoiceDispatcher {
         }
     }
 
-    public Invoice processAccount(final UUID accountId, final DateTime targetDate,
+    public Invoice processAccount(final UUID accountId,  @Nullable final DateTime targetDate,
                                   @Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
         GlobalLock lock = null;
         try {
@@ -226,12 +228,14 @@ public class InvoiceDispatcher {
                                            @Nullable final DryRunArguments dryRunArguments, final InternalCallContext context) throws InvoiceApiException {
 
         final boolean isDryRun = dryRunArguments != null;
-        // inputTargetDateTime is only allowed in dryRun mode to have the system compute it
+        // A null inputTargetDateTime is only allowed in dryRun mode to have the system compute it
         Preconditions.checkArgument(inputTargetDateTime != null || isDryRun, "inputTargetDateTime is required in non dryRun mode");
         try {
             // Make sure to first set the BCD if needed then get the account object (to have the BCD set)
             final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, dryRunArguments, context);
-
+            if (billingEvents.isEmpty()) {
+                return null;
+            }
             final List<DateTime> candidateDateTimes = (inputTargetDateTime != null) ? ImmutableList.of(inputTargetDateTime) : getUpcomingInvoiceCandidateDates(context);
             for (final DateTime curTargetDateTime : candidateDateTimes) {
                 final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDateTime, billingEvents, isDryRun, context);
@@ -250,9 +254,7 @@ public class InvoiceDispatcher {
                                                              final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext context) throws InvoiceApiException {
         try {
             final Account account = accountApi.getAccountById(accountId, context);
-            final DateAndTimeZoneContext dateAndTimeZoneContext = billingEvents.iterator().hasNext() ?
-                                                                  new DateAndTimeZoneContext(billingEvents.iterator().next().getEffectiveDate(), account.getTimeZone(), clock) :
-                                                                  null;
+            final DateAndTimeZoneContext dateAndTimeZoneContext = new DateAndTimeZoneContext(billingEvents.iterator().next().getEffectiveDate(), account.getTimeZone(), clock);
 
             final List<Invoice> invoices = billingEvents.isAccountAutoInvoiceOff() ?
                                            ImmutableList.<Invoice>of() :
@@ -265,20 +267,30 @@ public class InvoiceDispatcher {
                                                                                                 }));
 
             final Currency targetCurrency = account.getCurrency();
+            final LocalDate targetDate = dateAndTimeZoneContext.computeTargetDate(targetDateTime);
+            final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, billingEvents, invoices, targetDate, targetCurrency, context);
+            final Invoice invoice = invoiceWithMetadata.getInvoice();
+
+            // Compute future notifications
+            final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, billingEvents, dateAndTimeZoneContext, context);
 
-            final LocalDate targetDate = (dateAndTimeZoneContext != null && targetDateTime != null) ? dateAndTimeZoneContext.computeTargetDate(targetDateTime) : null;
-            final Invoice invoice = targetDate != null ? generator.generateInvoice(account, billingEvents, invoices, targetDate, targetCurrency, context) : null;
             //
+
             // If invoice comes back null, there is nothing new to generate, we can bail early
             //
             if (invoice == null) {
-                log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
-                if (!isDryRun) {
+                if (isDryRun) {
+                    log.info("Generated null dryRun invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
+                } else {
+                    log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
+
                     final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(),
                                                                                context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+
+                    commitInvoiceAndSetFutureNotifications(account, null, ImmutableList.<InvoiceItemModelDao>of(), futureAccountNotifications, false, context);
                     postEvent(event, accountId, context);
                 }
-                return invoice;
+                return null;
             }
 
             // Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin
@@ -286,14 +298,39 @@ public class InvoiceDispatcher {
             if (cbaItem != null) {
                 invoice.addInvoiceItem(cbaItem);
             }
-
             //
             // Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
             //
             final CallContext callContext = buildCallContext(context);
             invoice.addInvoiceItems(invoicePluginDispatcher.getAdditionalInvoiceItems(invoice, callContext));
+
+
+
             if (!isDryRun) {
-                commitInvoiceStateAndNotifyAccountIfConfigured(account, invoice, billingEvents, dateAndTimeZoneContext, targetDate, context);
+
+                // Compute whether this is a new invoice object (or just some adjustments on an existing invoice), and extract invoiceIds for later use
+                final Set<UUID> uniqueInvoiceIds = getUniqueInvoiceIds(invoice);
+                final boolean  isRealInvoiceWithItems = uniqueInvoiceIds.remove(invoice.getId());
+                final Set<UUID> adjustedUniqueOtherInvoiceId = uniqueInvoiceIds;
+
+                logInvoiceWithItems(account, invoice, targetDate, adjustedUniqueOtherInvoiceId, isRealInvoiceWithItems);
+
+                // Transformation to Invoice -> InvoiceModelDao
+                final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
+                final Iterable<InvoiceItemModelDao> invoiceItemModelDaos = transformToInvoiceModelDao(invoice.getInvoiceItems());
+
+                // Commit invoice on disk
+                final boolean isThereAnyItemsLeft = commitInvoiceAndSetFutureNotifications(account, invoiceModelDao, invoiceItemModelDaos, futureAccountNotifications, isRealInvoiceWithItems, context);
+
+                final boolean isRealInvoiceWithNonEmptyItems = isThereAnyItemsLeft ? isRealInvoiceWithItems : false;
+
+
+                setChargedThroughDates(dateAndTimeZoneContext, invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), context);
+
+                // TODO we should send bus events when we commit the ionvoice on disk in commitInvoice
+                postEvents(account, invoice, adjustedUniqueOtherInvoiceId, isRealInvoiceWithNonEmptyItems, context);
+
+                notifyAccountIfEnabled(account, invoice, isRealInvoiceWithNonEmptyItems, context);
             }
             return invoice;
         } catch (final AccountApiException e) {
@@ -305,38 +342,88 @@ public class InvoiceDispatcher {
         }
     }
 
-    private void commitInvoiceStateAndNotifyAccountIfConfigured(final Account account, final Invoice invoice, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext, final LocalDate targetDate, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
-        boolean isRealInvoiceWithNonEmptyItems = false;
-        // Extract the set of invoiceId for which we see items that don't belong to current generated invoice
-        final Set<UUID> adjustedUniqueOtherInvoiceId = new TreeSet<UUID>();
-        adjustedUniqueOtherInvoiceId.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function<InvoiceItem, UUID>() {
+
+    private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext, final InternalCallContext context) {
+
+        final Map<UUID, List<SubscriptionNotification>> result = new HashMap<UUID, List<SubscriptionNotification>>();
+
+        for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
+
+            final List<SubscriptionNotification> perSubscriptionNotifications = new ArrayList<SubscriptionNotification>();
+
+            final SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().get(subscriptionId);
+            // Add next recurring date if any
+            if (subscriptionFutureNotificationDates.getNextRecurringDate() != null) {
+                perSubscriptionNotifications.add(new SubscriptionNotification(dateAndTimeZoneContext.computeUTCDateTimeFromLocalDate(subscriptionFutureNotificationDates.getNextRecurringDate()), true));
+            }
+            // Add next usage dates if any
+            if (subscriptionFutureNotificationDates.getNextUsageDates() != null) {
+                for (String usageName : subscriptionFutureNotificationDates.getNextUsageDates().keySet()) {
+                    final LocalDate nextNotificationDateForUsage = subscriptionFutureNotificationDates.getNextUsageDates().get(usageName);
+                    final DateTime subscriptionUsageCallbackDate = getNextUsageBillingDate(subscriptionId, usageName, nextNotificationDateForUsage, dateAndTimeZoneContext, billingEvents);
+                    perSubscriptionNotifications.add(new SubscriptionNotification(subscriptionUsageCallbackDate, true));
+                }
+            }
+            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().getMillis() > 0;
+        if (isInvoiceNotificationEnabled) {
+            final Map<UUID, DateTime> upcomingPhasesForSubscriptions = subscriptionApi.getNextFutureEventForSubscriptions(SubscriptionBaseTransitionType.PHASE, context);
+            for (UUID cur : upcomingPhasesForSubscriptions.keySet()) {
+                final DateTime curDate = upcomingPhasesForSubscriptions.get(cur);
+                List<SubscriptionNotification> resultValue = result.get(cur);
+                if (resultValue == null) {
+                    resultValue = new ArrayList<SubscriptionNotification>();
+                }
+                resultValue.add(new SubscriptionNotification(curDate, false));
+                result.put(cur, resultValue);
+            }
+        }
+        return new FutureAccountNotifications(dateAndTimeZoneContext, result);
+    }
+
+    private Iterable<InvoiceItemModelDao> transformToInvoiceModelDao(final List<InvoiceItem> invoiceItems) {
+        return Iterables.transform(invoiceItems,
+                            new Function<InvoiceItem, InvoiceItemModelDao>() {
+                                @Override
+                                public InvoiceItemModelDao apply(final InvoiceItem input) {
+                                    return new InvoiceItemModelDao(input);
+                                }
+                            });
+    }
+
+    private Set<UUID> getUniqueInvoiceIds(final Invoice invoice) {
+        final Set<UUID> uniqueInvoiceIds = new TreeSet<UUID>();
+        uniqueInvoiceIds.addAll(Collections2.transform(invoice.getInvoiceItems(), new Function<InvoiceItem, UUID>() {
             @Nullable
             @Override
             public UUID apply(@Nullable final InvoiceItem input) {
                 return input.getInvoiceId();
             }
         }));
-        boolean isRealInvoiceWithItems = adjustedUniqueOtherInvoiceId.remove(invoice.getId());
+        return uniqueInvoiceIds;
+    }
+
+    private void logInvoiceWithItems(final Account account, final Invoice invoice, final LocalDate targetDate, final Set<UUID> adjustedUniqueOtherInvoiceId, final boolean isRealInvoiceWithItems) {
         if (isRealInvoiceWithItems) {
             log.info("Generated invoice {} with {} items for accountId {} and targetDate {}", new Object[]{invoice.getId(), invoice.getNumberOfItems(), account.getId(), targetDate});
         } else {
             final Joiner joiner = Joiner.on(",");
             final String adjustedInvoices = joiner.join(adjustedUniqueOtherInvoiceId.toArray(new UUID[adjustedUniqueOtherInvoiceId.size()]));
             log.info("Adjusting existing invoices {} with {} items for accountId {} and targetDate {})", new Object[]{adjustedInvoices, invoice.getNumberOfItems(),
-                                                                                                                                         account.getId(), targetDate});
+                                                                                                                      account.getId(), targetDate});
         }
+    }
 
-        // Transformation to Invoice -> InvoiceModelDao
-        final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
-        final Iterable<InvoiceItemModelDao> invoiceItemModelDaos = Iterables.transform(invoice.getInvoiceItems(),
-                                                                                       new Function<InvoiceItem, InvoiceItemModelDao>() {
-                                                                                           @Override
-                                                                                           public InvoiceItemModelDao apply(final InvoiceItem input) {
-                                                                                               return new InvoiceItemModelDao(input);
-                                                                                           }
-                                                                                       });
-        final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceItemModelDaos, billingEvents, dateAndTimeZoneContext, context);
 
+    private boolean commitInvoiceAndSetFutureNotifications(final Account account, final InvoiceModelDao invoiceModelDao,
+                                                           final Iterable<InvoiceItemModelDao> invoiceItemModelDaos,
+                                                           final FutureAccountNotifications futureAccountNotifications,
+                                                           boolean isRealInvoiceWithItems, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
         // We filter any zero amount for USAGE items prior we generate the invoice, which may leave us with an invoice with no items;
         // we recompute the isRealInvoiceWithItems flag based on what is left (the call to invoice is still necessary to set the future notifications).
         final Iterable<InvoiceItemModelDao> filteredInvoiceItemModelDaos = Iterables.filter(invoiceItemModelDaos, new Predicate<InvoiceItemModelDao>() {
@@ -347,17 +434,15 @@ public class InvoiceDispatcher {
         });
 
         final boolean isThereAnyItemsLeft = filteredInvoiceItemModelDaos.iterator().hasNext();
-        isRealInvoiceWithNonEmptyItems = isThereAnyItemsLeft ? isRealInvoiceWithItems : false;
-
         if (isThereAnyItemsLeft) {
             invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(filteredInvoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
         } else {
             invoiceDao.setFutureAccountNotificationsForEmptyInvoice(account.getId(), futureAccountNotifications, context);
         }
+        return isThereAnyItemsLeft;
+    }
 
-        final List<InvoiceItem> fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
-        final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
-        setChargedThroughDates(dateAndTimeZoneContext, fixedPriceInvoiceItems, recurringInvoiceItems, context);
+    private void postEvents(final Account account, final Invoice invoice, final Set<UUID> adjustedUniqueOtherInvoiceId, final boolean isRealInvoiceWithNonEmptyItems, final InternalCallContext context) {
 
         final List<InvoiceInternalEvent> events = new ArrayList<InvoiceInternalEvent>();
         if (isRealInvoiceWithNonEmptyItems) {
@@ -370,11 +455,12 @@ public class InvoiceDispatcher {
                                                                                            context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
             events.add(event);
         }
-
         for (final InvoiceInternalEvent event : events) {
             postEvent(event, account.getId(), context);
         }
+    }
 
+    private void notifyAccountIfEnabled(final Account account, final Invoice invoice, final boolean isRealInvoiceWithNonEmptyItems, final InternalCallContext context) throws InvoiceApiException {
         if (account.isNotifiedForInvoices() && isRealInvoiceWithNonEmptyItems) {
             // Need to re-hydrate the invoice object to get the invoice number (record id)
             // API_FIX InvoiceNotifier public API?
@@ -382,6 +468,7 @@ public class InvoiceDispatcher {
         }
     }
 
+
     private InvoiceItem computeCBAOnExistingInvoice(final Invoice invoice, final InternalCallContext context) throws InvoiceApiException {
         // Transformation to Invoice -> InvoiceModelDao
         final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
@@ -405,75 +492,6 @@ public class InvoiceDispatcher {
         return internalCallContextFactory.createCallContext(context);
     }
 
-    @VisibleForTesting
-    FutureAccountNotifications createNextFutureNotificationDate(final Iterable<InvoiceItemModelDao> invoiceItems, final BillingEventSet billingEvents, final DateAndTimeZoneContext dateAndTimeZoneContext, final InternalCallContext context) {
-
-        final Map<UUID, List<SubscriptionNotification>> result = new HashMap<UUID, List<SubscriptionNotification>>();
-
-        final Map<String, LocalDate> perSubscriptionUsage = new HashMap<String, LocalDate>();
-
-        // For each subscription that has a positive (amount) recurring item, create the date
-        // at which we should be called back for next invoice.
-        //
-        for (final InvoiceItemModelDao item : invoiceItems) {
-
-            List<SubscriptionNotification> perSubscriptionCallback = result.get(item.getSubscriptionId());
-            if (perSubscriptionCallback == null && (item.getType() == InvoiceItemType.RECURRING || item.getType() == InvoiceItemType.USAGE)) {
-                perSubscriptionCallback = new ArrayList<SubscriptionNotification>();
-                result.put(item.getSubscriptionId(), perSubscriptionCallback);
-            }
-
-            switch (item.getType()) {
-                case RECURRING:
-                    if ((item.getEndDate() != null) &&
-                        (item.getAmount() == null ||
-                         item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
-                        perSubscriptionCallback.add(new SubscriptionNotification(dateAndTimeZoneContext.computeUTCDateTimeFromLocalDate(item.getEndDate()), true));
-                    }
-                    break;
-
-                case USAGE:
-                    final String key = item.getSubscriptionId().toString() + ":" + item.getUsageName();
-                    final LocalDate perSubscriptionUsageRecurringDate = perSubscriptionUsage.get(key);
-                    if (perSubscriptionUsageRecurringDate == null || perSubscriptionUsageRecurringDate.compareTo(item.getEndDate()) < 0) {
-                        perSubscriptionUsage.put(key, item.getEndDate());
-                    }
-                    break;
-
-                default:
-                    // Ignore
-            }
-        }
-
-        for (final String key : perSubscriptionUsage.keySet()) {
-            final String[] parts = key.split(":");
-            final UUID subscriptionId = UUID.fromString(parts[0]);
-
-            final List<SubscriptionNotification> perSubscriptionCallback = result.get(subscriptionId);
-            final String usageName = parts[1];
-            final LocalDate endDate = perSubscriptionUsage.get(key);
-
-            final DateTime subscriptionUsageCallbackDate = getNextUsageBillingDate(subscriptionId, usageName, endDate, dateAndTimeZoneContext, billingEvents);
-            perSubscriptionCallback.add(new SubscriptionNotification(subscriptionUsageCallbackDate, true));
-        }
-
-        // If dryRunNotification is enabled we also need to fetch the upcoming PHASE dates (we add SubscriptionNotification with isForInvoiceNotificationTrigger = false)
-        final boolean isInvoiceNotificationEnabled = invoiceConfig.getDryRunNotificationSchedule().getMillis() > 0;
-        if (isInvoiceNotificationEnabled) {
-            final Map<UUID, DateTime> upcomingPhasesForSubscriptions = subscriptionApi.getNextFutureEventForSubscriptions(SubscriptionBaseTransitionType.PHASE, context);
-            for (UUID cur : upcomingPhasesForSubscriptions.keySet()) {
-                final DateTime curDate = upcomingPhasesForSubscriptions.get(cur);
-                List<SubscriptionNotification> resultValue = result.get(cur);
-                if (resultValue == null) {
-                    resultValue = new ArrayList<SubscriptionNotification>();
-                }
-                resultValue.add(new SubscriptionNotification(curDate, false));
-                result.put(cur, resultValue);
-            }
-        }
-        return new FutureAccountNotifications(dateAndTimeZoneContext, result);
-    }
-
     private DateTime getNextUsageBillingDate(final UUID subscriptionId, final String usageName, final LocalDate chargedThroughDate, final DateAndTimeZoneContext dateAndTimeZoneContext, final BillingEventSet billingEvents) {
 
         final Usage usage = billingEvents.getUsages().get(usageName);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultBillingModeGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultBillingModeGenerator.java
index cb8f6f8..29bcc1f 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultBillingModeGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultBillingModeGenerator.java
@@ -54,7 +54,7 @@ public class DefaultBillingModeGenerator implements BillingModeGenerator {
 
         final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod, billingMode);
 
-        // We are not billing for less than a day (we could...)
+        // We are not billing for less than a day
         if (!billingIntervalDetail.hasSomethingToBill()) {
             return results;
         }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index 97cd232..e1ad03a 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -53,6 +53,7 @@ import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.api.InvoicePayment;
 import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata;
 import org.killbill.billing.invoice.model.CreditAdjInvoiceItem;
 import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import org.killbill.billing.invoice.model.DefaultInvoice;
@@ -556,7 +557,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         final UUID accountId = account.getId();
         final UUID bundleId = UUID.randomUUID();
         final LocalDate targetDate1 = new LocalDate(2011, 10, 6);
-        final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
+        final Invoice  invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate1, Currency.USD);
         invoiceUtil.createInvoice(invoice1, true, context);
 
         final LocalDate startDate = new LocalDate(2011, 3, 1);
@@ -1109,7 +1110,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         final BillingEventSet events = new MockBillingEventSet();
         events.add(event1);
 
-        final Invoice invoice1 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertEquals(invoice1.getBalance(), KillBillMoney.of(TEN, invoice1.getCurrency()));
         invoiceList.add(invoice1);
 
@@ -1127,7 +1129,9 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
 
         // second invoice should be for one half (14/28 days) the difference between the rate plans
         // this is a temporary state, since it actually contains an adjusting item that properly belong to invoice 1
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, context);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
+
         assertEquals(invoice2.getBalance(), KillBillMoney.of(FIVE, invoice2.getCurrency()));
         invoiceList.add(invoice2);
 
@@ -1159,7 +1163,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         events.add(event);
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, context);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
 
         // expect one pro-ration item and one full-period item
         assertEquals(invoice.getNumberOfItems(), 2);
@@ -1202,7 +1207,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         events.add(event1);
 
         final UUID accountId = account.getId();
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate1), Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate1), Currency.USD, context);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertNotNull(invoice1);
         assertEquals(invoice1.getNumberOfItems(), 1);
         assertEquals(invoice1.getBalance().compareTo(ZERO), 0);
@@ -1218,7 +1224,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
                                                                        "testEvent2", 2L, SubscriptionBaseTransitionType.PHASE);
         events.add(event2);
 
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate2), Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate2), Currency.USD, context);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertNotNull(invoice2);
         assertEquals(invoice2.getNumberOfItems(), 1);
         assertEquals(invoice2.getBalance().compareTo(cheapAmount), 0);
@@ -1228,7 +1235,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         //invoiceUtil.createInvoice(invoice2, invoice2.getTargetDate().getDayOfMonth(), callcontext);
 
         final DateTime effectiveDate3 = effectiveDate2.plusMonths(1);
-        final Invoice invoice3 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate3), Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, new LocalDate(effectiveDate3), Currency.USD, context);
+        final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
         assertNotNull(invoice3);
         assertEquals(invoice3.getNumberOfItems(), 1);
         assertEquals(invoice3.getBalance().compareTo(cheapAmount), 0);
@@ -1239,7 +1247,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
     @Test(groups = "slow")
     public void testInvoiceForEmptyEventSet() throws InvoiceApiException {
         final BillingEventSet events = new MockBillingEventSet();
-        final Invoice invoice = generator.generateInvoice(account, events, null, new LocalDate(), Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, new LocalDate(), Currency.USD, context);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNull(invoice);
     }
 
@@ -1273,7 +1282,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
                                                                        "testEvent2", 2L, SubscriptionBaseTransitionType.CHANGE);
         events.add(event2);
 
-        final Invoice invoice = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate2), Currency.USD, context);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, new LocalDate(effectiveDate2), Currency.USD, context);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 2);
         assertEquals(invoice.getBalance().compareTo(cheapAmount), 0);
@@ -1344,7 +1354,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
                                                                        BillingPeriod.MONTHLY, 31, BillingMode.IN_ADVANCE,
                                                                        "new-event", 1L, SubscriptionBaseTransitionType.CREATE);
         events.add(event1);
-        final Invoice newInvoice = generator.generateInvoice(account, events, invoices, targetDate, Currency.USD, context);
+        final InvoiceWithMetadata newInvoiceWithMetadata = generator.generateInvoice(account, events, invoices, targetDate, Currency.USD, context);
+        final Invoice newInvoice = newInvoiceWithMetadata.getInvoice();
         invoiceUtil.createInvoice(newInvoice, true, context);
 
         // VERIFY THAT WE STILL HAVE ONLY 2 ITEMS, MEANING THERE WERE NO REPAIR AND NO CBA GENERATED
@@ -1379,7 +1390,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
                                                                        "testEvent1", 1L, SubscriptionBaseTransitionType.CHANGE);
         events.add(event1);
 
-        Invoice invoice1 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate1), Currency.USD, context);
+        InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate1), Currency.USD, context);
+        Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         invoices.add(invoice1);
         invoiceUtil.createInvoice(invoice1, true, context);
         invoice1 = new DefaultInvoice(invoiceDao.getById(invoice1.getId(), context));
@@ -1390,7 +1402,8 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
                                                                        BillingPeriod.MONTHLY, 31, BillingMode.IN_ADVANCE,
                                                                        "testEvent2", 2L, SubscriptionBaseTransitionType.CHANGE);
         events.add(event2);
-        Invoice invoice2 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate2), Currency.USD, context);
+        InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, new LocalDate(targetDate2), Currency.USD, context);
+        Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         invoiceUtil.createInvoice(invoice2, true, context);
         invoice2 = new DefaultInvoice(invoiceDao.getById(invoice2.getId(), context));
         assertNotNull(invoice2.getInvoiceNumber());
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 9094ddc..cadda9d 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -146,15 +146,15 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
     @Test(groups = "fast")
     public void testWithNullEventSetAndNullInvoiceSet() throws InvoiceApiException {
-        final Invoice invoice = generator.generateInvoice(account, null, null, clock.getUTCToday(), Currency.USD, internalCallContext);
-        assertNull(invoice);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, null, null, clock.getUTCToday(), Currency.USD, internalCallContext);
+        assertNull(invoiceWithMetadata.getInvoice());
     }
 
     @Test(groups = "fast")
     public void testWithEmptyEventSet() throws InvoiceApiException {
         final BillingEventSet events = new MockBillingEventSet();
-        final Invoice invoice = generator.generateInvoice(account, events, null, clock.getUTCToday(), Currency.USD, internalCallContext);
-        assertNull(invoice);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, clock.getUTCToday(), Currency.USD, internalCallContext);
+        assertNull(invoiceWithMetadata.getInvoice());
     }
 
     @Test(groups = "fast")
@@ -172,8 +172,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event);
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 2);
         assertEquals(invoice.getBalance(), KillBillMoney.of(TWENTY, invoice.getCurrency()));
@@ -209,8 +209,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         // Target date is the next BCD, in local time
         final LocalDate targetDate = invoiceUtil.buildDate(2012, 8, bcdLocal);
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 2);
         assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), invoiceUtil.buildDate(2012, 7, 16));
@@ -233,8 +233,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         // Set a target date of today (start date)
         final LocalDate targetDate = startDate;
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 1);
         assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), invoiceUtil.buildDate(2012, 7, 16));
@@ -255,8 +255,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event);
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 2);
 
@@ -287,8 +287,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event2);
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 2);
         assertEquals(invoice.getBalance(), KillBillMoney.of(rate1.add(rate2), invoice.getCurrency()));
@@ -313,8 +313,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 3);
         final UUID accountId = UUID.randomUUID();
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 4);
 
@@ -354,8 +354,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event3);
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 3);
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), 4);
         assertEquals(invoice.getBalance(), KillBillMoney.of(rate1.add(rate2).add(TWO.multiply(rate3)), invoice.getCurrency()));
@@ -376,13 +376,14 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event1);
 
         LocalDate targetDate = invoiceUtil.buildDate(2011, 12, 1);
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
         final List<Invoice> existingInvoices = new ArrayList<Invoice>();
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         existingInvoices.add(invoice1);
 
         targetDate = invoiceUtil.buildDate(2011, 12, 3);
-        final Invoice invoice2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, existingInvoices, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertNull(invoice2);
     }
 
@@ -554,8 +555,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1);
         events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), targetDate, plan, planPhase, 1));
 
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
-
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertEquals(invoice.getNumberOfItems(), 1);
     }
 
@@ -569,9 +570,11 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         events.add(createBillingEvent(UUID.randomUUID(), UUID.randomUUID(), startDate, plan, planPhase, startDate.getDayOfMonth()));
 
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         final RecurringInvoiceItem item = (RecurringInvoiceItem) invoice.getInvoiceItems().get(0);
 
+
         // end date of the invoice item should be equal to exactly one month later (rounded)
         assertEquals(item.getEndDate(), startDate.plusMonths(1));
     }
@@ -605,13 +608,15 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         events.add(event2);
         events.add(event1);
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, new LocalDate("2012-02-01"), Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, new LocalDate("2012-02-01"), Currency.USD, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertNotNull(invoice1);
         assertEquals(invoice1.getNumberOfItems(), 1);
 
         final List<Invoice> invoiceList = new ArrayList<Invoice>();
         invoiceList.add(invoice1);
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, new LocalDate("2012-04-05"), Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, new LocalDate("2012-04-05"), Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertNotNull(invoice2);
         assertEquals(invoice2.getNumberOfItems(), 1);
         final FixedPriceInvoiceItem item = (FixedPriceInvoiceItem) invoice2.getInvoiceItems().get(0);
@@ -635,7 +640,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event1);
 
         // ensure both components are invoiced
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertNotNull(invoice1);
         assertEquals(invoice1.getNumberOfItems(), 2);
         assertEquals(invoice1.getBalance(), KillBillMoney.of(FIFTEEN, invoice1.getCurrency()));
@@ -647,7 +653,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final LocalDate currentDate = startDate.plusMonths(1);
 
         // ensure that only the recurring price is invoiced
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, currentDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, currentDate, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertNotNull(invoice2);
         assertEquals(invoice2.getNumberOfItems(), 1);
         assertEquals(invoice2.getBalance(), KillBillMoney.of(FIVE, invoice2.getCurrency()));
@@ -672,8 +679,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event1);
 
         // ensure that a single invoice item is generated for the fixed cost
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext);
-        assertNotNull(invoice1);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, startDate, Currency.USD, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();assertNotNull(invoice1);
         assertEquals(invoice1.getNumberOfItems(), 1);
         assertEquals(invoice1.getBalance(), KillBillMoney.of(fixedCost1, invoice1.getCurrency()));
 
@@ -686,8 +693,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(event2);
 
         // ensure that a single invoice item is generated for the fixed cost
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, phaseChangeDate, Currency.USD, internalCallContext);
-        assertNotNull(invoice2);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, phaseChangeDate, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertEquals(invoice2.getNumberOfItems(), 1);
         assertEquals(invoice2.getBalance(), KillBillMoney.of(fixedCost2, invoice2.getCurrency()));
     }
@@ -718,7 +725,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final LocalDate discountPhaseEndDate = trialPhaseEndDate.plusMonths(6);
         events.add(createBillingEvent(subscriptionId, bundleId, discountPhaseEndDate, plan1, phase3, BILL_CYCLE_DAY));
 
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, creationDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, creationDate, Currency.USD, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertNotNull(invoice1);
         assertEquals(invoice1.getNumberOfItems(), 1);
         assertEquals(invoice1.getBalance().compareTo(ZERO), 0);
@@ -726,7 +734,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final List<Invoice> invoiceList = new ArrayList<Invoice>();
         invoiceList.add(invoice1);
 
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoiceList, trialPhaseEndDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoiceList, trialPhaseEndDate, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertNotNull(invoice2);
         assertEquals(invoice2.getNumberOfItems(), 1);
         assertEquals(invoice2.getInvoiceItems().get(0).getStartDate(), trialPhaseEndDate);
@@ -734,7 +743,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         invoiceList.add(invoice2);
         LocalDate targetDate = new LocalDate(trialPhaseEndDate.getYear(), trialPhaseEndDate.getMonthOfYear(), BILL_CYCLE_DAY);
-        final Invoice invoice3 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
         assertNotNull(invoice3);
         assertEquals(invoice3.getNumberOfItems(), 1);
         assertEquals(invoice3.getInvoiceItems().get(0).getStartDate(), targetDate);
@@ -742,7 +752,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         invoiceList.add(invoice3);
         targetDate = targetDate.plusMonths(6);
-        final Invoice invoice4 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata4 = generator.generateInvoice(account, events, invoiceList, targetDate, Currency.USD, internalCallContext);
+        final Invoice invoice4 = invoiceWithMetadata4.getInvoice();
         assertNotNull(invoice4);
         assertEquals(invoice4.getNumberOfItems(), 7);
     }
@@ -801,7 +812,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
                                        final LocalDate targetDate, final int expectedNumberOfItems,
                                        final BigDecimal expectedAmount) throws InvoiceApiException {
         final Currency currency = Currency.USD;
-        final Invoice invoice = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertNotNull(invoice);
         assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
         existingInvoices.add(invoice);
@@ -827,7 +839,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(createBillingEvent(baseSubscription.getId(), baseSubscription.getBundleId(), april25, basePlan, basePlanEvergreen, 25));
 
         // generate invoice
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertNotNull(invoice1);
         assertEquals(invoice1.getNumberOfItems(), 1);
         assertEquals(invoice1.getBalance().compareTo(TEN), 0);
@@ -848,7 +861,9 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(createBillingEvent(addOnSubscription2.getId(), baseSubscription.getBundleId(), april28, addOn2Plan, addOn2PlanPhaseEvergreen, 25));
 
         // generate invoice
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoices, april28, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, april28, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
+
         invoices.add(invoice2);
         assertNotNull(invoice2);
         assertEquals(invoice2.getNumberOfItems(), 2);
@@ -865,7 +880,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         // generate invoice
         final LocalDate may1 = new LocalDate(2012, 5, 1);
-        final Invoice invoice3 = generator.generateInvoice(account, newEvents, invoices, may1, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, newEvents, invoices, may1, Currency.USD, internalCallContext);
+        final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
         assertNotNull(invoice3);
         assertEquals(invoice3.getNumberOfItems(), 3);
         // -4.50 -18 - 10 (to correct the previous 2 invoices) + 4.50 + 13
@@ -887,7 +903,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final BillingEventSet events = new MockBillingEventSet();
         events.add(createBillingEvent(originalSubscription.getId(), originalSubscription.getBundleId(), april25, originalPlan, originalPlanEvergreen, 25));
 
-        final Invoice invoice1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, events, null, april25, Currency.USD, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
 
         printDetailInvoice(invoice1);
 
@@ -909,7 +926,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         events.add(createBillingEvent(newSubscription.getId(), originalSubscription.getBundleId(), april25, newPlan, newPlanEvergreen, 25));
 
         // generate a new invoice
-        final Invoice invoice2 = generator.generateInvoice(account, events, invoices, april25, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, events, invoices, april25, Currency.USD, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
 
         printDetailInvoice(invoice2);
         assertEquals(invoice2.getNumberOfItems(), 2);
@@ -973,7 +991,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         // Generate a new invoice
 
-        final Invoice invoice = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+        final Invoice invoice = invoiceWithMetadata.getInvoice();
         assertEquals(invoice.getNumberOfItems(), 7);
         assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
         assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2013, 6, 15));
@@ -1007,7 +1026,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         existingInvoices.add(invoice);
 
         // Generate next invoice (no-op)
-        final Invoice newInvoice = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+        final InvoiceWithMetadata newInvoiceWithMetdata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+        final Invoice newInvoice = newInvoiceWithMetdata.getInvoice();
         assertNull(newInvoice);
     }
 
@@ -1054,9 +1074,9 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
         final LocalDate targetDate = invoiceUtil.buildDate(2011, 10, 3);
         final UUID accountId = UUID.randomUUID();
-        final Invoice invoice = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
 
-        assertNull(invoice);
+        assertNull(invoiceWithMetadata.getInvoice());
     }
 
     public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceApiException {
@@ -1082,7 +1102,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         eventSet.add(createBillingEvent(subscriptionId2, bundleId, startDate, plan2, plan2phase1, 1));
 
         // generate the first invoice
-        final Invoice invoice1 = generator.generateInvoice(account, eventSet, invoices, startDate, currency, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata1 = generator.generateInvoice(account, eventSet, invoices, startDate, currency, internalCallContext);
+        final Invoice invoice1 = invoiceWithMetadata1.getInvoice();
         assertNotNull(invoice1);
         assertTrue(invoice1.getBalance().compareTo(FIFTEEN.add(TWELVE)) == 0);
         invoices.add(invoice1);
@@ -1093,7 +1114,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         eventSet.addSubscriptionWithAutoInvoiceOff(subscriptionId1);
 
         final LocalDate targetDate2 = startDate.plusMonths(1);
-        final Invoice invoice2 = generator.generateInvoice(account, eventSet, invoices, targetDate2, currency, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata2 = generator.generateInvoice(account, eventSet, invoices, targetDate2, currency, internalCallContext);
+        final Invoice invoice2 = invoiceWithMetadata2.getInvoice();
         assertNotNull(invoice2);
         assertTrue(invoice2.getBalance().compareTo(TWELVE) == 0);
         invoices.add(invoice2);
@@ -1101,7 +1123,8 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final LocalDate targetDate3 = targetDate2.plusMonths(1);
         eventSet.clearSubscriptionsWithAutoInvoiceOff();
         eventSet.add(subscription1creation);
-        final Invoice invoice3 = generator.generateInvoice(account, eventSet, invoices, targetDate3, currency, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata3 = generator.generateInvoice(account, eventSet, invoices, targetDate3, currency, internalCallContext);
+        final Invoice invoice3 = invoiceWithMetadata3.getInvoice();
         assertNotNull(invoice3);
         assertTrue(invoice3.getBalance().compareTo(FIFTEEN.multiply(TWO).add(TWELVE)) == 0);
     }
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 5929b0b..2bdde60 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -19,12 +19,10 @@
 package org.killbill.billing.invoice;
 
 import java.math.BigDecimal;
-import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
@@ -38,8 +36,6 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.TestInvoiceHelper.DryRunFutureDateArguments;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.invoice.api.Invoice;
@@ -47,14 +43,11 @@ 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.api.InvoiceNotifier;
-import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
 import org.killbill.billing.invoice.dao.InvoiceModelDao;
 import org.killbill.billing.invoice.notification.NullInvoiceNotifier;
 import org.killbill.billing.junction.BillingEventSet;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
-import org.killbill.billing.util.timezone.DateAndTimeZoneContext;
-import org.killbill.clock.ClockMock;
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
@@ -82,7 +75,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         final BillingEventSet events = new MockBillingEventSet();
         final Plan plan = MockPlan.createBicycleNoTrialEvergreen1USD();
         final PlanPhase planPhase = MockPlanPhase.create1USDMonthlyEvergreen();
-        final DateTime effectiveDate = new DateTime().minusDays(1);
+        final DateTime effectiveDate = clock.getUTCNow().minusDays(1);
         final Currency currency = Currency.USD;
         final BigDecimal fixedPrice = null;
         events.add(invoiceUtil.createMockBillingEvent(account, subscription, effectiveDate, plan, planPhase,
@@ -91,7 +84,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
 
         Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
 
-        final DateTime target = new DateTime();
+        final DateTime target = effectiveDate;
 
         final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
         final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
@@ -190,35 +183,4 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
             Assert.assertEquals(item.getSubscriptionId(), subscription.getId());
         }
     }
-
-    @Test(groups = "slow")
-    public void testCreateNextFutureNotificationDate() throws Exception {
-
-        final LocalDate startDate = new LocalDate("2012-10-26");
-        final LocalDate endDate = new LocalDate("2012-11-26");
-
-        ((ClockMock) clock).setTime(new DateTime(2012, 10, 13, 1, 12, 23, DateTimeZone.UTC));
-
-        final DateAndTimeZoneContext dateAndTimeZoneContext = new DateAndTimeZoneContext(clock.getUTCNow(), DateTimeZone.forID("Pacific/Pitcairn"), clock);
-
-        final InvoiceItemModelDao item = new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.RECURRING, UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(),
-                                                                 null, "planName", "phaseName", null, startDate, endDate, new BigDecimal("23.9"), new BigDecimal("23.9"), Currency.EUR, null);
-
-        final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
-        final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
-                                                                   internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
-                                                                   null, invoiceConfig, clock);
-
-        final FutureAccountNotifications futureAccountNotifications = dispatcher.createNextFutureNotificationDate(Collections.singletonList(item), null, dateAndTimeZoneContext, context);
-
-        Assert.assertEquals(futureAccountNotifications.getNotifications().size(), 1);
-
-        final List<SubscriptionNotification> receivedDates = futureAccountNotifications.getNotifications().get(item.getSubscriptionId());
-        Assert.assertEquals(receivedDates.size(), 1);
-
-        final LocalDate receivedTargetDate = new LocalDate(receivedDates.get(0).getEffectiveDate(), DateTimeZone.forID("Pacific/Pitcairn"));
-        Assert.assertEquals(receivedTargetDate, endDate);
-
-        Assert.assertTrue(receivedDates.get(0).getEffectiveDate().compareTo(new DateTime(2012, 11, 27, 1, 12, 23, DateTimeZone.UTC)) <= 0);
-    }
 }