killbill-aplcache

invoice: Invoice Plugin Api Enhancements - initial changes See

3/20/2018 6:41:16 AM

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..16835c6 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
@@ -31,11 +31,13 @@ import javax.inject.Inject;
 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 +78,21 @@ 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);
+        invoicePluginDispatcher.priorCall(targetDate, existingInvoices, isDryRun, isRescheduled, context, internalTenantContext);
+
+        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 +112,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 +122,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/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index fa42a24..b936c5a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -509,11 +509,17 @@ public class InvoiceDispatcher {
                                                              final List<Invoice> existingInvoices,
                                                              final boolean isDryRun,
                                                              final InternalCallContext internalCallContext) throws InvoiceApiException {
+        final boolean isRescheduled = false; // TODO
+
+        final CallContext callContext = buildCallContext(internalCallContext);
+        invoicePluginDispatcher.priorCall(targetDate, existingInvoices, isDryRun, isRescheduled, callContext, 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;
         }
 
@@ -525,6 +531,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 +559,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 +566,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 +611,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 +639,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;
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..2c3a6b1 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
@@ -25,13 +25,16 @@ import java.util.Set;
 
 import javax.inject.Inject;
 
+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.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
@@ -48,14 +51,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 +65,78 @@ public class InvoicePluginDispatcher {
         this.invoiceConfig = invoiceConfig;
     }
 
+    public void priorCall(final LocalDate targetDate, final List<Invoice> existingInvoices, final boolean isDryRun, final boolean isRescheduled, final CallContext callContext, final InternalTenantContext internalTenantContext) {
+        final List<InvoicePluginApi> invoicePlugins = getInvoicePlugins(internalTenantContext);
+        if (invoicePlugins.isEmpty()) {
+            return;
+        }
+
+        final InvoiceContext invoiceContext = new DefaultInvoiceContext(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext);
+        for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
+            invoicePlugin.priorCall(invoiceContext, ImmutableList.<PluginProperty>of());
+        }
+    }
+
+    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) {
+        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) {
+        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 List<InvoicePluginApi> invoicePlugins = getInvoicePlugins(internalTenantContext);
+        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 {
-        // 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);
+        if (invoicePlugins.isEmpty()) {
+            return additionalInvoiceItems;
+        }
+
+        // We clone the original invoice so plugins don't remove/add items
+        final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
         for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
             final List<InvoiceItem> items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
             if (items != null) {
@@ -93,7 +158,6 @@ public class InvoicePluginDispatcher {
 
     private List<InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {
 
-
         final Collection<String> resultingPluginList = getResultingPluginNameList(tenantContext);
 
         final List<InvoicePluginApi> invoicePlugins = new ArrayList<InvoicePluginApi>();
@@ -112,7 +176,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/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;
+    }
 }

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 77e2b9d..33d1295 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.44</version>
+        <version>0.141.45-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.19.7-SNAPSHOT</version>