killbill-memoizeit

Merge pull request #923 from killbill/fix-for-893 invoice:

3/27/2018 4:03:54 AM

Changes

pom.xml 2(+1 -1)

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
index c8780b4..17a7e68 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithInvoicePlugin.java
@@ -45,8 +45,12 @@ import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
 import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
 import org.killbill.billing.invoice.model.TaxInvoiceItem;
 import org.killbill.billing.invoice.notification.DefaultNextBillingDateNotifier;
+import org.killbill.billing.invoice.plugin.api.InvoiceContext;
 import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
 import org.killbill.billing.invoice.plugin.api.InvoicePluginApiRetryException;
+import org.killbill.billing.invoice.plugin.api.OnFailureInvoiceResult;
+import org.killbill.billing.invoice.plugin.api.OnSuccessInvoiceResult;
+import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
 import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
@@ -402,6 +406,11 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
         InvoiceItem additionalInvoiceItem;
 
         @Override
+        public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+            return null;
+        }
+
+        @Override
         public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) {
             if (shouldThrowException) {
                 throw new InvoicePluginApiRetryException();
@@ -412,6 +421,16 @@ public class TestWithInvoicePlugin extends TestIntegrationBase {
             }
         }
 
+        @Override
+        public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+            return null;
+        }
+
+        @Override
+        public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+            return null;
+        }
+
         private InvoiceItem createTaxInvoiceItem(final Invoice invoice) {
             return new TaxInvoiceItem(invoice.getId(), invoice.getAccountId(), null, "Tax Item", clock.getUTCNow().toLocalDate(), BigDecimal.ONE, invoice.getCurrency());
         }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
index c02877a..d0cfcee 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
@@ -40,7 +40,11 @@ import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
 import org.killbill.billing.invoice.model.TaxInvoiceItem;
+import org.killbill.billing.invoice.plugin.api.InvoiceContext;
 import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
+import org.killbill.billing.invoice.plugin.api.OnFailureInvoiceResult;
+import org.killbill.billing.invoice.plugin.api.OnSuccessInvoiceResult;
+import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
 import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
@@ -293,15 +297,30 @@ public class TestWithTaxItems extends TestIntegrationBase {
         }
 
         @Override
+        public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+            return null;
+        }
+
+        @Override
         public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) {
             final List<InvoiceItem> result = new ArrayList<InvoiceItem>();
-            for (TaxInvoiceItem item : taxItems) {
+            for (final TaxInvoiceItem item : taxItems) {
                 result.add(new TaxInvoiceItem(item.getId(), invoice.getId(), invoice.getAccountId(), item.getBundleId(), "Tax Item", item.getStartDate(), item.getAmount(), invoice.getCurrency()));
             }
             taxItems.clear();
             return result;
         }
 
+        @Override
+        public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+            return null;
+        }
+
+        @Override
+        public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> iterable) {
+            return null;
+        }
+
         public void reset() {
             taxItems.clear();
         }
@@ -309,6 +328,5 @@ public class TestWithTaxItems extends TestIntegrationBase {
         public void addTaxItem(final TaxInvoiceItem item) {
             taxItems.add(item);
         }
-
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceContext.java b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceContext.java
new file mode 100644
index 0000000..8f7f37b
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceContext.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * 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.api;
+
+import java.util.List;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.invoice.plugin.api.InvoiceContext;
+import org.killbill.billing.util.callcontext.CallContext;
+
+public class DefaultInvoiceContext extends DefaultCallContext implements InvoiceContext {
+
+    private final LocalDate targetDate;
+    private final Invoice invoice;
+    private final List<Invoice> existingInvoices;
+    private final boolean isDryRun;
+    private final boolean isRescheduled;
+
+    public DefaultInvoiceContext(final LocalDate targetDate,
+                                 final Invoice invoice,
+                                 final List<Invoice> existingInvoices,
+                                 final boolean isDryRun,
+                                 final boolean isRescheduled,
+                                 final CallContext context) {
+        super(context.getAccountId(),
+              context.getTenantId(),
+              context.getUserName(),
+              context.getCallOrigin(),
+              context.getUserType(),
+              context.getReasonCode(),
+              context.getComments(),
+              context.getUserToken(),
+              context.getCreatedDate(),
+              context.getUpdatedDate());
+        this.targetDate = targetDate;
+        this.invoice = invoice;
+        this.existingInvoices = existingInvoices;
+        this.isDryRun = isDryRun;
+        this.isRescheduled = isRescheduled;
+    }
+
+    @Override
+    public LocalDate getTargetDate() {
+        return targetDate;
+    }
+
+    @Override
+    public Invoice getInvoice() {
+        return invoice;
+    }
+
+    @Override
+    public List<Invoice> getExistingInvoices() {
+        return existingInvoices;
+    }
+
+    @Override
+    public boolean isDryRun() {
+        return isDryRun;
+    }
+
+    @Override
+    public boolean isRescheduled() {
+        return isRescheduled;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultInvoiceContext{");
+        sb.append("targetDate=").append(targetDate);
+        sb.append(", invoice=").append(invoice);
+        sb.append(", isDryRun=").append(isDryRun);
+        sb.append(", isRescheduled=").append(isRescheduled);
+        sb.append(", accountId=").append(accountId);
+        sb.append(", tenantId=").append(tenantId);
+        sb.append(", userToken=").append(userToken);
+        sb.append(", userName='").append(userName).append('\'');
+        sb.append(", callOrigin=").append(callOrigin);
+        sb.append(", userType=").append(userType);
+        sb.append(", reasonCode='").append(reasonCode).append('\'');
+        sb.append(", comments='").append(comments).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        final DefaultInvoiceContext that = (DefaultInvoiceContext) o;
+
+        if (isDryRun != that.isDryRun) {
+            return false;
+        }
+        if (isRescheduled != that.isRescheduled) {
+            return false;
+        }
+        if (targetDate != null ? targetDate.compareTo(that.targetDate) != 0 : that.targetDate != null) {
+            return false;
+        }
+        if (invoice != null ? !invoice.equals(that.invoice) : that.invoice != null) {
+            return false;
+        }
+        return existingInvoices != null ? existingInvoices.equals(that.existingInvoices) : that.existingInvoices == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (targetDate != null ? targetDate.hashCode() : 0);
+        result = 31 * result + (invoice != null ? invoice.hashCode() : 0);
+        result = 31 * result + (existingInvoices != null ? existingInvoices.hashCode() : 0);
+        result = 31 * result + (isDryRun ? 1 : 0);
+        result = 31 * result + (isRescheduled ? 1 : 0);
+        return result;
+    }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
index c4e791f..0f6c011 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
@@ -28,14 +28,17 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.invoice.InvoicePluginDispatcher;
 import org.killbill.billing.invoice.dao.InvoiceDao;
 import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
 import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.model.DefaultInvoice;
 import org.killbill.billing.invoice.model.InvoiceItemFactory;
 import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
 import org.killbill.billing.util.UUIDs;
@@ -76,11 +79,24 @@ public class InvoiceApiHelper {
     }
 
     public List<InvoiceItem> dispatchToInvoicePluginsAndInsertItems(final UUID accountId, final boolean isDryRun, final WithAccountLock withAccountLock, final CallContext context) throws InvoiceApiException {
+        // Invoked by User API call
+        final LocalDate targetDate = null;
+        final List<Invoice> existingInvoices = null;
+        final boolean isRescheduled = false;
+
+        final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
+        final DateTime rescheduleDate = invoicePluginDispatcher.priorCall(targetDate, existingInvoices, isDryRun, isRescheduled, context, internalTenantContext);
+        if (rescheduleDate != null) {
+            throw new InvoiceApiException(ErrorCode.INVOICE_PLUGIN_API_ABORTED, "delayed scheduling is unsupported for API calls");
+        }
+
+        boolean success = false;
         GlobalLock lock = null;
+        Iterable<Invoice> invoicesForPlugins = null;
         try {
             lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
 
-            final Iterable<Invoice> invoicesForPlugins = withAccountLock.prepareInvoices();
+            invoicesForPlugins = withAccountLock.prepareInvoices();
 
             final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
             final List<InvoiceModelDao> invoiceModelDaos = new LinkedList<InvoiceModelDao>();
@@ -100,6 +116,8 @@ public class InvoiceApiHelper {
             }
 
             final List<InvoiceItemModelDao> createdInvoiceItems = dao.createInvoices(invoiceModelDaos, internalCallContext);
+            success = true;
+
             return fromInvoiceItemModelDao(createdInvoiceItems);
         } catch (final LockFailedException e) {
             log.warn("Failed to process invoice items for accountId='{}'", accountId.toString(), e);
@@ -108,6 +126,15 @@ public class InvoiceApiHelper {
             if (lock != null) {
                 lock.release();
             }
+
+            if (success) {
+                for (final Invoice invoiceForPlugin : invoicesForPlugins) {
+                    final DefaultInvoice refreshedInvoice = new DefaultInvoice(dao.getById(invoiceForPlugin.getId(), internalTenantContext));
+                    invoicePluginDispatcher.onSuccessCall(targetDate, refreshedInvoice, existingInvoices, isDryRun, isRescheduled, context, internalTenantContext);
+                }
+            } else {
+                invoicePluginDispatcher.onFailureCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, context, internalTenantContext);
+            }
         }
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 29784f4..29668fb 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -230,7 +230,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
                                             final CallContext context) throws InvoiceApiException {
         final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(accountId, context);
 
-        final Invoice result = dispatcher.processAccount(true, accountId, targetDate, dryRunArguments, internalContext);
+        final Invoice result = dispatcher.processAccount(true, accountId, targetDate, dryRunArguments, false, internalContext);
         if (result == null) {
             throw new InvoiceApiException(ErrorCode.INVOICE_NOTHING_TO_DO, accountId, targetDate != null ? targetDate : "null");
         } else {
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 c51221b..1283805 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
@@ -1000,13 +1000,14 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         });
     }
 
-    private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
-                                             final FutureAccountNotifications callbackDateTimePerSubscriptions, final InternalCallContext internalCallContext) {
-
+    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);
+            nextBillingDatePoster.insertNextBillingNotificationFromTransaction(entitySqlDaoWrapperFactory, accountId, subscriptionIds, notificationDateTime, callbackDateTimePerSubscriptions.isRescheduled(), internalCallContext);
         }
 
         final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(internalCallContext).getMillis();
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 fa42a24..71accaa 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -116,6 +116,7 @@ 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.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
@@ -214,27 +215,21 @@ public class InvoiceDispatcher {
         } catch (final CatalogApiException e) {
             log.warn("Failed to retrieve BillingEvents for accountId='{}'", accountId, e);
         }
-
-
-
-
     }
 
-
-
     public void processSubscriptionForInvoiceGeneration(final EffectiveSubscriptionInternalEvent transition,
                                                         final InternalCallContext context) throws InvoiceApiException {
         final UUID subscriptionId = transition.getSubscriptionId();
         final LocalDate targetDate = context.toLocalDate(transition.getEffectiveTransitionTime());
-        processSubscriptionForInvoiceGeneration(subscriptionId, targetDate, context);
+        processSubscriptionForInvoiceGeneration(subscriptionId, targetDate, false, context);
     }
 
-    public void processSubscriptionForInvoiceGeneration(final UUID subscriptionId, final LocalDate targetDate, final InternalCallContext context) throws InvoiceApiException {
-        processSubscriptionInternal(subscriptionId, targetDate, false, context);
+    public void processSubscriptionForInvoiceGeneration(final UUID subscriptionId, final LocalDate targetDate, final boolean isRescheduled, final InternalCallContext context) throws InvoiceApiException {
+        processSubscriptionInternal(subscriptionId, targetDate, false, isRescheduled, context);
     }
 
     public void processSubscriptionForInvoiceNotification(final UUID subscriptionId, final LocalDate targetDate, final InternalCallContext context) throws InvoiceApiException {
-        final Invoice dryRunInvoice = processSubscriptionInternal(subscriptionId, targetDate, true, context);
+        final Invoice dryRunInvoice = processSubscriptionInternal(subscriptionId, targetDate, true, false, context);
         if (dryRunInvoice != null && dryRunInvoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
             final InvoiceNotificationInternalEvent event = new DefaultInvoiceNotificationInternalEvent(dryRunInvoice.getAccountId(), dryRunInvoice.getBalance(), dryRunInvoice.getCurrency(),
                                                                                                        context.toUTCDateTime(targetDate), context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
@@ -246,7 +241,7 @@ public class InvoiceDispatcher {
         }
     }
 
-    private Invoice processSubscriptionInternal(final UUID subscriptionId, final LocalDate targetDate, final boolean dryRunForNotification, final InternalCallContext context) throws InvoiceApiException {
+    private Invoice processSubscriptionInternal(final UUID subscriptionId, final LocalDate targetDate, final boolean dryRunForNotification, final boolean isRescheduled, final InternalCallContext context) throws InvoiceApiException {
         try {
             if (subscriptionId == null) {
                 log.warn("Failed handling SubscriptionBase change.", new InvoiceApiException(ErrorCode.INVOICE_INVALID_TRANSITION));
@@ -255,7 +250,7 @@ public class InvoiceDispatcher {
             final UUID accountId = subscriptionApi.getAccountIdFromSubscriptionId(subscriptionId, context);
             final DryRunArguments dryRunArguments = dryRunForNotification ? TARGET_DATE_DRY_RUN_ARGUMENTS : null;
 
-            return processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, context);
+            return processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, isRescheduled, context);
         } catch (final SubscriptionBaseApiException e) {
             log.warn("Failed handling SubscriptionBase change.",
                      new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, subscriptionId.toString()));
@@ -266,6 +261,7 @@ public class InvoiceDispatcher {
     public Invoice processAccountFromNotificationOrBusEvent(final UUID accountId,
                                                             @Nullable final LocalDate targetDate,
                                                             @Nullable final DryRunArguments dryRunArguments,
+                                                            final boolean isRescheduled,
                                                             final InternalCallContext context) throws InvoiceApiException {
         if (!invoiceConfig.isInvoicingSystemEnabled(context)) {
             log.warn("Invoicing system is off, parking accountId='{}'", accountId);
@@ -273,13 +269,14 @@ public class InvoiceDispatcher {
             return null;
         }
 
-        return processAccount(false, accountId, targetDate, dryRunArguments, context);
+        return processAccount(false, accountId, targetDate, dryRunArguments, isRescheduled, context);
     }
 
     public Invoice processAccount(final boolean isApiCall,
                                   final UUID accountId,
                                   @Nullable final LocalDate targetDate,
                                   @Nullable final DryRunArguments dryRunArguments,
+                                  final boolean isRescheduled,
                                   final InternalCallContext context) throws InvoiceApiException {
         boolean parkedAccount = false;
         try {
@@ -296,7 +293,7 @@ public class InvoiceDispatcher {
         try {
             lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
 
-            return processAccountWithLock(parkedAccount, accountId, targetDate, dryRunArguments, context);
+            return processAccountWithLock(parkedAccount, accountId, targetDate, dryRunArguments, isRescheduled, context);
         } catch (final LockFailedException e) {
             log.warn("Failed to process invoice for accountId='{}', targetDate='{}'", accountId.toString(), targetDate, e);
         } finally {
@@ -311,6 +308,7 @@ public class InvoiceDispatcher {
                                            final UUID accountId,
                                            @Nullable final LocalDate inputTargetDateMaybeNull,
                                            @Nullable final DryRunArguments dryRunArguments,
+                                           final boolean isRescheduled,
                                            final InternalCallContext context) throws InvoiceApiException {
         final boolean isDryRun = dryRunArguments != null;
         final boolean upcomingInvoiceDryRun = isDryRun && DryRunType.UPCOMING_INVOICE.equals(dryRunArguments.getDryRunType());
@@ -340,9 +338,9 @@ public class InvoiceDispatcher {
                                                                                                                 return new DefaultInvoice(input);
                                                                                                             }
                                                                                                         }));
-            Invoice invoice;
+            final Invoice invoice;
             if (!isDryRun) {
-                invoice = processAccountWithLockAndInputTargetDate(accountId, inputTargetDate, billingEvents, existingInvoices, false, context);
+                invoice = processAccountWithLockAndInputTargetDate(accountId, inputTargetDate, billingEvents, existingInvoices, false, isRescheduled, context);
                 if (parkedAccount) {
                     try {
                         log.info("Illegal invoicing state fixed for accountId='{}', unparking account", accountId);
@@ -392,6 +390,10 @@ public class InvoiceDispatcher {
             log.warn("Failed to retrieve BillingEvents for accountId='{}', dryRunArguments='{}'", accountId, dryRunArguments, e);
             return null;
         } catch (final InvoiceApiException e) {
+            if (e.getCode() == ErrorCode.INVOICE_PLUGIN_API_ABORTED.getCode()) {
+                return null;
+            }
+
             if (e.getCode() == ErrorCode.UNEXPECTED_ERROR.getCode() && !isDryRun) {
                 log.warn("Illegal invoicing state detected for accountId='{}', dryRunArguments='{}', parking account", accountId, dryRunArguments, e);
                 parkAccount(accountId, context);
@@ -428,7 +430,7 @@ public class InvoiceDispatcher {
 
     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);
+            final Invoice invoice = processAccountWithLockAndInputTargetDate(accountId, curTargetDate, billingEvents, existingInvoices, true, false, context);
             if (invoice != null) {
                 return invoice;
             }
@@ -459,14 +461,14 @@ public class InvoiceDispatcher {
 
         // 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) :
+                                          processAccountWithLockAndInputTargetDate(accountId, prevLocalDate, billingEvents, existingInvoices, true, false, 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);
+        final Invoice targetInvoice = processAccountWithLockAndInputTargetDate(accountId, targetDate, billingEvents, augmentedExistingInvoices, true, false, context);
         // If our targetDate -- user specified -- did not align with any boundary, we return previous 'additionalInvoice' invoice
         return targetInvoice != null ? targetInvoice : additionalInvoice;
     }
@@ -508,12 +510,27 @@ public class InvoiceDispatcher {
                                                              final BillingEventSet billingEvents,
                                                              final List<Invoice> existingInvoices,
                                                              final boolean isDryRun,
+                                                             final boolean isRescheduled,
                                                              final InternalCallContext internalCallContext) throws InvoiceApiException {
+        final CallContext callContext = buildCallContext(internalCallContext);
+
         final ImmutableAccountData account;
         try {
             account = accountApi.getImmutableAccountDataById(accountId, internalCallContext);
         } catch (final AccountApiException e) {
             log.error("Unable to generate invoice for accountId='{}', a future notification has NOT been recorded", accountId, e);
+            invoicePluginDispatcher.onFailureCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
+            return null;
+        }
+
+        final DateTime rescheduleDate = invoicePluginDispatcher.priorCall(targetDate, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
+        if (rescheduleDate != null) {
+            if (isDryRun) {
+                log.warn("Ignoring rescheduleDate='{}', delayed scheduling is unsupported in dry-run", rescheduleDate);
+            } else {
+                final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(rescheduleDate, billingEvents, internalCallContext);
+                commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, internalCallContext);
+            }
             return null;
         }
 
@@ -525,6 +542,8 @@ public class InvoiceDispatcher {
 
         // If invoice comes back null, there is nothing new to generate, we can bail early
         if (invoice == null) {
+            invoicePluginDispatcher.onSuccessCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
+
             if (isDryRun) {
                 log.info("Generated null dryRun invoice for accountId='{}', targetDate='{}'", accountId, targetDate);
             } else {
@@ -551,7 +570,6 @@ public class InvoiceDispatcher {
             //
             // Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
             //
-            final CallContext callContext = buildCallContext(internalCallContext);
             final List<InvoiceItem> additionalInvoiceItemsFromPlugins = invoicePluginDispatcher.getAdditionalInvoiceItems(tmpInvoiceForInvoicePlugins, isDryRun, callContext, internalCallContext);
             if (additionalInvoiceItemsFromPlugins.isEmpty()) {
                 // PERF: avoid re-computing the CBA if no change was made
@@ -559,7 +577,6 @@ public class InvoiceDispatcher {
                     invoice.addInvoiceItem(cbaItemPreInvoicePlugins);
                 }
             } else {
-
                 // Add or update items from generated invoice
                 for (final InvoiceItem cur : additionalInvoiceItemsFromPlugins) {
                     final InvoiceItem exitingItem = Iterables.tryFind(tmpInvoiceForInvoicePlugins.getInvoiceItems(), new Predicate<InvoiceItem>() {
@@ -605,7 +622,6 @@ public class InvoiceDispatcher {
             }
 
             if (!isDryRun) {
-
                 // 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());
@@ -634,6 +650,13 @@ public class InvoiceDispatcher {
             if (!isDryRun && !success) {
                 commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, internalCallContext);
             }
+
+            if (success) {
+                final DefaultInvoice refreshedInvoice = new DefaultInvoice(invoiceDao.getById(invoice.getId(), internalCallContext));
+                invoicePluginDispatcher.onSuccessCall(targetDate, refreshedInvoice, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
+            } else {
+                invoicePluginDispatcher.onFailureCall(targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalCallContext);
+            }
         }
 
         return invoice;
@@ -657,17 +680,37 @@ public class InvoiceDispatcher {
         return generator.generateInvoice(account, billingEvents, existingInvoices, targetInvoiceId, targetDate, account.getCurrency(), context);
     }
 
+    private FutureAccountNotifications createNextFutureNotificationDate(final DateTime rescheduleDate, final BillingEventSet billingEvents, final InternalCallContext context) {
+        final FutureAccountNotificationsBuilder notificationsBuilder = new FutureAccountNotificationsBuilder();
+        notificationsBuilder.setRescheduled(true);
+
+        final Set<UUID> subscriptionIds = ImmutableSet.<UUID>copyOf(Iterables.<BillingEvent, UUID>transform(billingEvents,
+                                                                                                            new Function<BillingEvent, UUID>() {
+                                                                                                                @Override
+                                                                                                                public UUID apply(final BillingEvent billingEvent) {
+                                                                                                                    return billingEvent.getSubscription().getId();
+                                                                                                                }
+                                                                                                            }));
+        populateNextFutureNotificationDate(rescheduleDate, subscriptionIds, notificationsBuilder, context);
+
+        // Even though a plugin forced us to reschedule the invoice generation, honor the dry run notifications settings
+        populateNextFutureDryRunNotificationDate(billingEvents, notificationsBuilder, context);
 
+        return notificationsBuilder.build();
+    }
 
-    private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final InternalCallContext context) {
+    private void populateNextFutureNotificationDate(final DateTime notificationDateTime, final Set<UUID> subscriptionIds, final FutureAccountNotificationsBuilder notificationsBuilder, final InternalCallContext context) {
+        final LocalDate notificationDate = context.toLocalDate(notificationDateTime);
+        notificationsBuilder.setNotificationListForTrigger(ImmutableMap.<LocalDate, Set<UUID>>of(notificationDate, subscriptionIds));
+    }
 
+    private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final InternalCallContext context) {
         final FutureAccountNotificationsBuilder notificationsBuilder = new FutureAccountNotificationsBuilder();
         populateNextFutureNotificationDate(invoiceWithMetadata, notificationsBuilder);
         populateNextFutureDryRunNotificationDate(billingEvents, notificationsBuilder, context);
         return notificationsBuilder.build();
     }
 
-
     private void populateNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final FutureAccountNotificationsBuilder notificationsBuilder) {
         final Map<LocalDate, Set<UUID>> notificationListForTrigger = new HashMap<LocalDate, Set<UUID>>();
 
@@ -857,14 +900,16 @@ public class InvoiceDispatcher {
 
         private final Map<LocalDate, Set<UUID>> notificationListForTrigger;
         private final Map<LocalDate, Set<UUID>> notificationListForDryRun;
+        private final boolean isRescheduled;
 
         public FutureAccountNotifications() {
-            this(ImmutableMap.<LocalDate, Set<UUID>>of(), ImmutableMap.<LocalDate, Set<UUID>>of());
+            this(ImmutableMap.<LocalDate, Set<UUID>>of(), ImmutableMap.<LocalDate, Set<UUID>>of(), false);
         }
 
-        public FutureAccountNotifications(final Map<LocalDate, Set<UUID>> notificationListForTrigger, final Map<LocalDate, Set<UUID>> notificationListForDryRun) {
+        public FutureAccountNotifications(final Map<LocalDate, Set<UUID>> notificationListForTrigger, final Map<LocalDate, Set<UUID>> notificationListForDryRun, final boolean isRescheduled) {
             this.notificationListForTrigger = notificationListForTrigger;
             this.notificationListForDryRun = notificationListForDryRun;
+            this.isRescheduled = isRescheduled;
         }
 
         public Map<LocalDate, Set<UUID>> getNotificationsForTrigger() {
@@ -875,12 +920,15 @@ public class InvoiceDispatcher {
             return notificationListForDryRun;
         }
 
-
+        public boolean isRescheduled() {
+            return isRescheduled;
+        }
 
         public static class FutureAccountNotificationsBuilder {
 
             private Map<LocalDate, Set<UUID>> notificationListForTrigger;
             private Map<LocalDate, Set<UUID>> notificationListForDryRun;
+            private boolean isRescheduled = false;
 
             public FutureAccountNotificationsBuilder() {
             }
@@ -893,6 +941,10 @@ public class InvoiceDispatcher {
                 this.notificationListForDryRun = notificationListForDryRun;
             }
 
+            public void setRescheduled(final boolean rescheduled) {
+                isRescheduled = rescheduled;
+            }
+
             public Map<LocalDate, Set<UUID>> getNotificationListForTrigger() {
                 return MoreObjects.firstNonNull(notificationListForTrigger, ImmutableMap.<LocalDate, Set<UUID>>of());
             }
@@ -901,8 +953,12 @@ public class InvoiceDispatcher {
                 return MoreObjects.firstNonNull(notificationListForDryRun, ImmutableMap.<LocalDate, Set<UUID>>of());
             }
 
+            public boolean isRescheduled() {
+                return isRescheduled;
+            }
+
             public FutureAccountNotifications build() {
-                return new FutureAccountNotifications(getNotificationListForTrigger(), getNotificationListForDryRun());
+                return new FutureAccountNotifications(getNotificationListForTrigger(), getNotificationListForDryRun(), isRescheduled());
             }
         }
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index eee8749..a57f621 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -109,7 +109,7 @@ public class InvoiceListener extends RetryableService implements InvoiceListener
                                                  try {
                                                      final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
                                                      final UUID accountId = accountApi.getByRecordId(event.getSearchKey1(), context);
-                                                     dispatcher.processAccountFromNotificationOrBusEvent(accountId, null, null, context);
+                                                     dispatcher.processAccountFromNotificationOrBusEvent(accountId, null, null, false, context);
                                                  } catch (final InvoiceApiException e) {
                                                      log.warn("Unable to process event {}", event, e);
                                                  } catch (final AccountApiException e) {
@@ -206,12 +206,10 @@ public class InvoiceListener extends RetryableService implements InvoiceListener
         retryableSubscriber.handleEvent(event);
     }
 
-
-
-    public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+    public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final boolean isRescheduled, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
         final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
         try {
-            dispatcher.processSubscriptionForInvoiceGeneration(subscriptionId, context.toLocalDate(eventDateTime), context);
+            dispatcher.processSubscriptionForInvoiceGeneration(subscriptionId, context.toLocalDate(eventDateTime), isRescheduled, context);
         } catch (final InvoiceApiException e) {
             log.warn("Unable to process subscriptionId='{}', eventDateTime='{}'", subscriptionId, eventDateTime, e);
         }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
index 45b12f8..d1abded 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -19,20 +19,27 @@ package org.killbill.billing.invoice;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.inject.Inject;
 
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.DefaultInvoiceContext;
 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.DefaultInvoice;
+import org.killbill.billing.invoice.plugin.api.InvoiceContext;
 import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
+import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.callcontext.CallContext;
@@ -48,14 +55,13 @@ public class InvoicePluginDispatcher {
     private static final Logger log = LoggerFactory.getLogger(InvoicePluginDispatcher.class);
 
     public static final Collection<InvoiceItemType> ALLOWED_INVOICE_ITEM_TYPES = ImmutableList.<InvoiceItemType>of(InvoiceItemType.EXTERNAL_CHARGE,
-                                                                                                                    InvoiceItemType.ITEM_ADJ,
-                                                                                                                    InvoiceItemType.CREDIT_ADJ,
-                                                                                                                    InvoiceItemType.TAX);
+                                                                                                                   InvoiceItemType.ITEM_ADJ,
+                                                                                                                   InvoiceItemType.CREDIT_ADJ,
+                                                                                                                   InvoiceItemType.TAX);
 
     private final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry;
     private final InvoiceConfig invoiceConfig;
 
-
     @Inject
     public InvoicePluginDispatcher(final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry,
                                    final InvoiceConfig invoiceConfig) {
@@ -63,15 +69,102 @@ public class InvoicePluginDispatcher {
         this.invoiceConfig = invoiceConfig;
     }
 
+    public DateTime priorCall(final LocalDate targetDate, final List<Invoice> existingInvoices, final boolean isDryRun, final boolean isRescheduled, final CallContext callContext, final InternalTenantContext internalTenantContext) throws InvoiceApiException {
+        log.debug("Invoking invoice plugins priorCall: targetDate='{}', isDryRun='{}', isRescheduled='{}'", targetDate, isDryRun, isRescheduled);
+        final Map<String, InvoicePluginApi> invoicePlugins = getInvoicePlugins(internalTenantContext);
+        if (invoicePlugins.isEmpty()) {
+            return null;
+        }
+
+        DateTime earliestRescheduleDate = null;
+        final InvoiceContext invoiceContext = new DefaultInvoiceContext(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext);
+        for (final String invoicePluginName : invoicePlugins.keySet()) {
+            final PriorInvoiceResult priorInvoiceResult = invoicePlugins.get(invoicePluginName).priorCall(invoiceContext, ImmutableList.<PluginProperty>of());
+            log.debug("Invoice plugin {} returned priorInvoiceResult='{}'", invoicePluginName, priorInvoiceResult);
+            if (priorInvoiceResult == null) {
+                // Naughty plugin...
+                continue;
+            }
+
+            if (priorInvoiceResult.getRescheduleDate() != null &&
+                (earliestRescheduleDate == null || earliestRescheduleDate.compareTo(priorInvoiceResult.getRescheduleDate()) > 0)) {
+                earliestRescheduleDate = priorInvoiceResult.getRescheduleDate();
+                log.info("Invoice plugin {} rescheduled invoice generation to {} for targetDate {}", invoicePluginName, earliestRescheduleDate, targetDate);
+            }
+
+            if (priorInvoiceResult.isAborted()) {
+                log.info("Invoice plugin {} aborted invoice generation for targetDate {}", invoicePluginName, targetDate);
+                throw new InvoiceApiException(ErrorCode.INVOICE_PLUGIN_API_ABORTED, invoicePluginName);
+            }
+        }
+
+        return earliestRescheduleDate;
+    }
+
+    public void onSuccessCall(final LocalDate targetDate,
+                              final DefaultInvoice invoice,
+                              final List<Invoice> existingInvoices,
+                              final boolean isDryRun,
+                              final boolean isRescheduled,
+                              final CallContext callContext,
+                              final InternalTenantContext internalTenantContext) {
+        log.debug("Invoking invoice plugins onSuccessCall: targetDate='{}', isDryRun='{}', isRescheduled='{}', invoice='{}'", targetDate, isDryRun, isRescheduled, invoice);
+        onCompletionCall(true, targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalTenantContext);
+    }
+
+    public void onFailureCall(final LocalDate targetDate,
+                              final DefaultInvoice invoice,
+                              final List<Invoice> existingInvoices,
+                              final boolean isDryRun,
+                              final boolean isRescheduled,
+                              final CallContext callContext,
+                              final InternalTenantContext internalTenantContext) {
+        log.debug("Invoking invoice plugins onFailureCall: targetDate='{}', isDryRun='{}', isRescheduled='{}', invoice='{}'", targetDate, isDryRun, isRescheduled, invoice);
+        onCompletionCall(false, targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, internalTenantContext);
+    }
+
+    private void onCompletionCall(final boolean isSuccess,
+                                  final LocalDate targetDate,
+                                  final DefaultInvoice originalInvoice,
+                                  final List<Invoice> existingInvoices,
+                                  final boolean isDryRun,
+                                  final boolean isRescheduled,
+                                  final CallContext callContext,
+                                  final InternalTenantContext internalTenantContext) {
+        final Collection<InvoicePluginApi> invoicePlugins = getInvoicePlugins(internalTenantContext).values();
+        if (invoicePlugins.isEmpty()) {
+            return;
+        }
+
+        // We clone the original invoice so plugins don't remove/add items
+        final Invoice clonedInvoice = (Invoice) originalInvoice.clone();
+        final InvoiceContext invoiceContext = new DefaultInvoiceContext(targetDate, clonedInvoice, existingInvoices, isDryRun, isRescheduled, callContext);
+
+        for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
+            if (isSuccess) {
+                invoicePlugin.onSuccessCall(invoiceContext, ImmutableList.<PluginProperty>of());
+            } else {
+                invoicePlugin.onFailureCall(invoiceContext, ImmutableList.<PluginProperty>of());
+            }
+        }
+    }
+
     //
     // If we have multiple plugins there is a question of plugin ordering and also a 'product' questions to decide whether
     // subsequent plugins should have access to items added by previous plugins
     //
     public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice originalInvoice, final boolean isDryRun, final CallContext callContext, final InternalTenantContext tenantContext) throws InvoiceApiException {
+        log.debug("Invoking invoice plugins getAdditionalInvoiceItems: isDryRun='{}', originalInvoice='{}'", isDryRun, originalInvoice);
+
+        final List<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();
+
+        final Collection<InvoicePluginApi> invoicePlugins = getInvoicePlugins(tenantContext).values();
+        if (invoicePlugins.isEmpty()) {
+            return additionalInvoiceItems;
+        }
+
         // We clone the original invoice so plugins don't remove/add items
         final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
-        final List<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();
-        final List<InvoicePluginApi> invoicePlugins = getInvoicePlugins(tenantContext);
         for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
             final List<InvoiceItem> items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
             if (items != null) {
@@ -91,15 +184,13 @@ public class InvoicePluginDispatcher {
         }
     }
 
-    private List<InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {
-
-
+    private Map<String, InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {
         final Collection<String> resultingPluginList = getResultingPluginNameList(tenantContext);
 
-        final List<InvoicePluginApi> invoicePlugins = new ArrayList<InvoicePluginApi>();
+        final Map<String, InvoicePluginApi> invoicePlugins = new HashMap<String, InvoicePluginApi>();
         for (final String name : resultingPluginList) {
             final InvoicePluginApi serviceForName = pluginRegistry.getServiceForName(name);
-            invoicePlugins.add(serviceForName);
+            invoicePlugins.put(name, serviceForName);
         }
         return invoicePlugins;
     }
@@ -112,7 +203,7 @@ public class InvoicePluginDispatcher {
         if (configuredPlugins == null || configuredPlugins.isEmpty()) {
             return registeredPlugins;
         } else {
-            final List<String> result  =  new ArrayList<String>(configuredPlugins.size());
+            final List<String> result = new ArrayList<String>(configuredPlugins.size());
             for (final String name : configuredPlugins) {
                 if (pluginRegistry.getServiceForName(name) != null) {
                     result.add(name);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
index afb7750..98eb563 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -107,7 +107,7 @@ public class InvoiceTagHandler extends RetryableService implements KillbillServi
 
     private void processUnpaid_AUTO_INVOICING_OFF_invoices(final UUID accountId, final InternalCallContext context) {
         try {
-            dispatcher.processAccountFromNotificationOrBusEvent(accountId, null, null, context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, null, null, false, context);
         } catch (final InvoiceApiException e) {
             log.warn("Failed to process tag removal AUTO_INVOICING_OFF for accountId='{}'", accountId, e);
         }
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 47dc673..98ee28a 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
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -94,9 +94,10 @@ public class DefaultNextBillingDateNotifier extends RetryableService implements 
                         key.isDryRunForInvoiceNotification()) {
                         processEventForInvoiceNotification(firstSubscriptionId, targetDate, userToken, accountRecordId, tenantRecordId);
                     } else {
-                        processEventForInvoiceGeneration(firstSubscriptionId, targetDate, userToken, accountRecordId, tenantRecordId);
+                        final boolean isRescheduled = key.isRescheduled() == Boolean.TRUE; // Handle null value (old versions < 0.19.7)
+                        processEventForInvoiceGeneration(firstSubscriptionId, targetDate, isRescheduled, userToken, accountRecordId, tenantRecordId);
                     }
-                } catch (SubscriptionBaseApiException e) {
+                } catch (final SubscriptionBaseApiException e) {
                     log.warn("Error retrieving subscriptionId='{}'", firstSubscriptionId, e);
                 }
             }
@@ -127,8 +128,8 @@ public class DefaultNextBillingDateNotifier extends RetryableService implements 
         super.stop();
     }
 
-    private void processEventForInvoiceGeneration(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
-        listener.handleNextBillingDateEvent(subscriptionId, eventDateTime, userToken, accountRecordId, tenantRecordId);
+    private void processEventForInvoiceGeneration(final UUID subscriptionId, final DateTime eventDateTime, final boolean isRescheduled, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+        listener.handleNextBillingDateEvent(subscriptionId, eventDateTime, isRescheduled, userToken, accountRecordId, tenantRecordId);
     }
 
     private void processEventForInvoiceNotification(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
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 6cfcb83..92b816a 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
@@ -35,7 +35,6 @@ 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 {
@@ -56,8 +55,9 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
                                                              final UUID accountId,
                                                              final Iterable<UUID> subscriptionIds,
                                                              final DateTime futureNotificationTime,
+                                                             final boolean isRescheduled,
                                                              final InternalCallContext internalCallContext) {
-        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.FALSE, futureNotificationTime, futureNotificationTime, internalCallContext);
+        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.FALSE, isRescheduled, futureNotificationTime, futureNotificationTime, internalCallContext);
     }
 
     @Override
@@ -67,12 +67,13 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
                                                                    final DateTime futureNotificationTime,
                                                                    final DateTime targetDate,
                                                                    final InternalCallContext internalCallContext) {
-        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.TRUE, futureNotificationTime, targetDate, internalCallContext);
+        insertNextBillingFromTransactionInternal(entitySqlDaoWrapperFactory, subscriptionIds, Boolean.TRUE, null, futureNotificationTime, targetDate, internalCallContext);
     }
 
     private void insertNextBillingFromTransactionInternal(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
                                                           final Iterable<UUID> subscriptionIds,
                                                           final Boolean isDryRunForInvoiceNotification,
+                                                          final Boolean isRescheduled,
                                                           final DateTime futureNotificationTime,
                                                           final DateTime targetDate,
                                                           final InternalCallContext internalCallContext) {
@@ -111,7 +112,7 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
             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);
+                final NextBillingDateNotificationKey newNotificationEvent = new NextBillingDateNotificationKey(null, subscriptionIds, targetDate, isDryRunForInvoiceNotification, isRescheduled);
                 nextBillingQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), futureNotificationTime,
                                                                          newNotificationEvent, internalCallContext.getUserToken(),
                                                                          internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
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 1b64c60..4f23d1d 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
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
  *
@@ -29,27 +31,31 @@ import com.google.common.collect.Iterables;
 
 public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
 
-    private Boolean isDryRunForInvoiceNotification;
-    private DateTime targetDate;
+    private final Boolean isDryRunForInvoiceNotification;
+    private final Boolean isRescheduled;
+    private final DateTime targetDate;
     private final Iterable<UUID> uuidKeys;
 
     @JsonCreator
     public NextBillingDateNotificationKey(@Deprecated @JsonProperty("uuidKey") final UUID uuidKey,
                                           @JsonProperty("uuidKeys") final Iterable<UUID> uuidKeys,
                                           @JsonProperty("targetDate") final DateTime targetDate,
-                                          @JsonProperty("isDryRunForInvoiceNotification") final Boolean isDryRunForInvoiceNotification) {
+                                          @JsonProperty("isDryRunForInvoiceNotification") final Boolean isDryRunForInvoiceNotification,
+                                          @JsonProperty("isRescheduled") final Boolean isRescheduled) {
         super(uuidKey);
         this.uuidKeys = uuidKeys;
         this.targetDate = targetDate;
         this.isDryRunForInvoiceNotification = isDryRunForInvoiceNotification;
+        this.isRescheduled = isRescheduled;
     }
 
-    public NextBillingDateNotificationKey(NextBillingDateNotificationKey existing,
+    public NextBillingDateNotificationKey(final 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();
+        this.isRescheduled = existing.isRescheduled();
     }
 
     @JsonProperty("isDryRunForInvoiceNotification")
@@ -57,6 +63,11 @@ public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
         return isDryRunForInvoiceNotification;
     }
 
+    @JsonProperty("isRescheduled")
+    public Boolean isRescheduled() {
+        return isRescheduled;
+    }
+
     public DateTime getTargetDate() {
         return targetDate;
     }
@@ -69,4 +80,51 @@ public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
             return uuidKeys;
         }
     }
+
+    @Override
+    public String toString() {
+        final StringBuffer sb = new StringBuffer("NextBillingDateNotificationKey{");
+        sb.append("isDryRunForInvoiceNotification=").append(isDryRunForInvoiceNotification);
+        sb.append(", isRescheduled=").append(isRescheduled);
+        sb.append(", targetDate=").append(targetDate);
+        sb.append(", uuidKeys=").append(uuidKeys);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        final NextBillingDateNotificationKey that = (NextBillingDateNotificationKey) o;
+
+        if (isDryRunForInvoiceNotification != null ? !isDryRunForInvoiceNotification.equals(that.isDryRunForInvoiceNotification) : that.isDryRunForInvoiceNotification != null) {
+            return false;
+        }
+        if (isRescheduled != null ? !isRescheduled.equals(that.isRescheduled) : that.isRescheduled != null) {
+            return false;
+        }
+        if (targetDate != null ? targetDate.compareTo(that.targetDate) != 0 : that.targetDate != null) {
+            return false;
+        }
+        return uuidKeys != null ? uuidKeys.equals(that.uuidKeys) : that.uuidKeys == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (isDryRunForInvoiceNotification != null ? isDryRunForInvoiceNotification.hashCode() : 0);
+        result = 31 * result + (isRescheduled != null ? isRescheduled.hashCode() : 0);
+        result = 31 * result + (targetDate != null ? targetDate.hashCode() : 0);
+        result = 31 * result + (uuidKeys != null ? uuidKeys.hashCode() : 0);
+        return result;
+    }
 }
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 d2dce63..f4b4a03 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,7 +27,7 @@ import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
 public interface NextBillingDatePoster {
 
     void insertNextBillingNotificationFromTransaction(EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, UUID accountId,
-                                                      Iterable<UUID> subscriptionId, DateTime futureNotificationTime, InternalCallContext internalCallContext);
+                                                      Iterable<UUID> subscriptionId, DateTime futureNotificationTime, final boolean isRescheduled, InternalCallContext internalCallContext);
 
     void insertNextBillingDryRunNotificationFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final UUID accountId,
                                                             final Iterable<UUID> subscriptionId, final DateTime futureNotificationTime, final DateTime targetDate, final InternalCallContext internalCallContext);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java
index 503d5e9..30e4371 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java
@@ -1,7 +1,8 @@
 /*
- * Copyright 2014 Groupon, Inc
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
- * Groupon licenses this file to you under the Apache License, version 2.0
+ * 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:
  *
@@ -20,10 +21,13 @@ import java.util.List;
 
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.plugin.api.InvoiceContext;
 import org.killbill.billing.invoice.plugin.api.NoOpInvoicePluginApi;
+import org.killbill.billing.invoice.plugin.api.OnFailureInvoiceResult;
+import org.killbill.billing.invoice.plugin.api.OnSuccessInvoiceResult;
+import org.killbill.billing.invoice.plugin.api.PriorInvoiceResult;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.callcontext.CallContext;
-import org.killbill.clock.Clock;
 
 import com.google.common.collect.ImmutableList;
 import com.google.inject.Inject;
@@ -35,7 +39,22 @@ public class DefaultNoOpInvoiceProviderPlugin implements NoOpInvoicePluginApi {
     }
 
     @Override
-    public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> properties, CallContext context) {
+    public PriorInvoiceResult priorCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> properties) {
+        return null;
+    }
+
+    @Override
+    public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> properties, final CallContext context) {
         return ImmutableList.<InvoiceItem>of();
     }
+
+    @Override
+    public OnSuccessInvoiceResult onSuccessCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> properties) {
+        return null;
+    }
+
+    @Override
+    public OnFailureInvoiceResult onFailureCall(final InvoiceContext invoiceContext, final Iterable<PluginProperty> properties) {
+        return null;
+    }
 }
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 b837bdf..0e6e941 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
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -38,7 +38,7 @@ public class TestNextBillingDateNotificationKey {
         final DateTime targetDate = new DateTime();
         final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
 
-        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(uuidKey, null, targetDate, isDryRunForInvoiceNotification);
+        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(uuidKey, null, targetDate, isDryRunForInvoiceNotification, false);
         final String json = mapper.writeValueAsString(key);
 
         final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
@@ -56,7 +56,7 @@ public class TestNextBillingDateNotificationKey {
         final DateTime targetDate = new DateTime();
         final Boolean isDryRunForInvoiceNotification = Boolean.FALSE;
 
-        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(null, ImmutableList.of(uuidKey1, uuidKey2), targetDate, isDryRunForInvoiceNotification);
+        final NextBillingDateNotificationKey key = new NextBillingDateNotificationKey(null, ImmutableList.of(uuidKey1, uuidKey2), targetDate, isDryRunForInvoiceNotification, false);
         final String json = mapper.writeValueAsString(key);
 
         final NextBillingDateNotificationKey result = mapper.readValue(json, NextBillingDateNotificationKey.class);
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 b620fab..fe11d11 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
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -49,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(null, ImmutableList.<UUID>of(subscriptionId), now, Boolean.FALSE), internalCallContext.getUserToken(), accountRecordId, internalCallContext.getTenantRecordId());
+        nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(null, ImmutableList.<UUID>of(subscriptionId), now, Boolean.FALSE, 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/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index ea9001c..c4db83f 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -104,21 +104,21 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
                                                                    internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
 
-        Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
+        Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), false, context);
         Assert.assertNotNull(invoice);
 
         List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(false, context);
         Assert.assertEquals(invoices.size(), 0);
 
         // Try it again to double check
-        invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
+        invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), false, context);
         Assert.assertNotNull(invoice);
 
         invoices = invoiceDao.getInvoicesByAccount(false, context);
         Assert.assertEquals(invoices.size(), 0);
 
         // This time no dry run
-        invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, context);
+        invoice = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, false, context);
         Assert.assertNotNull(invoice);
 
         invoices = invoiceDao.getInvoicesByAccount(false, context);
@@ -194,7 +194,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceModelDao), context);
 
         try {
-            dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), false, context);
             Assert.fail();
         } catch (final InvoiceApiException e) {
             Assert.assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
@@ -205,7 +205,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertTrue(tagUserApi.getTagsForAccount(accountId, true, callContext).isEmpty());
 
         try {
-            dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, context);
+            dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, false, context);
             Assert.fail();
         } catch (final InvoiceApiException e) {
             Assert.assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
@@ -218,12 +218,12 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         Assert.assertEquals(tags.get(0).getTagDefinitionId(), SystemTags.PARK_TAG_DEFINITION_ID);
 
         // isApiCall=false
-        final Invoice nullInvoice1 = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, context);
+        final Invoice nullInvoice1 = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, null, false, context);
         Assert.assertNull(nullInvoice1);
 
         // No dry-run and isApiCall=true
         try {
-            dispatcher.processAccount(true, accountId, target, null, context);
+            dispatcher.processAccount(true, accountId, target, null, false, context);
             Assert.fail();
         } catch (final InvoiceApiException e) {
             Assert.assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
@@ -244,18 +244,18 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         });
 
         // Dry-run and isApiCall=false: still parked
-        final Invoice nullInvoice2 = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), context);
+        final Invoice nullInvoice2 = dispatcher.processAccountFromNotificationOrBusEvent(accountId, target, new DryRunFutureDateArguments(), false, context);
         Assert.assertNull(nullInvoice2);
 
         // Dry-run and isApiCall=true: call goes through
-        final Invoice invoice1 = dispatcher.processAccount(true, accountId, target, new DryRunFutureDateArguments(), context);
+        final Invoice invoice1 = dispatcher.processAccount(true, accountId, target, new DryRunFutureDateArguments(), false, context);
         Assert.assertNotNull(invoice1);
         Assert.assertEquals(invoiceDao.getInvoicesByAccount(false, context).size(), 0);
         // Dry-run: still parked
         Assert.assertEquals(tagUserApi.getTagsForAccount(accountId, false, callContext).size(), 1);
 
         // No dry-run and isApiCall=true: call goes through
-        final Invoice invoice2 = dispatcher.processAccount(true, accountId, target, null, context);
+        final Invoice invoice2 = dispatcher.processAccount(true, accountId, target, null, false, context);
         Assert.assertNotNull(invoice2);
         Assert.assertEquals(invoiceDao.getInvoicesByAccount(false, context).size(), 1);
         // No dry-run: now unparked
@@ -293,7 +293,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
                                                                    internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
-        final Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), new LocalDate("2012-07-30"), null, context);
+        final Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), new LocalDate("2012-07-30"), null, false, context);
         Assert.assertNotNull(invoice);
 
         final List<InvoiceItem> invoiceItems = invoice.getInvoiceItems();
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 f5e9bd1..30d07e7 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -238,7 +238,7 @@ public class TestInvoiceHelper {
                                                                    invoiceDao, internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
 
-        return dispatcher.processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, internalCallContext);
+        return dispatcher.processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, false, internalCallContext);
     }
 
     public SubscriptionBase createSubscription() throws SubscriptionBaseApiException {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java
index ef72b84..3d8a454 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
  *
@@ -21,17 +23,16 @@ import java.util.UUID;
 import javax.inject.Inject;
 
 import org.joda.time.DateTime;
-
 import org.killbill.billing.account.api.AccountInternalApi;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
-import org.killbill.clock.Clock;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
 import org.killbill.notificationq.api.NotificationQueueService;
 
 public class TestInvoiceNotificationQListener extends InvoiceListener {
 
-    int eventCount = 0;
-    UUID latestSubscriptionId = null;
+    private int eventCount = 0;
+    private UUID latestSubscriptionId = null;
 
     @Inject
     public TestInvoiceNotificationQListener(final AccountInternalApi accountApi,
@@ -44,7 +45,7 @@ public class TestInvoiceNotificationQListener extends InvoiceListener {
     }
 
     @Override
-    public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+    public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final boolean isRescheduled, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
         eventCount++;
         latestSubscriptionId = subscriptionId;
     }
@@ -56,5 +57,4 @@ public class TestInvoiceNotificationQListener extends InvoiceListener {
     public UUID getLatestSubscriptionId() {
         return latestSubscriptionId;
     }
-
 }

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 6f1bea2..3521af4 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.46</version>
+        <version>0.141.47</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.19.7-SNAPSHOT</version>