killbill-aplcache

invoice: Add config property to have a deterministic invoice

10/11/2017 8:07:14 PM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index c52b981..1f06e65 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -1014,6 +1014,16 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
         }
 
         @Override
+        public List<String> getInvoicePluginNames() {
+            return defaultInvoiceConfig.getInvoicePluginNames();
+        }
+
+        @Override
+        public List<String> getInvoicePluginNames(final InternalTenantContext tenantContext) {
+            return defaultInvoiceConfig.getInvoicePluginNames();
+        }
+
+        @Override
         public boolean isEmailNotificationsEnabled() {
             return defaultInvoiceConfig.isEmailNotificationsEnabled();
         }
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 047f073..5b8cbe7 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
@@ -82,10 +82,11 @@ public class InvoiceApiHelper {
 
             final Iterable<Invoice> invoicesForPlugins = withAccountLock.prepareInvoices();
 
+            final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
             final List<InvoiceModelDao> invoiceModelDaos = new LinkedList<InvoiceModelDao>();
             for (final Invoice invoiceForPlugin : invoicesForPlugins) {
                 // Call plugin
-                final List<InvoiceItem> additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, isDryRun, context);
+                final List<InvoiceItem> additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, isDryRun, context, internalCallContext);
                 invoiceForPlugin.addInvoiceItems(additionalInvoiceItems);
 
                 // Transformation to InvoiceModelDao
@@ -98,7 +99,6 @@ public class InvoiceApiHelper {
                 invoiceModelDaos.add(invoiceModelDao);
             }
 
-            final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
             final List<InvoiceItemModelDao> createdInvoiceItems = dao.createInvoices(invoiceModelDaos, internalCallContext);
             return fromInvoiceItemModelDao(createdInvoiceItems);
         } catch (final LockFailedException e) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
index 45de5f6..ba2e1e0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
@@ -17,6 +17,8 @@
 
 package org.killbill.billing.invoice.config;
 
+import java.util.List;
+
 import javax.inject.Inject;
 import javax.inject.Named;
 
@@ -119,6 +121,20 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
     }
 
     @Override
+    public List<String> getInvoicePluginNames() {
+        return staticConfig.getInvoicePluginNames();
+    }
+
+    @Override
+    public List<String> getInvoicePluginNames(final InternalTenantContext tenantContext) {
+        final String result = getStringTenantConfig("getInvoicePluginNames", tenantContext);
+        if (result != null) {
+            return convertToListString(result, "getInvoicePluginNames");
+        }
+        return getInvoicePluginNames();
+    }
+
+    @Override
     public boolean isInvoicingSystemEnabled() {
         return staticConfig.isInvoicingSystemEnabled();
     }
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 d55ebac..86f9c90 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -363,20 +363,20 @@ public class InvoiceDispatcher {
     }
 
     private Invoice processAccountWithLockAndInputTargetDate(final UUID accountId, final LocalDate targetDate,
-                                                             final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext context) throws InvoiceApiException {
+                                                             final BillingEventSet billingEvents, final boolean isDryRun, final InternalCallContext internalCallContext) throws InvoiceApiException {
         final ImmutableAccountData account;
         try {
-            account = accountApi.getImmutableAccountDataById(accountId, context);
+            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);
             return null;
         }
 
-        final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, context);
+        final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, internalCallContext);
         final DefaultInvoice invoice = invoiceWithMetadata.getInvoice();
 
         // Compute future notifications
-        final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, context);
+        final FutureAccountNotifications futureAccountNotifications = createNextFutureNotificationDate(invoiceWithMetadata, internalCallContext);
 
         // If invoice comes back null, there is nothing new to generate, we can bail early
         if (invoice == null) {
@@ -386,9 +386,9 @@ public class InvoiceDispatcher {
                 log.info("Generated null invoice for accountId='{}', targetDate='{}'", accountId, targetDate);
 
                 final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(),
-                                                                           context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+                                                                           internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), internalCallContext.getUserToken());
 
-                commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, context);
+                commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, internalCallContext);
                 postEvent(event);
             }
             return null;
@@ -397,7 +397,7 @@ public class InvoiceDispatcher {
         boolean success = false;
         try {
             // Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin
-            final InvoiceItem cbaItemPreInvoicePlugins = computeCBAOnExistingInvoice(invoice, context);
+            final InvoiceItem cbaItemPreInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext);
             DefaultInvoice tmpInvoiceForInvoicePlugins = invoice;
             if (cbaItemPreInvoicePlugins != null) {
                 tmpInvoiceForInvoicePlugins = (DefaultInvoice) tmpInvoiceForInvoicePlugins.clone();
@@ -406,8 +406,8 @@ public class InvoiceDispatcher {
             //
             // Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
             //
-            final CallContext callContext = buildCallContext(context);
-            final List<InvoiceItem> additionalInvoiceItemsFromPlugins = invoicePluginDispatcher.getAdditionalInvoiceItems(tmpInvoiceForInvoicePlugins, isDryRun, callContext);
+            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
                 if (cbaItemPreInvoicePlugins != null) {
@@ -416,7 +416,7 @@ public class InvoiceDispatcher {
             } else {
                 invoice.addInvoiceItems(additionalInvoiceItemsFromPlugins);
                 // Use credit after we call the plugin (https://github.com/killbill/killbill/issues/637)
-                final InvoiceItem cbaItemPostInvoicePlugins = computeCBAOnExistingInvoice(invoice, context);
+                final InvoiceItem cbaItemPostInvoicePlugins = computeCBAOnExistingInvoice(invoice, internalCallContext);
                 if (cbaItemPostInvoicePlugins != null) {
                     invoice.addInvoiceItem(cbaItemPostInvoicePlugins);
                 }
@@ -437,11 +437,11 @@ public class InvoiceDispatcher {
                 invoiceModelDao.addInvoiceItems(invoiceItemModelDaos);
 
                 // Commit invoice on disk
-                commitInvoiceAndSetFutureNotifications(account, invoiceModelDao, futureAccountNotifications, context);
+                commitInvoiceAndSetFutureNotifications(account, invoiceModelDao, futureAccountNotifications, internalCallContext);
                 success = true;
 
                 try {
-                    setChargedThroughDates(invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), context);
+                    setChargedThroughDates(invoice.getInvoiceItems(FixedPriceInvoiceItem.class), invoice.getInvoiceItems(RecurringInvoiceItem.class), internalCallContext);
                 } catch (final SubscriptionBaseApiException e) {
                     log.error("Failed handling SubscriptionBase change.", e);
                     return null;
@@ -450,7 +450,7 @@ public class InvoiceDispatcher {
         } finally {
             // Make sure we always set future notifications in case of errors
             if (!isDryRun && !success) {
-                commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, context);
+                commitInvoiceAndSetFutureNotifications(account, null, futureAccountNotifications, internalCallContext);
             }
         }
 
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 82c62f5..2d175e4 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
@@ -21,10 +21,12 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import javax.inject.Inject;
 
 import org.killbill.billing.ErrorCode;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceItem;
@@ -34,9 +36,11 @@ import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.config.definition.InvoiceConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 
 public class InvoicePluginDispatcher {
@@ -49,21 +53,25 @@ public class InvoicePluginDispatcher {
                                                                                                                     InvoiceItemType.TAX);
 
     private final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry;
+    private final InvoiceConfig invoiceConfig;
+
 
     @Inject
-    public InvoicePluginDispatcher(final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry) {
+    public InvoicePluginDispatcher(final OSGIServiceRegistration<InvoicePluginApi> pluginRegistry,
+                                   final InvoiceConfig invoiceConfig) {
         this.pluginRegistry = pluginRegistry;
+        this.invoiceConfig = invoiceConfig;
     }
 
     //
     // 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) throws InvoiceApiException {
+    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();
+        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) {
@@ -83,11 +91,34 @@ public class InvoicePluginDispatcher {
         }
     }
 
-    private List<InvoicePluginApi> getInvoicePlugins() {
+    private List<InvoicePluginApi> getInvoicePlugins(final InternalTenantContext tenantContext) {
+
+
+        final Collection<String> resultingPluginList = getResultingPluginNameList(tenantContext);
+
         final List<InvoicePluginApi> invoicePlugins = new ArrayList<InvoicePluginApi>();
-        for (final String name : pluginRegistry.getAllServices()) {
-            invoicePlugins.add(pluginRegistry.getServiceForName(name));
+        for (final String name : resultingPluginList) {
+            final InvoicePluginApi serviceForName = pluginRegistry.getServiceForName(name);
+            invoicePlugins.add(serviceForName);
         }
         return invoicePlugins;
     }
+
+    @VisibleForTesting
+    final Collection<String> getResultingPluginNameList(final InternalTenantContext tenantContext) {
+        final List<String> configuredPlugins = invoiceConfig.getInvoicePluginNames(tenantContext);
+        final Set<String> registeredPlugins = pluginRegistry.getAllServices();
+        // No configuration, we return undeterministic list of registered plugins
+        if (configuredPlugins == null || configuredPlugins.isEmpty()) {
+            return registeredPlugins;
+        } else {
+            final List<String> result  =  new ArrayList<String>(configuredPlugins.size());
+            for (final String name : configuredPlugins) {
+                if (pluginRegistry.getServiceForName(name) != null) {
+                    result.add(name);
+                }
+            }
+            return result;
+        }
+    }
 }
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 6b2883d..503d5e9 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
@@ -30,11 +30,8 @@ import com.google.inject.Inject;
 
 public class DefaultNoOpInvoiceProviderPlugin implements NoOpInvoicePluginApi {
 
-    private final Clock clock;
-
     @Inject
-    public DefaultNoOpInvoiceProviderPlugin(final Clock clock) {
-        this.clock = clock;
+    public DefaultNoOpInvoiceProviderPlugin() {
     }
 
     @Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/provider/NoOpInvoiceProviderPluginProvider.java b/invoice/src/main/java/org/killbill/billing/invoice/provider/NoOpInvoiceProviderPluginProvider.java
index 1901ffb..c4444af 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/provider/NoOpInvoiceProviderPluginProvider.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/provider/NoOpInvoiceProviderPluginProvider.java
@@ -45,7 +45,7 @@ public class NoOpInvoiceProviderPluginProvider implements Provider<DefaultNoOpIn
     @Override
     public DefaultNoOpInvoiceProviderPlugin get() {
 
-        final DefaultNoOpInvoiceProviderPlugin plugin = new DefaultNoOpInvoiceProviderPlugin(clock);
+        final DefaultNoOpInvoiceProviderPlugin plugin = new DefaultNoOpInvoiceProviderPlugin();
         final OSGIServiceDescriptor desc = new OSGIServiceDescriptor() {
             @Override
             public String getPluginSymbolicName() {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoicePluginDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoicePluginDispatcher.java
new file mode 100644
index 0000000..78d1b74
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoicePluginDispatcher.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
+import org.killbill.billing.invoice.provider.DefaultNoOpInvoiceProviderPlugin;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestInvoicePluginDispatcher extends InvoiceTestSuiteNoDB {
+
+    private final String PLUGIN_1 = "plugin1";
+    private final String PLUGIN_2 = "plugin2";
+    private final String PLUGIN_3 = "plugin3";
+
+    @Inject
+    protected InvoicePluginDispatcher invoicePluginDispatcher;
+    @Inject
+    OSGIServiceRegistration<InvoicePluginApi> pluginRegistry;
+
+    @Inject
+    TenantInternalApi tenantInternalApi;
+
+    protected KillbillConfigSource getConfigSource() {
+        return getConfigSource("/resource.properties", ImmutableMap.<String, String>builder()
+                .put("org.killbill.invoice.plugin", Joiner.on(",").join(PLUGIN_1, PLUGIN_2))
+                .build());
+    }
+
+    @Override
+    @BeforeMethod(groups = "fast")
+    public void beforeMethod() {
+        super.beforeMethod();
+        for (final String name : pluginRegistry.getAllServices()) {
+            pluginRegistry.unregisterService(name);
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testWithNoConfig() throws Exception {
+
+        // We Use the per-tenant config and specify a empty list of plugins
+        Mockito.when(tenantInternalApi.getTenantConfig(Mockito.any(InternalCallContext.class))).thenReturn("{\"org.killbill.invoice.plugin\":\"\"}");
+        // We register one plugin
+        registerPlugin(PLUGIN_1);
+
+        final Collection<String> result = invoicePluginDispatcher.getResultingPluginNameList(internalCallContext);
+        // Se expect to seee the list of registered plugins
+        assertEquals(result.size(), 1);
+        final Iterator<String> iterator = result.iterator();
+        assertEquals(iterator.next(), PLUGIN_1);
+    }
+
+    @Test(groups = "fast")
+    public void testWithNoRegistration() throws Exception {
+        // Nothing has been registered, we see nothing
+        final Collection<String> result = invoicePluginDispatcher.getResultingPluginNameList(internalCallContext);
+        assertEquals(result.size(), 0);
+    }
+
+    @Test(groups = "fast")
+    public void testWithCorrectOrder() throws Exception {
+        // 3 plugins registered in correct order but only 2 got specified in config
+        registerPlugin(PLUGIN_1);
+        registerPlugin(PLUGIN_2);
+        registerPlugin(PLUGIN_3);
+
+        final Collection<String> result = invoicePluginDispatcher.getResultingPluginNameList(internalCallContext);
+        assertEquals(result.size(), 2);
+        final Iterator<String> iterator = result.iterator();
+        assertEquals(iterator.next(), PLUGIN_1);
+        assertEquals(iterator.next(), PLUGIN_2);
+    }
+
+    @Test(groups = "fast")
+    public void testWithIncorrectCorrectOrder() throws Exception {
+
+        // 3 plugins registered in *incorrect* order and  only 2 got specified in config
+        registerPlugin(PLUGIN_2);
+        registerPlugin(PLUGIN_3);
+        registerPlugin(PLUGIN_1);
+
+        final Collection<String> result = invoicePluginDispatcher.getResultingPluginNameList(internalCallContext);
+        assertEquals(result.size(), 2);
+        final Iterator<String> iterator = result.iterator();
+        assertEquals(iterator.next(), PLUGIN_1);
+        assertEquals(iterator.next(), PLUGIN_2);
+    }
+
+
+    private void registerPlugin(final String plugin) {
+        pluginRegistry.registerService(new OSGIServiceDescriptor() {
+            @Override
+            public String getPluginSymbolicName() {
+                return plugin;
+            }
+
+            @Override
+            public String getPluginName() {
+                return plugin;
+            }
+
+            @Override
+            public String getRegistrationName() {
+                return plugin;
+            }
+        }, new DefaultNoOpInvoiceProviderPlugin());
+    }
+}
\ No newline at end of file
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
index 8170bd6..7ed6ac9 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
@@ -17,6 +17,8 @@
 
 package org.killbill.billing.util.config.definition;
 
+import java.util.List;
+
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.skife.config.Config;
 import org.skife.config.Default;
@@ -81,6 +83,16 @@ public interface InvoiceConfig extends KillbillConfig {
     @Description("Maximum number of times the system will retry to grab global lock (with a 100ms wait each time)")
     int getMaxGlobalLockRetries();
 
+    @Config("org.killbill.invoice.plugin")
+    @Default("")
+    @Description("Default invoice plugin names")
+    List<String> getInvoicePluginNames();
+
+    @Config("org.killbill.invoice.plugin")
+    @Default("")
+    @Description("Default invoice plugin names")
+    List<String> getInvoicePluginNames(@Param("dummy") final InternalTenantContext tenantContext);
+
     @Config("org.killbill.invoice.emailNotificationsEnabled")
     @Default("false")
     @Description("Whether to send email notifications on invoice creation (for configured accounts)")
diff --git a/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java b/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java
index 39562ee..10c9e38 100644
--- a/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java
+++ b/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java
@@ -108,7 +108,11 @@ public abstract class MultiTenantConfigBase {
 
     private List<String> getTokens(final Method method, final String value) {
         final Separator separator = method.getAnnotation(Separator.class);
-        return ImmutableList.copyOf(value.split(separator == null ? Separator.DEFAULT : separator.value()));
+        if (value == null || value.isEmpty()) {
+            return ImmutableList.of();
+        } else {
+            return ImmutableList.copyOf(value.split(separator == null ? Separator.DEFAULT : separator.value()));
+        }
     }
 
     protected Method getConfigStaticMethod(final String methodName) {